From f120111ae50b0151af03584327815d0d0e66ae8f Mon Sep 17 00:00:00 2001 From: Nikolay Vlasov Date: Fri, 22 Dec 2023 17:00:39 +1100 Subject: [PATCH 01/75] Initial commit for Base --- lib/base/.gitignore | 11 + lib/base/.npmignore | 6 + lib/base/README.md | 239 +++++++ lib/base/app.ts | 34 + lib/base/cdk.json | 57 ++ .../doc/assets/Architecture-SingleNode-v3.jpg | Bin 0 -> 48320 bytes .../doc/assets/Architecture-SingleNode.drawio | 77 +++ lib/base/jest.config.js | 8 + .../lib/amb-ethereum-single-node-stack.ts | 43 ++ .../lib/assets/cfn-hup/cfn-auto-reloader.conf | 4 + lib/base/lib/assets/cfn-hup/cfn-hup.conf | 5 + lib/base/lib/assets/cfn-hup/cfn-hup.service | 8 + lib/base/lib/assets/cw-agent.json | 76 +++ lib/base/lib/assets/node-cw-dashboard.ts | 235 +++++++ lib/base/lib/assets/restore-from-snapshot.sh | 21 + .../assets/sync-checker/syncchecker-base.sh | 24 + lib/base/lib/assets/user-data/node.sh | 228 +++++++ lib/base/lib/common-stack.ts | 71 ++ lib/base/lib/config/baseConfig.interface.ts | 22 + lib/base/lib/config/baseConfig.ts | 38 ++ .../constructs/base-node-security-group.ts | 34 + lib/base/lib/single-node-stack.ts | 136 ++++ lib/base/package-lock.json | 641 ++++++++++++++++++ lib/base/package.json | 20 + lib/base/sample-configs/.env-sample-rpc | 20 + lib/base/test/.env-test | 22 + lib/base/test/base-ethereum-l1-node.test.ts | 57 ++ lib/base/test/base-single-node.test.ts | 123 ++++ lib/base/tsconfig.json | 31 + 29 files changed, 2291 insertions(+) create mode 100644 lib/base/.gitignore create mode 100644 lib/base/.npmignore create mode 100644 lib/base/README.md create mode 100644 lib/base/app.ts create mode 100644 lib/base/cdk.json create mode 100644 lib/base/doc/assets/Architecture-SingleNode-v3.jpg create mode 100644 lib/base/doc/assets/Architecture-SingleNode.drawio create mode 100644 lib/base/jest.config.js create mode 100644 lib/base/lib/amb-ethereum-single-node-stack.ts create mode 100644 lib/base/lib/assets/cfn-hup/cfn-auto-reloader.conf create mode 100644 lib/base/lib/assets/cfn-hup/cfn-hup.conf create mode 100644 lib/base/lib/assets/cfn-hup/cfn-hup.service create mode 100644 lib/base/lib/assets/cw-agent.json create mode 100644 lib/base/lib/assets/node-cw-dashboard.ts create mode 100644 lib/base/lib/assets/restore-from-snapshot.sh create mode 100644 lib/base/lib/assets/sync-checker/syncchecker-base.sh create mode 100644 lib/base/lib/assets/user-data/node.sh create mode 100644 lib/base/lib/common-stack.ts create mode 100644 lib/base/lib/config/baseConfig.interface.ts create mode 100644 lib/base/lib/config/baseConfig.ts create mode 100644 lib/base/lib/constructs/base-node-security-group.ts create mode 100644 lib/base/lib/single-node-stack.ts create mode 100644 lib/base/package-lock.json create mode 100644 lib/base/package.json create mode 100644 lib/base/sample-configs/.env-sample-rpc create mode 100644 lib/base/test/.env-test create mode 100644 lib/base/test/base-ethereum-l1-node.test.ts create mode 100644 lib/base/test/base-single-node.test.ts create mode 100644 lib/base/tsconfig.json diff --git a/lib/base/.gitignore b/lib/base/.gitignore new file mode 100644 index 00000000..0304fefb --- /dev/null +++ b/lib/base/.gitignore @@ -0,0 +1,11 @@ +*.js +!jest.config.js +*.d.ts +node_modules + +# CDK asset staging directory +.cdk.staging +cdk.out +.idea + +*-node.json diff --git a/lib/base/.npmignore b/lib/base/.npmignore new file mode 100644 index 00000000..c1d6d45d --- /dev/null +++ b/lib/base/.npmignore @@ -0,0 +1,6 @@ +*.ts +!*.d.ts + +# CDK asset staging directory +.cdk.staging +cdk.out diff --git a/lib/base/README.md b/lib/base/README.md new file mode 100644 index 00000000..dac10bb6 --- /dev/null +++ b/lib/base/README.md @@ -0,0 +1,239 @@ +# Sample AWS Blockchain Node Runner app for Base Nodes + +[Base](https://base.org/) is a "Layer 2" scaling solution for Ethereum. This blueprint helps to deploy Base RPC nodes on AWS and use [Amazon Managed Blockchain Access Ethereum](https://docs.aws.amazon.com/managed-blockchain/latest/ethereum-dev/ethereum-concepts.html) node for "Layer 1". It is meant to be used for development, testing or Proof of Concept purposes. + +## Overview of Deployment Architectures for Single Node setups + +### Single node setup + +![Single Node Deployment](./doc/assets/Architecture-SingleNode-v3.jpg) + +1. A Base node deployed in the [Default VPC](https://docs.aws.amazon.com/vpc/latest/userguide/default-vpc.html) continuously synchronizes with the rest of nodes on Base blockchain network through [Internet Gateway](https://docs.aws.amazon.com/vpc/latest/userguide/VPC_Internet_Gateway.html). +2. The Base node is used by dApps or development tools internally from within the Default VPC. JSON RPC API is not exposed to the Internet directly to protect nodes from unauthorized access. +3. You will need access to a fully-synced Ethereum Mainnet RPC endpoint before running. +4. The Base node sends various monitoring metrics for both EC2 and Base nodes to Amazon CloudWatch. + + +## Additional materials + +
+ +Review the for pros and cons of this solution. + +### Well-Architected Checklist + +This is the Well-Architected checklist for AWS Blockchain Node Runner app. This checklist takes into account questions from the [AWS Well-Architected Framework](https://aws.amazon.com/architecture/well-architected/) which are relevant to this workload. Please feel free to add more checks from the framework if required for your workload. + +| Pillar | Control | Question/Check | Remarks | +|:------------------------|:----------------------------------|:---------------------------------------------------------------------------------|:-----------------| +| Security | Network protection | Are there unnecessary open ports in security groups? | Please note that ports 9222 (TCP/UDP) for Base are open to public to support P2P protocols. We have to rely on the protection mechanisms built into the Base software to protect those ports. | +| | | Traffic inspection | AWS WAF could be implemented for traffic inspection. Additional charges will apply. | +| | Compute protection | Reduce attack surface | This solution uses Amazon Linux AMI. You may choose to run hardening scripts on it. | +| | | Enable people to perform actions at a distance | This solution uses AWS Systems Manager for terminal session, not ssh ports. | +| | Data protection at rest | Use encrypted Amazon Elastic Block Store (Amazon EBS) volumes | This solution uses encrypted Amazon EBS volumes. | +| | Data protection in transit | Use TLS | By design TLS is not used in Base RPC and P2P protocols because the data is considered public. To protect RPC traffic we expose the port only for internal use. | +| | Authorization and access control | Use instance profile with Amazon Elastic Compute Cloud (Amazon EC2) instances | This solution uses AWS Identity and Access Management (AWS IAM) role instead of IAM user. | +| | | Following principle of least privilege access | In the node, root user is not used (using special user "ubuntu" instead). | +| | Application security | Security focused development practices | cdk-nag is being used with documented suppressions. | +| Cost optimization | Service selection | Use cost effective resources | Base nodes currently doesn't provide binaries for ARM architecture, so AMD-powered EC2 instance type for better cost effectiveness. | +| | Cost awareness | Estimate costs | One Base node on m6a.2xlarge and 1T EBS gp3 volume will cost around US$367.21 per month in the US East (N. Virginia) region. Additionally the AMB Access Ethereum on bc.m5.xlarge will cost additional ~US$202 per month in the US East (N. Virginia) region. Approximately the total cost will be US$367.21 + US$202 = US$569.21 per month. | +| Reliability | Resiliency implementation | Withstand component failures | This solution currently does not have high availability and is deployed to a single availability zone. | +| | Data backup | How is data backed up? | The data is not specially backed up. The node will have to re-sync its state from other nodes in the Base network to recover. | +| | Resource monitoring | How are workload resources monitored? | Resources are being monitored using Amazon CloudWatch dashboards. Amazon CloudWatch custom metrics are being pushed via CloudWatch Agent. | +| Performance efficiency | Compute selection | How is compute solution selected? | Compute solution is selected based on the recommendations the from Base community to provide stable and cost-effective operations. | +| | Storage selection | How is storage solution selected? | Storage solution is selected based on the recommendations the from Base community to provide stable and cost-effective operations. | +| | Architecture selection | How is the best performance architecture selected? | In this solution we try to balance price and performance to achieve better cost efficiency, but not necessarily the best performance. | +| Operational excellence | Workload health | How is health of workload determined? | We rely on the standard EC2 instance monitoring tool to detect stalled instances. | +| Sustainability | Hardware & services | Select most efficient hardware for your workload | Base nodes currently doesn't provide binaries for ARM architecture, so AMD-powered EC2 instance type for better cost effectiveness. | +
+
+Recommended Infrastructure + +## Hardware Requirements + +**Minimum for Base node** + +- Instance type [m6a.xlarge](https://aws.amazon.com/ec2/instance-types/m6a/). +- 2500GB EBS gp3 storage with at least 6000 IOPS. + +**Recommended for Base node** + +- Instance type [m6a.2xlarge](https://aws.amazon.com/ec2/instance-types/m6a/). +- 2500GB EBS gp3 storage with at least 6000 IOPS.` + +**Amazon Managed Blockchain Ethereum L1** + +- Minimum instance type: [bc.m5.xlarge](https://aws.amazon.com/managed-blockchain/instance-types/) +- Recommended instance type: [bc.m5.2xlarge](https://aws.amazon.com/managed-blockchain/instance-types/) + +
+ +## Setup Instructions + +### Setup Cloud9 + +We will use AWS Cloud9 to execute the subsequent commands. Follow the instructions in [Cloud9 Setup](../../docs/setup-cloud9.md) + +### Clone this repository and install dependencies + +```bash + git clone https://github.com/alickwong/aws-blockchain-node-runners + cd aws-blockchain-node-runners + npm install +``` + +### Deploy Single Node + +1. Make sure you are in the root directory of the cloned repository + +2. If you have deleted or don't have the default VPC, create default VPC + + ```bash + aws ec2 create-default-vpc + ``` + + > NOTE: + > You may see the following error if the default VPC already exists: `An error occurred (DefaultVpcAlreadyExists) when calling the CreateDefaultVpc operation: A Default VPC already exists for this account in this region.`. That means you can just continue with the following steps. + +3. Configure your setup + + Create your own copy of `.env` file and edit it to update with your AWS Account ID and Region: + ```bash + # Make sure you are in aws-blockchain-node-runners/lib/base + cd lib/base + npm install + pwd + cp ./sample-configs/.env-sample-rpc .env + nano .env + ``` + > NOTE: + > Example configuration parameters are set in the local `.env-sample` file. You can find more examples inside `sample-configs` directory. + +4. Deploy common components such as IAM role + + ```bash + pwd + # Make sure you are in aws-blockchain-node-runners/lib/base + npx cdk deploy base-common + ``` + + > IMPORTANT: + > All AWS CDK v2 deployments use dedicated AWS resources to hold data during deployment. Therefore, your AWS account and Region must be [bootstrapped](https://docs.aws.amazon.com/cdk/v2/guide/bootstrapping.html) to create these resources before you can deploy. If you haven't already bootstrapped, issue the following command: + > ```bash + > cdk bootstrap aws://ACCOUNT-NUMBER/REGION + > ``` + +5. Deploy Amazon Managed Blockchain (AMB) Access Ethereum node and wait about 35-70 minutes for the node to sync + + ```bash + pwd + # Make sure you are in aws-blockchain-node-runners/lib/base + npx cdk deploy base-ethereum-l1-node --json --outputs-file base-ethereum-l1-node.json + ``` + To watch the progress, open the [AMB Web UI](https://console.aws.amazon.com/managedblockchain/home), click the name of your target network from the list (Mainnet, Goerly, etc.) and watch the status of the node to change from `Creating` to `Available`. + +6. Deploy Base RPC Node and wait for another 10-20 minutes for it to sync + + ```bash + pwd + # Make sure you are in aws-blockchain-node-runners/lib/base + npx cdk deploy base-single-node --json --outputs-file single-node-deploy.json + ``` + After starting the node you will need to wait for the initial synchronization process to finish.To see the progress, you may use SSM to connect into EC2 first and watch the log like this: + + ```bash + export INSTANCE_ID=$(cat single-node-deploy.json | jq -r '..|.node-instance-id? | select(. != null)') + echo "INSTANCE_ID=" $INSTANCE_ID + export AWS_REGION=us-east-1 + aws ssm start-session --target $INSTANCE_ID --region $AWS_REGION + echo Latest synced block behind by: $((($(date +%s)-$( \ + curl -d '{"id":0,"jsonrpc":"2.0","method":"optimism_syncStatus"}' \ + -H "Content-Type: application/json" http://localhost:7545 | \ + jq -r .result.unsafe_l2.timestamp))/60)) minutes + ``` + +7. Test Base RPC API + Use curl to query from within the node instance: + ```bash + export INSTANCE_ID=$(cat single-node-deploy.json | jq -r '..|.node-instance-id? | select(. != null)') + echo "INSTANCE_ID=" $INSTANCE_ID + export AWS_REGION=us-east-1 + aws ssm start-session --target $INSTANCE_ID --region $AWS_REGION + + curl -s -X POST -H "Content-Type: application/json" --data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' http://localhost:8545 + ``` + +### Monitoring +A script on the Base node publishes current block and blocks behind metrics to CloudWatch metrics every 5 minutes. When the node is fully synced the blocks behind metric should get to 0, which might take about 1.5 days. To see the metrics: + +- Navigate to CloudWatch service (make sure you are in the region you have specified for AWS_REGION) +- Open Dashboards and select `base-single-node` from the list of dashboards. + +## Clear up and undeploy everything + +1. Undeploy all Nodes and Common stacks + + ```bash + # Setting the AWS account id and region in case local .env file is lost + export AWS_ACCOUNT_ID= + export AWS_REGION= + + pwd + # Make sure you are in aws-blockchain-node-runners/lib/base + + # Undeploy Single Node + npx cdk destroy base-single-node + + # Undeploy AMB Etheruem node + npx cdk destroy base-ethereum-l1-node + + # Delete all common components like IAM role and Security Group + npx cdk destroy base-common + ``` + +2. Follow steps to delete the Cloud9 instance in [Cloud9 Setup](../../doc/setup-cloud9.md) + +## FAQ + +1. How to check the logs of the clients running on my Base node? + + **Note:** In this tutorial we chose not to use SSH and use Session Manager instead. That allows you to log all sessions in AWS CloudTrail to see who logged into the server and when. If you receive an error similar to `SessionManagerPlugin is not found`, [install Session Manager plugin for AWS CLI](https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-working-with-install-plugin.html) + + ```bash + pwd + # Make sure you are in aws-blockchain-node-runners/lib/base + + export INSTANCE_ID=$(cat single-node-deploy.json | jq -r '..|.node-instance-id? | select(. != null)') + echo "INSTANCE_ID=" $INSTANCE_ID + export AWS_REGION=us-east-1 + aws ssm start-session --target $INSTANCE_ID --region $AWS_REGION + sudo su bcuser + docker logs --tail 50 node_node_1 -f + ``` +2. How to check the logs from the EC2 user-data script? + + ```bash + pwd + # Make sure you are in aws-blockchain-node-runners/lib/base + + export INSTANCE_ID=$(cat single-node-deploy.json | jq -r '..|.node-instance-id? | select(. != null)') + echo "INSTANCE_ID=" $INSTANCE_ID + export AWS_REGION=us-east-1 + aws ssm start-session --target $INSTANCE_ID --region $AWS_REGION + sudo cat /var/log/cloud-init-output.log + ``` + +3. How can I restart the Base node? + + ``` bash + export INSTANCE_ID=$(cat single-node-deploy.json | jq -r '..|.node-instance-id? | select(. != null)') + echo "INSTANCE_ID=" $INSTANCE_ID + export AWS_REGION=us-east-1 + aws ssm start-session --target $INSTANCE_ID --region $AWS_REGION + sudo su bcuser + /usr/local/bin/docker-compose -f /home/bcuser/node/docker-compose.yml down && \ + /usr/local/bin/docker-compose -f /home/bcuser/node/docker-compose.yml up -d + ``` +4. Where to find the key Base client directories? + + - The data directory is `/data` diff --git a/lib/base/app.ts b/lib/base/app.ts new file mode 100644 index 00000000..8eea3432 --- /dev/null +++ b/lib/base/app.ts @@ -0,0 +1,34 @@ +#!/usr/bin/env node +import 'dotenv/config' +import 'source-map-support/register'; +import * as cdk from 'aws-cdk-lib'; +import * as config from "./lib/config/baseConfig"; +import {BaseCommonStack} from "./lib/common-stack"; +import {BaseAMBEthereumSingleNodeStack} from "./lib/amb-ethereum-single-node-stack"; +import {BaseSingleNodeStack} from "./lib/single-node-stack"; + +const app = new cdk.App(); +cdk.Tags.of(app).add("Project", "AWSBase"); + +new BaseCommonStack(app, "base-common", { + stackName: `base-nodes-common`, + env: { account: config.baseConfig.accountId, region: config.baseConfig.region }, +}); + +new BaseAMBEthereumSingleNodeStack(app, "base-ethereum-l1-node", { + stackName: `base-amb-ethereum-single-node-${config.baseNodeConfig.baseNetworkId}`, + env: { account: config.baseConfig.accountId, region: config.baseConfig.region }, + + ambEthereumNodeNetworkId: config.baseNodeConfig.ambEntereumNodeNetworkId, + ambEthereumNodeInstanceType: config.baseNodeConfig.ambEntereumNodeInstanceType, +}); + +new BaseSingleNodeStack(app, "base-single-node", { + stackName: `base-single-node-${config.baseNodeConfig.baseNetworkId}`, + env: { account: config.baseConfig.accountId, region: config.baseConfig.region }, + + instanceType: config.baseNodeConfig.instanceType, + instanceCpuType: config.baseNodeConfig.instanceCpuType, + baseNetworkId: config.baseNodeConfig.baseNetworkId, + dataVolume: config.baseNodeConfig.dataVolume, +}); diff --git a/lib/base/cdk.json b/lib/base/cdk.json new file mode 100644 index 00000000..7714e8c2 --- /dev/null +++ b/lib/base/cdk.json @@ -0,0 +1,57 @@ +{ + "app": "npx ts-node --prefer-ts-exts app.ts", + "watch": { + "include": [ + "**" + ], + "exclude": [ + "README.md", + "cdk*.json", + "**/*.d.ts", + "**/*.js", + "tsconfig.json", + "package*.json", + "yarn.lock", + "node_modules", + "test" + ] + }, + "context": { + "@aws-cdk/aws-lambda:recognizeLayerVersion": true, + "@aws-cdk/core:checkSecretUsage": true, + "@aws-cdk/core:target-partitions": [ + "aws", + "aws-cn" + ], + "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, + "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, + "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, + "@aws-cdk/aws-iam:minimizePolicies": true, + "@aws-cdk/core:validateSnapshotRemovalPolicy": true, + "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, + "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, + "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, + "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, + "@aws-cdk/core:enablePartitionLiterals": true, + "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, + "@aws-cdk/aws-iam:standardizedServicePrincipals": true, + "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, + "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, + "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, + "@aws-cdk/aws-route53-patters:useCertificate": true, + "@aws-cdk/customresources:installLatestAwsSdkDefault": false, + "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, + "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, + "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, + "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, + "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, + "@aws-cdk/aws-redshift:columnId": true, + "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, + "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, + "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, + "@aws-cdk/aws-kms:aliasNameRef": true, + "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true, + "@aws-cdk/core:includePrefixInUniqueNameGeneration": true, + "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true + } +} diff --git a/lib/base/doc/assets/Architecture-SingleNode-v3.jpg b/lib/base/doc/assets/Architecture-SingleNode-v3.jpg new file mode 100644 index 0000000000000000000000000000000000000000..99e05c260cd9207eb7e32f058e667a57c1b5daa5 GIT binary patch literal 48320 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

t<(c_=yk;Cj)sg-%aTeR>0Qe`#?dQ>sp5#p6XO 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 SRqozg5!gIov6im>dFbC-Q80-B literal 0 HcmV?d00001 diff --git a/lib/base/doc/assets/Architecture-SingleNode.drawio b/lib/base/doc/assets/Architecture-SingleNode.drawio new file mode 100644 index 00000000..f6fb4275 --- /dev/null +++ b/lib/base/doc/assets/Architecture-SingleNode.drawio @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/base/jest.config.js b/lib/base/jest.config.js new file mode 100644 index 00000000..08263b89 --- /dev/null +++ b/lib/base/jest.config.js @@ -0,0 +1,8 @@ +module.exports = { + testEnvironment: 'node', + roots: ['/test'], + testMatch: ['**/*.test.ts'], + transform: { + '^.+\\.tsx?$': 'ts-jest' + } +}; diff --git a/lib/base/lib/amb-ethereum-single-node-stack.ts b/lib/base/lib/amb-ethereum-single-node-stack.ts new file mode 100644 index 00000000..614a171f --- /dev/null +++ b/lib/base/lib/amb-ethereum-single-node-stack.ts @@ -0,0 +1,43 @@ +import * as cdk from "aws-cdk-lib"; +import * as cdkConstructs from "constructs"; +import * as configTypes from "./config/baseConfig.interface"; +import { SingleNodeAMBEthereumConstruct } from "../../constructs/amb-ethereum-single-node"; + +export interface BaseAMBEthereumSingleNodeStackProps extends cdk.StackProps { + ambEthereumNodeNetworkId: configTypes.AMBEthereumNodeNetworkId, + ambEthereumNodeInstanceType: string, +} + +export class BaseAMBEthereumSingleNodeStack extends cdk.Stack { + constructor(scope: cdkConstructs.Construct, id: string, props: BaseAMBEthereumSingleNodeStackProps) { + super(scope, id, props); + + // Setting up necessary environment variables + const availabilityZones = cdk.Stack.of(this).availabilityZones; + const chosenAvailabilityZone = availabilityZones.slice(0, 1)[0]; + + // Getting our config from initialization properties + const { + ambEthereumNodeNetworkId, + ambEthereumNodeInstanceType, + } = props; + + // Setting up L1 Ethereum node with AMB Ethereum node construct + + const ambEthereumNode = new SingleNodeAMBEthereumConstruct(this, "base-amb-ethereum-l1-single-node", { + instanceType: ambEthereumNodeInstanceType, + availabilityZone: chosenAvailabilityZone, + ethNetworkId: ambEthereumNodeNetworkId, + }) + + new cdk.CfnOutput(this, "amb-eth-node-id", { + value: ambEthereumNode.nodeId, + exportName: "BaseAmbEthereumNodeId" + }); + + new cdk.CfnOutput(this, "amb-eth-node-rpc-url-billing-token", { + value: ambEthereumNode.rpcUrlWithBillingToken, + exportName: "BaseAmbEthereumNodeRpcUrlWithBillingToken", + }); + } +} diff --git a/lib/base/lib/assets/cfn-hup/cfn-auto-reloader.conf b/lib/base/lib/assets/cfn-hup/cfn-auto-reloader.conf new file mode 100644 index 00000000..3cd32a0a --- /dev/null +++ b/lib/base/lib/assets/cfn-hup/cfn-auto-reloader.conf @@ -0,0 +1,4 @@ +[cfn-auto-reloader-hook] +triggers=post.update +path=Resources.WebServerHost.Metadata.AWS::CloudFormation::Init +action=/opt/aws/bin/cfn-init -v --stack __AWS_STACK_NAME__ --resource WebServerHost --region __AWS_REGION__ diff --git a/lib/base/lib/assets/cfn-hup/cfn-hup.conf b/lib/base/lib/assets/cfn-hup/cfn-hup.conf new file mode 100644 index 00000000..2163b37a --- /dev/null +++ b/lib/base/lib/assets/cfn-hup/cfn-hup.conf @@ -0,0 +1,5 @@ +[main] +stack=__AWS_STACK_ID__ +region=__AWS_REGION__ +# The interval used to check for changes to the resource metadata in minutes. Default is 15 +interval=2 diff --git a/lib/base/lib/assets/cfn-hup/cfn-hup.service b/lib/base/lib/assets/cfn-hup/cfn-hup.service new file mode 100644 index 00000000..2660ea46 --- /dev/null +++ b/lib/base/lib/assets/cfn-hup/cfn-hup.service @@ -0,0 +1,8 @@ +[Unit] +Description=cfn-hup daemon +[Service] +Type=simple +ExecStart=/usr/local/bin/cfn-hup +Restart=always +[Install] +WantedBy=multi-user.target diff --git a/lib/base/lib/assets/cw-agent.json b/lib/base/lib/assets/cw-agent.json new file mode 100644 index 00000000..28833017 --- /dev/null +++ b/lib/base/lib/assets/cw-agent.json @@ -0,0 +1,76 @@ +{ + "agent": { + "metrics_collection_interval": 60, + "run_as_user": "root" + }, + "metrics": { + "aggregation_dimensions": [ + [ + "InstanceId" + ] + ], + "append_dimensions": { + "InstanceId": "${aws:InstanceId}" + }, + "metrics_collected": { + "cpu": { + "measurement": [ + "cpu_usage_idle", + "cpu_usage_iowait", + "cpu_usage_user", + "cpu_usage_system" + ], + "metrics_collection_interval": 60, + "resources": [ + "*" + ], + "totalcpu": false + }, + "disk": { + "measurement": [ + "used_percent" + ], + "metrics_collection_interval": 60, + "resources": [ + "*" + ] + }, + "diskio": { + "measurement": [ + "io_time", + "write_bytes", + "read_bytes", + "writes", + "reads", + "write_time", + "read_time", + "iops_in_progress" + ], + "metrics_collection_interval": 60, + "resources": [ + "*" + ] + }, + "mem": { + "measurement": [ + "mem_used_percent", + "mem_cached" + ], + "metrics_collection_interval": 60 + }, + "netstat": { + "measurement": [ + "tcp_established", + "tcp_time_wait" + ], + "metrics_collection_interval": 60 + }, + "swap": { + "measurement": [ + "swap_used_percent" + ], + "metrics_collection_interval": 60 + } + } + } +} diff --git a/lib/base/lib/assets/node-cw-dashboard.ts b/lib/base/lib/assets/node-cw-dashboard.ts new file mode 100644 index 00000000..fd8b5462 --- /dev/null +++ b/lib/base/lib/assets/node-cw-dashboard.ts @@ -0,0 +1,235 @@ +export const SyncNodeCWDashboardJSON = { + "widgets": [ + { + "height": 5, + "width": 6, + "y": 0, + "x": 0, + "type": "metric", + "properties": { + "view": "timeSeries", + "stat": "Average", + "period": 300, + "stacked": false, + "yAxis": { + "left": { + "min": 0 + } + }, + "region": "${REGION}", + "metrics": [ + [ "AWS/EC2", "CPUUtilization", "InstanceId", "${INSTANCE_ID}", { "label": "${INSTANCE_ID}-${INSTANCE_NAME}" } ] + ], + "title": "CPU utilization (%)" + } + }, + { + "height": 5, + "width": 6, + "y": 5, + "x": 18, + "type": "metric", + "properties": { + "view": "timeSeries", + "stat": "Average", + "period": 300, + "stacked": false, + "yAxis": { + "left": { + "min": 0 + } + }, + "region": "${REGION}", + "metrics": [ + [ "AWS/EC2", "NetworkIn", "InstanceId", "${INSTANCE_ID}", { "label": "${INSTANCE_ID}-${INSTANCE_NAME}" } ] + ], + "title": "Network in (bytes)" + } + }, + { + "height": 5, + "width": 6, + "y": 0, + "x": 18, + "type": "metric", + "properties": { + "view": "timeSeries", + "stat": "Average", + "period": 300, + "stacked": false, + "yAxis": { + "left": { + "min": 0 + } + }, + "region": "${REGION}", + "metrics": [ + [ "AWS/EC2", "NetworkOut", "InstanceId", "${INSTANCE_ID}", { "label": "${INSTANCE_ID}-${INSTANCE_NAME}" } ] + ], + "title": "Network out (bytes)" + } + }, + { + "height": 5, + "width": 6, + "y": 10, + "x": 0, + "type": "metric", + "properties": { + "view": "timeSeries", + "stacked": false, + "region": "${REGION}", + "stat": "Average", + "period": 300, + "metrics": [ + [ "CWAgent", "mem_used_percent", "InstanceId", "${INSTANCE_ID}", { "label": "${INSTANCE_ID}-${INSTANCE_NAME}" } ] + ], + "title": "Mem Used (%)" + } + }, + { + "height": 5, + "width": 6, + "y": 5, + "x": 0, + "type": "metric", + "properties": { + "view": "timeSeries", + "stacked": false, + "region": "${REGION}", + "stat": "Average", + "period": 300, + "metrics": [ + [ "CWAgent", "cpu_usage_iowait", "InstanceId", "${INSTANCE_ID}", { "label": "${INSTANCE_ID}-${INSTANCE_NAME}" } ] + ], + "title": "CPU Usage IO wait (%)" + } + }, + { + "height": 5, + "width": 6, + "y": 0, + "x": 6, + "type": "metric", + "properties": { + "metrics": [ + [ { "expression": "m7/PERIOD(m7)", "label": "Read", "id": "e7" } ], + [ "CWAgent", "diskio_reads", "InstanceId", "${INSTANCE_ID}", "name", "nvme1n1", { "id": "m7", "visible": false, "stat": "Sum", "period": 60 } ], + [ { "expression": "m8/PERIOD(m8)", "label": "Write", "id": "e8" } ], + [ "CWAgent", "diskio_writes", "InstanceId", "${INSTANCE_ID}", "name", "nvme1n1", { "id": "m8", "visible": false, "stat": "Sum", "period": 60 } ] + ], + "view": "timeSeries", + "stacked": false, + "region": "${REGION}", + "stat": "Sum", + "period": 60, + "title": "nvme1n1 Volume Read/Write (IO/sec)" + } + }, + { + "height": 5, + "width": 6, + "y": 5, + "x": 12, + "type": "metric", + "properties": { + "metrics": [ + [ "CWAgent", "elc_sync_block", "InstanceId", "${INSTANCE_ID}", { "label": "${INSTANCE_ID}-${INSTANCE_NAME}" } ] + ], + "sparkline": true, + "view": "timeSeries", + "stacked": false, + "region": "${REGION}", + "stat": "Maximum", + "period": 60, + "title": "Base Client Block Height" + } + }, + { + "height": 5, + "width": 6, + "y": 10, + "x": 12, + "type": "metric", + "properties": { + "sparkline": true, + "view": "timeSeries", + "stacked": false, + "region": "${REGION}", + "stat": "Maximum", + "period": 60, + "metrics": [ + [ "CWAgent", "elc_blocks_behind", "InstanceId", "${INSTANCE_ID}", { "label": "${INSTANCE_ID}-${INSTANCE_NAME}" } ] + ], + "title": "Base Client Blocks Behind" + } + }, + { + "height": 5, + "width": 6, + "y": 5, + "x": 6, + "type": "metric", + "properties": { + "view": "timeSeries", + "stat": "Sum", + "period": 60, + "stacked": false, + "yAxis": { + "left": { + "min": 0 + } + }, + "region": "${REGION}", + "metrics": [ + [ { "expression": "IF(m7_2 !=0, (m7_1 / m7_2), 0)", "label": "Read", "id": "e7" } ], + [ "CWAgent", "diskio_read_time", "InstanceId", "${INSTANCE_ID}", "name", "nvme1n1", { "id": "m7_1", "visible": false, "stat": "Sum", "period": 60 } ], + [ "CWAgent", "diskio_reads", "InstanceId", "${INSTANCE_ID}", "name", "nvme1n1", { "id": "m7_2", "visible": false, "stat": "Sum", "period": 60 } ], + [ { "expression": "IF(m7_4 !=0, (m7_3 / m7_4), 0)", "label": "Write", "id": "e8" } ], + [ "CWAgent", "diskio_write_time", "InstanceId", "${INSTANCE_ID}", "name", "nvme1n1", { "id": "m7_3", "visible": false, "stat": "Sum", "period": 60 } ], + [ "CWAgent", "diskio_writes", "InstanceId", "${INSTANCE_ID}", "name", "nvme1n1", { "id": "m7_4", "visible": false, "stat": "Sum", "period": 60 } ] + ], + "title": "nvme1n1 Volume Read/Write latency (ms/op)" + } + }, + { + "height": 5, + "width": 6, + "y": 10, + "x": 6, + "type": "metric", + "properties": { + "metrics": [ + [ { "expression": "(m2/1048576)/PERIOD(m2)", "label": "Read", "id": "e2", "period": 60, "region": "${REGION}" } ], + [ "CWAgent", "diskio_read_bytes", "InstanceId", "${INSTANCE_ID}", "name", "nvme1n1", { "id": "m2", "stat": "Sum", "visible": false, "period": 60 } ], + [ { "expression": "(m3/1048576)/PERIOD(m3)", "label": "Write", "id": "e3", "period": 60, "region": "${REGION}" } ], + [ "CWAgent", "diskio_write_bytes", "InstanceId", "${INSTANCE_ID}", "name", "nvme1n1", { "id": "m3", "stat": "Sum", "visible": false, "period": 60 } ] + ], + "view": "timeSeries", + "stacked": false, + "region": "${REGION}", + "stat": "Average", + "period": 60, + "title": "nvme1n1 Volume Read/Write throughput (MiB/sec)" + } + }, + { + "height": 5, + "width": 6, + "y": 0, + "x": 12, + "type": "metric", + "properties": { + "metrics": [ + [ "CWAgent", "disk_used_percent", "path", "/data", "InstanceId", "${INSTANCE_ID}", "device", "nvme1n1", "fstype", "ext4", { "region": "${REGION}", "label": "/data" } ] + ], + "sparkline": true, + "view": "singleValue", + "region": "${REGION}", + "title": "nvme1n1 Disk Used (%)", + "period": 60, + "stat": "Average" + } + } + ] +} diff --git a/lib/base/lib/assets/restore-from-snapshot.sh b/lib/base/lib/assets/restore-from-snapshot.sh new file mode 100644 index 00000000..0460fda8 --- /dev/null +++ b/lib/base/lib/assets/restore-from-snapshot.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +source /etc/environment +TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600") +INSTANCE_ID=$(curl -H "X-aws-ec2-metadata-token: $TOKEN" -s http://169.254.169.254/latest/meta-data/instance-id) +LATEST_SNAPSHOT_FILE_NAME=$(curl https://base-snapshots-$NETWORK_ID-archive.s3.amazonaws.com/latest) + +echo "Sync started at " $(date) +SECONDS=0 + +s5cmd --log error cp s3://base-snapshots-$NETWORK_ID-archive/$LATEST_SNAPSHOT_FILE_NAME /data && \ +tar -I zstd -xf /data/$LATEST_SNAPSHOT_FILE_NAME -C /data && \ +mv /data/snapshots/$NETWORK_ID/download/* /data && \ +rm -rf /data/snapshots && \ +rm -rf /data/$LATEST_SNAPSHOT_FILE_NAME + +chown -R bdcuser:bdcuser /data && \ +echo "Sync finished at " $(date) && \ +echo "$(($SECONDS / 60)) minutes and $(($SECONDS % 60)) seconds elapsed." && \ +sudo su bdcuser && \ +/usr/local/bin/docker-compose -f /home/bcuser/node/docker-compose.yml up -d \ No newline at end of file diff --git a/lib/base/lib/assets/sync-checker/syncchecker-base.sh b/lib/base/lib/assets/sync-checker/syncchecker-base.sh new file mode 100644 index 00000000..a084df3b --- /dev/null +++ b/lib/base/lib/assets/sync-checker/syncchecker-base.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +BASE_SYNC_STATS=$(curl -s -X POST -H "Content-Type: application/json" --data '{"jsonrpc":"2.0","method":"eth_syncing","params":[],"id":1}' http://localhost:8545 | jq -r ".result") + +if [[ "$BASE_SYNC_STATS" == "false" ]]; then + BASE_SYNC_BLOCK_HEX=$(curl -s -X POST -H "Content-Type: application/json" --data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' http://localhost:8545 | jq -r ".result") + BASE_HIGHEST_BLOCK_HEX=$BASE_SYNC_BLOCK_HEX +else + BASE_SYNC_BLOCK_HEX=$(echo $BASE_SYNC_STATS | jq -r ".currentBlock") + BASE_HIGHEST_BLOCK_HEX=$(echo $BASE_SYNC_STATS | jq -r ".highestBlock") +fi + +BASE_HIGHEST_BLOCK=$(echo $((${BASE_HIGHEST_BLOCK_HEX}))) +BASE_SYNC_BLOCK=$(echo $((${BASE_SYNC_BLOCK_HEX}))) +BASE_BLOCKS_BEHIND="$((BASE_HIGHEST_BLOCK-BASE_SYNC_BLOCK))" + +# Sending data to CloudWatch +TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600") +INSTANCE_ID=$(curl -H "X-aws-ec2-metadata-token: $TOKEN" -s http://169.254.169.254/latest/meta-data/instance-id) +REGION=$(curl -H "X-aws-ec2-metadata-token: $TOKEN" -s http://169.254.169.254/latest/dynamic/instance-identity/document | jq .region -r) +TIMESTAMP=$(date +"%Y-%m-%dT%H:%M:%S%:z") + +aws cloudwatch put-metric-data --metric-name elc_sync_block --namespace CWAgent --value $BASE_SYNC_BLOCK --timestamp $TIMESTAMP --dimensions InstanceId=$INSTANCE_ID --region $REGION +aws cloudwatch put-metric-data --metric-name elc_blocks_behind --namespace CWAgent --value $BASE_BLOCKS_BEHIND --timestamp $TIMESTAMP --dimensions InstanceId=$INSTANCE_ID --region $REGION diff --git a/lib/base/lib/assets/user-data/node.sh b/lib/base/lib/assets/user-data/node.sh new file mode 100644 index 00000000..94d1ad1c --- /dev/null +++ b/lib/base/lib/assets/user-data/node.sh @@ -0,0 +1,228 @@ +#!/bin/bash +set +e + +# Set by generic single-node and ha-node CDK components +LIFECYCLE_HOOK_NAME=${_LIFECYCLE_HOOK_NAME_} +AUTOSCALING_GROUP_NAME=${_AUTOSCALING_GROUP_NAME_} +RESOURCE_ID=${_NODE_CF_LOGICAL_ID_} +ASSETS_S3_PATH=${_ASSETS_S3_PATH_} +echo "LIFECYCLE_HOOK_NAME=$LIFECYCLE_HOOK_NAME" >> /etc/environment +echo "AUTOSCALING_GROUP_NAME=$AUTOSCALING_GROUP_NAME" >> /etc/environment +echo "ASSETS_S3_PATH=$ASSETS_S3_PATH" >> /etc/environment + +arch=$(uname -m) + +echo "Architecture detected: $arch" + +if [ "$arch" == "x86_64" ]; then + SSM_AGENT_BINARY_URI=https://s3.amazonaws.com/ec2-downloads-windows/SSMAgent/latest/linux_amd64/amazon-ssm-agent.rpm + AWS_CLI_BINARY_URI=https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip + S5CMD_URI=https://github.com/peak/s5cmd/releases/download/v2.1.0/s5cmd_2.1.0_Linux-64bit.tar.gz + YQ_URI=https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 +else + SSM_AGENT_BINARY_URI=https://s3.amazonaws.com/ec2-downloads-windows/SSMAgent/latest/linux_arm64/amazon-ssm-agent.rpm + AWS_CLI_BINARY_URI=https://awscli.amazonaws.com/awscli-exe-linux-aarch64.zip + S5CMD_URI=https://github.com/peak/s5cmd/releases/download/v2.1.0/s5cmd_2.1.0_Linux-arm64.tar.gz + YQ_URI=https://github.com/mikefarah/yq/releases/latest/download/yq_linux_arm64 +fi + +echo "Updating and installing required system packages" +yum update -y +yum -y install amazon-cloudwatch-agent collectd jq yq gcc ncurses-devel aws-cfn-bootstrap zstd +wget $YQ_URI -O /usr/bin/yq && chmod +x /usr/bin/yq + +cd /opt + +echo "Downloading assets zip file" +aws s3 cp $ASSETS_S3_PATH ./assets.zip +unzip -q assets.zip + +echo 'Configuring CloudWatch Agent' +cp /opt/cw-agent.json /opt/aws/amazon-cloudwatch-agent/etc/custom-amazon-cloudwatch-agent.json + +echo "Starting CloudWatch Agent" +/opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl \ +-a fetch-config -c file:/opt/aws/amazon-cloudwatch-agent/etc/custom-amazon-cloudwatch-agent.json -m ec2 -s +systemctl status amazon-cloudwatch-agent + +echo 'Uninstalling AWS CLI v1' +yum remove awscli + +echo 'Installing AWS CLI v2' +curl $AWS_CLI_BINARY_URI -o "awscliv2.zip" +unzip -q awscliv2.zip +./aws/install +rm /usr/bin/aws +ln /usr/local/bin/aws /usr/bin/aws + +aws configure set default.s3.max_concurrent_requests 50 +aws configure set default.s3.multipart_chunksize 256MB + +echo 'Installing SSM Agent' +yum install -y $SSM_AGENT_BINARY_URI + +echo "Installing s5cmd" +cd /opt +wget -q $S5CMD_URI -O s5cmd.tar.gz +tar -xf s5cmd.tar.gz +chmod +x s5cmd +mv s5cmd /usr/bin +s5cmd version + +# Base specific setup starts here + +# Set by Base-specic CDK components and stacks +REGION=${_REGION_} +STACK_NAME=${_STACK_NAME_} +AUTOSTART_CONTAINER=${_AUTOSTART_CONTAINER_} +FORMAT_DISK=${_FORMAT_DISK_} +NETWORK_ID=${_NETWORK_ID_} +L1_ENDPOINT=${_L1_ENDPOINT_} + +echo "REGION=$REGION" >> /etc/environment +echo "NETWORK_ID=$NETWORK_ID" >> /etc/environment +echo "L1_ENDPOINT=$L1_ENDPOINT" >> /etc/environment + +GIT_URL=https://github.com/base-org/node.git +SYNC_CHECKER_FILE_NAME=syncchecker-base.sh +SNAPSHOT_S3_PATH=s3://base-snapshots-$NETWORK_ID-archive.s3.amazonaws.com/$(curl https://base-snapshots-$NETWORK_ID-archive.s3.amazonaws.com/latest) + +yum -y install docker python3-pip cronie cronie-anacron gcc python3-devel git +yum -y remove python-requests +pip3 install docker-compose +pip3 install hapless +pip3 uninstall -y urllib3 +pip3 install 'urllib3<2.0' + +echo "Assigning Swap Space" +# Check if a swap file already exists +if [ -f /swapfile ]; then + # Remove the existing swap file + swapoff /swapfile + rm -rf /swapfile +fi + +# Create a new swap file +total_mem=$(grep MemTotal /proc/meminfo | awk '{print $2}') +# Calculate the swap size +swap_size=$((total_mem / 3)) +# Convert the swap size to MB +swap_size_mb=$((swap_size / 1024)) +unit=M +fallocate -l $swap_size_mb$unit /swapfile +chmod 600 /swapfile +mkswap /swapfile +swapon /swapfile + +# Enable the swap space to persist after reboot. +echo "/swapfile none swap sw 0 0" | sudo tee -a /etc/fstab + +sysctl vm.swappiness=6 +sysctl vm.vfs_cache_pressure=10 +echo "vm.swappiness=10" | sudo tee -a /etc/sysctl.conf +echo "vm.vfs_cache_pressure=10" | sudo tee -a /etc/sysctl.conf + +free -h + +mkdir -p /data + +# Creating run user and making sure it has all necessary permissions +groupadd -g 1002 bcuser +useradd -u 1002 -g 1002 -m -s /bin/bash bcuser +usermod -a -G docker bcuser +usermod -a -G docker ec2-user +chown -R bcuser:bcuser /secrets +chmod -R 755 /home/bcuser +chmod -R 755 /secrets + +echo "Starting docker" +service docker start +systemctl enable docker + +echo "Clonning node repo" +cd /home/bcuser +git clone $GIT_URL +cd ./node + +echo "Configuring node" + +case $NETWORK_ID in + "mainnet") + sed -i "s#OP_NODE_L1_ETH_RPC=https://1rpc.io/eth#OP_NODE_L1_ETH_RPC=$L1_ENDPOINT#g" /home/bcuser/node/.env.mainnet + sed -i '/.env.mainnet/s/^#//g' /home/bcuser/node/docker-compose.yml + ;; + "goerli") + sed -i "s#OP_NODE_L1_ETH_RPC=https://Base-goerli-rpc.allthatnode.com#OP_NODE_L1_ETH_RPC=$L1_ENDPOINT#g" /home/bcuser/node/.env.goerli + sed -i "s/.env.goerli/s/^#//g" /home/bcuser/node/docker-compose.yml + ;; + "sepolia") + sed -i "s#OP_NODE_L1_ETH_RPC=https://rpc.sepolia.org#OP_NODE_L1_ETH_RPC=$L1_ENDPOINT#g" /home/bcuser/node/.env.sepolia + sed -i "s/.env.sepolia/s/^#//g" /home/bcuser/node/docker-compose.yml + ;; + *) + echo "Network id is not valid." + exit 1 + ;; +esac + +yq -i '.services.geth.volumes[0] = "/data:/data"' /home/bcuser/node/docker-compose.yml + +chown -R bcuser:bcuser /home/bcuser/node + +echo "Configuring syncchecker script" +cp /opt/sync-checker/$SYNC_CHECKER_FILE_NAME /opt/syncchecker.sh +chmod 766 /opt/syncchecker.sh + +echo "*/5 * * * * /opt/syncchecker.sh" | crontab +crontab -l + +echo "Signaling completion to CloudFormation to continue with volume mount" +/opt/aws/bin/cfn-signal --stack $STACK_NAME --resource $RESOURCE_ID --region $REGION + +echo "Preparing data volume" + +echo "Wait for one minute for the volume to be available" +sleep 60 + +if $(lsblk | grep -q nvme1n1); then + echo "nvme1n1 is found. Configuring attached storage" + + if [ "$FORMAT_DISK" == "false" ]; then + echo "Not creating a new filesystem in the disk. Existing data might be present!!" + else + mkfs -t ext4 /dev/nvme1n1 + fi + + sleep 10 + # Define the line to add to fstab + uuid=$(lsblk -n -o UUID /dev/nvme1n1) + line="UUID=$uuid /data ext4 defaults 0 2" + + # Write the line to fstab + echo $line | sudo tee -a /etc/fstab + + mount -a + +else + echo "nvme1n1 is not found. Not doing anything" +fi + +lsblk -d + +chown -R bcuser:bcuser /data +chmod -R 755 /data + +if [ "$AUTOSTART_CONTAINER" == "false" ]; then + echo "Sync node. Autostart disabled. Start docker-compose manually!" +else + echo "Sync node. Autostart enabled. Starting docker-compose in 3 min." + cd /home/bcuser/node + echo "sudo su bcuser && /usr/local/bin/docker-compose -f /home/bcuser/node/docker-compose.yml up -d" | at now +3 minutes +fi + +# TODO: Add copy data from S3 script +# echo "Restoring data from snapshot" +# chmod 766 /opt/restore-from-snapshot.sh +# echo "/opt/restore-from-snapshot.sh" | at now +3 minutes + +echo "All Done!!" diff --git a/lib/base/lib/common-stack.ts b/lib/base/lib/common-stack.ts new file mode 100644 index 00000000..cb734545 --- /dev/null +++ b/lib/base/lib/common-stack.ts @@ -0,0 +1,71 @@ +import * as cdk from "aws-cdk-lib"; +import * as cdkConstructs from "constructs"; +import * as iam from 'aws-cdk-lib/aws-iam'; +import * as nag from "cdk-nag"; + +export interface BaseCommonStackProps extends cdk.StackProps { + +} + +export class BaseCommonStack extends cdk.Stack { + AWS_STACKNAME = cdk.Stack.of(this).stackName; + AWS_ACCOUNT_ID = cdk.Stack.of(this).account; + + constructor(scope: cdkConstructs.Construct, id: string, props: BaseCommonStackProps) { + super(scope, id, props); + + const region = cdk.Stack.of(this).region; + + const instanceRole = new iam.Role(this, `node-role`, { + assumedBy: new iam.ServicePrincipal("ec2.amazonaws.com"), + managedPolicies: [ + iam.ManagedPolicy.fromAwsManagedPolicyName("AmazonSSMManagedInstanceCore"), + iam.ManagedPolicy.fromAwsManagedPolicyName("CloudWatchAgentServerPolicy"), + ], + }); + + instanceRole.addToPolicy(new iam.PolicyStatement({ + // Can't target specific stack: https://github.com/aws/aws-cdk/issues/22657 + resources: ["*"], + actions: ["cloudformation:SignalResource"], + })); + + instanceRole.addToPolicy(new iam.PolicyStatement({ + resources: [`arn:aws:autoscaling:${region}:${this.AWS_ACCOUNT_ID}:autoScalingGroup:*:autoScalingGroupName/base-*`], + actions: ["autoscaling:CompleteLifecycleAction"], + })); + + instanceRole.addToPolicy( + new iam.PolicyStatement({ + resources: [ + "arn:aws:s3:::base-snapshots-*-archive", + "arn:aws:s3:::base-snapshots-*-archive/*" + ], + actions: ["s3:*Object"], + })); + + new cdk.CfnOutput(this, "Instance Role ARN", { + value: instanceRole.roleArn, + exportName: "BaseNodeInstanceRoleArn", + }); + + /** + * cdk-nag suppressions + */ + + nag.NagSuppressions.addResourceSuppressions( + this, + [ + { + id: "AwsSolutions-IAM4", + reason: "AmazonSSMManagedInstanceCore and CloudWatchAgentServerPolicy are restrictive enough", + }, + { + id: "AwsSolutions-IAM5", + reason: "Can't target specific stack: https://github.com/aws/aws-cdk/issues/22657", + }, + ], + true + ); + } +} diff --git a/lib/base/lib/config/baseConfig.interface.ts b/lib/base/lib/config/baseConfig.interface.ts new file mode 100644 index 00000000..b6a695ba --- /dev/null +++ b/lib/base/lib/config/baseConfig.interface.ts @@ -0,0 +1,22 @@ +import * as configTypes from "../../../constructs/config.interface"; + +export type BaseNetworkId = "mainnet" ; +export type BaseNodeConfiguration = "full" ; + +export {AMBEthereumNodeNetworkId} from "../../../constructs/config.interface"; + +export interface BaseDataVolumeConfig extends configTypes.DataVolumeConfig { +} + +export interface BaseAccountsVolumeConfig extends configTypes.DataVolumeConfig { +} + +export interface BaseBaseConfig extends configTypes.BaseConfig { +} + +export interface BaseBaseNodeConfig extends configTypes.BaseNodeConfig { + ambEntereumNodeNetworkId: configTypes.AMBEthereumNodeNetworkId; + ambEntereumNodeInstanceType: string; + baseNetworkId: BaseNetworkId; + dataVolume: BaseDataVolumeConfig; +} diff --git a/lib/base/lib/config/baseConfig.ts b/lib/base/lib/config/baseConfig.ts new file mode 100644 index 00000000..a8a314ea --- /dev/null +++ b/lib/base/lib/config/baseConfig.ts @@ -0,0 +1,38 @@ +import * as ec2 from "aws-cdk-lib/aws-ec2"; +import * as configTypes from "./baseConfig.interface"; +import * as constants from "../../../constructs/constants"; + + +const parseDataVolumeType = (dataVolumeType: string) => { + switch (dataVolumeType) { + case "gp3": + return ec2.EbsDeviceVolumeType.GP3; + case "io2": + return ec2.EbsDeviceVolumeType.IO2; + case "io1": + return ec2.EbsDeviceVolumeType.IO1; + case "instance-store": + return constants.InstanceStoreageDeviceVolumeType; + default: + return ec2.EbsDeviceVolumeType.GP3; + } +} + +export const baseConfig: configTypes.BaseBaseConfig = { + accountId: process.env.AWS_ACCOUNT_ID || "xxxxxxxxxxx", + region: process.env.AWS_REGION || "us-east-2", +} + +export const baseNodeConfig: configTypes.BaseBaseNodeConfig = { + ambEntereumNodeNetworkId: process.env.AMB_ENTEREUM_NODE_NETWORK_ID || "mainnet", + ambEntereumNodeInstanceType: process.env.AMB_ETHEREUM_NODE_INSTANCE_TYPE || "bc.m5.xlarge", + instanceType: new ec2.InstanceType(process.env.BASE_INSTANCE_TYPE ? process.env.BASE_INSTANCE_TYPE : "m6a.2xlarge"), + instanceCpuType: process.env.BASE_CPU_TYPE?.toLowerCase() == "x86_64" ? ec2.AmazonLinuxCpuType.X86_64 : ec2.AmazonLinuxCpuType.ARM_64, + baseNetworkId: process.env.BASE_NETWORK_ID || "mainnet", + dataVolume: { + sizeGiB: process.env.BASE_DATA_VOL_SIZE ? parseInt(process.env.BASE_DATA_VOL_SIZE): 1000, + type: parseDataVolumeType(process.env.BASE_DATA_VOL_TYPE?.toLowerCase() ? process.env.BASE_DATA_VOL_TYPE?.toLowerCase() : "gp3"), + iops: process.env.BASE_DATA_VOL_IOPS ? parseInt(process.env.BASE_DATA_VOL_IOPS): 5000, + throughput: process.env.BASE_DATA_VOL_THROUGHPUT ? parseInt(process.env.BASE_DATA_VOL_THROUGHPUT): 700, + }, +}; diff --git a/lib/base/lib/constructs/base-node-security-group.ts b/lib/base/lib/constructs/base-node-security-group.ts new file mode 100644 index 00000000..41e43bb1 --- /dev/null +++ b/lib/base/lib/constructs/base-node-security-group.ts @@ -0,0 +1,34 @@ +import * as cdk from "aws-cdk-lib"; +import * as cdkConstructs from 'constructs'; +import * as ec2 from "aws-cdk-lib/aws-ec2"; + +export interface BaseNodeSecurityGroupConstructProps { + vpc: cdk.aws_ec2.IVpc; + } + + export class BaseNodeSecurityGroupConstruct extends cdkConstructs.Construct { + public securityGroup: cdk.aws_ec2.ISecurityGroup; + + constructor(scope: cdkConstructs.Construct, id: string, props: BaseNodeSecurityGroupConstructProps) { + super(scope, id); + + const { + vpc, + } = props; + + const sg = new ec2.SecurityGroup(this, `rpc-node-security-group`, { + vpc, + description: "Security Group for Blockchain nodes", + allowAllOutbound: true, + }); + + // Public ports + sg.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(9222), "P2P"); + sg.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.udp(9222), "P2P"); + + // Private port + sg.addIngressRule(ec2.Peer.ipv4(vpc.vpcCidrBlock), ec2.Port.tcp(8545), "Base Client RPC"); + + this.securityGroup = sg + } + } diff --git a/lib/base/lib/single-node-stack.ts b/lib/base/lib/single-node-stack.ts new file mode 100644 index 00000000..e7e64dbc --- /dev/null +++ b/lib/base/lib/single-node-stack.ts @@ -0,0 +1,136 @@ +import * as cdk from "aws-cdk-lib"; +import * as cdkConstructs from "constructs"; +import * as ec2 from "aws-cdk-lib/aws-ec2"; +import * as iam from "aws-cdk-lib/aws-iam"; +import * as s3Assets from "aws-cdk-lib/aws-s3-assets"; +import * as path from "path"; +import * as fs from "fs"; +import * as nodeCwDashboard from "./assets/node-cw-dashboard" +import * as cw from 'aws-cdk-lib/aws-cloudwatch'; +import * as nag from "cdk-nag"; +import { SingleNodeConstruct } from "../../constructs/single-node" +import * as configTypes from "./config/baseConfig.interface"; +import * as constants from "../../constructs/constants"; +import { BaseNodeSecurityGroupConstruct } from "./constructs/base-node-security-group"; + +export interface BaseSingleNodeStackProps extends cdk.StackProps { + instanceType: ec2.InstanceType; + instanceCpuType: ec2.AmazonLinuxCpuType; + baseNetworkId: configTypes.BaseNetworkId; + dataVolume: configTypes.BaseDataVolumeConfig; +} + +export class BaseSingleNodeStack extends cdk.Stack { + constructor(scope: cdkConstructs.Construct, id: string, props: BaseSingleNodeStackProps) { + super(scope, id, props); + + // Setting up necessary environment variables + const REGION = cdk.Stack.of(this).region; + const STACK_NAME = cdk.Stack.of(this).stackName; + const STACK_ID = cdk.Stack.of(this).stackId; + const availabilityZones = cdk.Stack.of(this).availabilityZones; + const chosenAvailabilityZone = availabilityZones.slice(0, 1)[0]; + + // Getting our config from initialization properties + const { + instanceType, + instanceCpuType, + baseNetworkId, + dataVolume, + } = props; + + // Using default VPC + const vpc = ec2.Vpc.fromLookup(this, "vpc", { isDefault: true }); + + // Setting up the security group for the node from Base-specific construct + const instanceSG = new BaseNodeSecurityGroupConstruct (this, "security-group", { + vpc: vpc, + }) + + // Making our scripts and configis from the local "assets" directory available for instance to download + const asset = new s3Assets.Asset(this, "assets", { + path: path.join(__dirname, "assets"), + }); + + // Getting the IAM role ARN from the common stack + const importedInstanceRoleArn = cdk.Fn.importValue("BaseNodeInstanceRoleArn"); + const ambEthereumNodeRpcUrlWithBillingToken = cdk.Fn.importValue("BaseAmbEthereumNodeRpcUrlWithBillingToken"); + + const instanceRole = iam.Role.fromRoleArn(this, "iam-role", importedInstanceRoleArn); + + // Making sure our instance will be able to read the assets + asset.bucket.grantRead(instanceRole); + + // Setting up the node using generic Single Node constract + if (instanceCpuType === ec2.AmazonLinuxCpuType.ARM_64) { + throw new Error("ARM_64 is not yet supported"); + } + + const node = new SingleNodeConstruct(this, "rpc-node", { + instanceName: STACK_NAME, + instanceType, + dataVolumes: [dataVolume], + rootDataVolumeDeviceName: "/dev/xvda", + machineImage: new ec2.AmazonLinuxImage({ + generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2, + cpuType: instanceCpuType, + }), + vpc, + availabilityZone: chosenAvailabilityZone, + role: instanceRole, + securityGroup: instanceSG.securityGroup, + vpcSubnets: { + subnetType: ec2.SubnetType.PUBLIC, + }, + }); + + // Parsing user data script and injecting necessary variables + const nodeStartScript = fs.readFileSync(path.join(__dirname, "assets", "user-data", "node.sh")).toString(); + const dataVolumeSizeBytes = dataVolume.sizeGiB * constants.GibibytesToBytesConversionCoefficient; + + const modifiedInitNodeScript = cdk.Fn.sub(nodeStartScript, { + _REGION_: REGION, + _ASSETS_S3_PATH_: `s3://${asset.s3BucketName}/${asset.s3ObjectKey}`, + _STACK_NAME_: STACK_NAME, + _NODE_CF_LOGICAL_ID_: node.nodeCFLogicalId, + _DATA_VOLUME_TYPE_: dataVolume.type, + _DATA_VOLUME_SIZE_: dataVolumeSizeBytes.toString(), + _NETWORK_ID_: baseNetworkId, + _LIFECYCLE_HOOK_NAME_: constants.NoneValue, + _AUTOSCALING_GROUP_NAME_: constants.NoneValue, + _AUTOSTART_CONTAINER_: "true", + _FORMAT_DISK_: "true", + _L1_ENDPOINT_: ambEthereumNodeRpcUrlWithBillingToken, + }); + + node.instance.addUserData(modifiedInitNodeScript); + + // Adding CloudWatch dashboard to the node + const dashboardString = cdk.Fn.sub(JSON.stringify(nodeCwDashboard.SyncNodeCWDashboardJSON), { + INSTANCE_ID:node.instanceId, + INSTANCE_NAME: STACK_NAME, + REGION: REGION, + }) + + new cw.CfnDashboard(this, 'base-cw-dashboard', { + dashboardName: STACK_NAME, + dashboardBody: dashboardString, + }); + + new cdk.CfnOutput(this, "node-instance-id", { + value: node.instanceId, + }); + + // Adding suppressions to the stack + nag.NagSuppressions.addResourceSuppressions( + this, + [ + { + id: "AwsSolutions-IAM5", + reason: "Need read access to the S3 bucket with assets", + }, + ], + true + ); + } +} diff --git a/lib/base/package-lock.json b/lib/base/package-lock.json new file mode 100644 index 00000000..5d2c6444 --- /dev/null +++ b/lib/base/package-lock.json @@ -0,0 +1,641 @@ +{ + "name": "aws-blockchain-node-runners-base", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "aws-blockchain-node-runners-base", + "version": "0.1.0", + "dependencies": { + "@types/node": "^20.10.0" + }, + "devDependencies": { + "@types/jest": "^29.5.11" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", + "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.23.4", + "chalk": "^2.4.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/code-frame/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/code-frame/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/@babel/code-frame/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/code-frame/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", + "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/@babel/highlight/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "29.5.11", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.11.tgz", + "integrity": "sha512-S2mHmYIVe13vrm6q4kN6fLYYAka15ALQki/vgDC3mIukEOx8WJlv0kQPM+d4w8Gp6u0uSdKND04IlTXBv0rwnQ==", + "dev": true, + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "node_modules/@types/node": { + "version": "20.10.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.5.tgz", + "integrity": "sha512-nNPsNE65wjMxEKI93yOP+NPGGBJz/PoN3kZsVLee0XMiJolxSekEVD8wRwBUBqkwc7UWop0edW50yrCQW4CyRw==", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true + }, + "node_modules/@types/yargs": { + "version": "17.0.32", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", + "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + } + } +} diff --git a/lib/base/package.json b/lib/base/package.json new file mode 100644 index 00000000..3fb94aea --- /dev/null +++ b/lib/base/package.json @@ -0,0 +1,20 @@ +{ + "name": "aws-blockchain-node-runners-base", + "version": "0.1.0", + "scripts": { + "build": "tsc", + "watch": "tsc -w", + "test": "jest", + "cdk": "cdk", + "cdk_deploy_common": "cdk deploy base-common", + "cdk_synth_single_node": "cdk synth base-single-node", + "cdk_deploy_single_node": "cdk deploy base-single-node", + "cdk_destroy_single_node": "cdk destroy base-single-node" + }, + "dependencies": { + "@types/node": "^20.10.0" + }, + "devDependencies": { + "@types/jest": "^29.5.11" + } +} diff --git a/lib/base/sample-configs/.env-sample-rpc b/lib/base/sample-configs/.env-sample-rpc new file mode 100644 index 00000000..579e2e69 --- /dev/null +++ b/lib/base/sample-configs/.env-sample-rpc @@ -0,0 +1,20 @@ +############################################################# +# Example configuration for Base nodes runner app on AWS # +############################################################# + +## Set the AWS account is and region for your environment ## +AWS_ACCOUNT_ID="xxxxxxxx" +AWS_REGION="us-east-1" # Regions supported by Amazon Managed Blockchain Access Ethereum: https://docs.aws.amazon.com/general/latest/gr/managedblockchain.html#managedblockchain-access + +## Common configuration parameters ## +AMB_ETHEREUM_NODE_NETWORK_ID="mainnet" # All options: "mainnet", "goerli" +AMB_ETHEREUM_NODE_INSTANCE_TYPE="bc.m5.xlarge" # For available options see: https://aws.amazon.com/managed-blockchain/instance-types/ +BASE_NETWORK_ID="mainnet" # All options: "mainnet", "goerli" + +BASE_INSTANCE_TYPE="m6a.2xlarge" +BASE_CPU_TYPE="x86_64" # All options: "x86_64", "ARM_64". IMPORTANT: Make sure the CPU type matches the instance type used +# Data volume configuration +BASE_DATA_VOL_TYPE="gp3" # Other options: "io1" | "io2" | "gp3" | "instance-store" . IMPORTANT: Use "instance-store" option only with instance types that support that feature, like popular for node im4gn, d3, i3en, and i4i instance families +BASE_DATA_VOL_SIZE="1000" # Current required data size to keep both snapshot archive and unarchived version of it +BASE_DATA_VOL_IOPS="3000" # Max IOPS for EBS volumes (not applicable for "instance-store") +BASE_DATA_VOL_THROUGHPUT="700" # Max throughput for EBS gp3 volumes (not applicable for "io1" | "io2" | "instance-store") diff --git a/lib/base/test/.env-test b/lib/base/test/.env-test new file mode 100644 index 00000000..fb62be98 --- /dev/null +++ b/lib/base/test/.env-test @@ -0,0 +1,22 @@ +############################################################# +# Example configuration for Base nodes runner app on AWS # +############################################################# + +## Set the AWS account is and region for your environment ## +AWS_ACCOUNT_ID="347616198663" +AWS_REGION="us-east-1" # Regions supported by Amazon Managed Blockchain Access Ethereum: https://docs.aws.amazon.com/general/latest/gr/managedblockchain.html#managedblockchain-access + +## Common configuration parameters ## +AMB_ETHEREUM_NODE_NETWORK_ID="mainnet" # All options: "mainnet", "goerli" +AMB_ETHEREUM_NODE_INSTANCE_TYPE="bc.m5.xlarge" # For available options see: https://aws.amazon.com/managed-blockchain/instance-types/ +BASE_NETWORK_ID="mainnet" # All options: "mainnet" +BASE_NODE_CONFIGURATION="full" # All options: "full" +BASE_VERSION="TODO: TBA" # Current required version of Docker Compose file for Base node + +BASE_INSTANCE_TYPE="m6a.2xlarge" +BASE_CPU_TYPE="x86_64" # All options: "x86_64", "ARM_64". IMPORTANT: Make sure the CPU type matches the instance type used +# Data volume configuration +BASE_DATA_VOL_TYPE="gp3" # Other options: "io1" | "io2" | "gp3" | "instance-store" . IMPORTANT: Use "instance-store" option only with instance types that support that feature, like popular for node im4gn, d3, i3en, and i4i instance families +BASE_DATA_VOL_SIZE="1000" # Current required data size to keep both snapshot archive and unarchived version of it +BASE_DATA_VOL_IOPS="3000" # Max IOPS for EBS volumes (not applicable for "instance-store") +BASE_DATA_VOL_THROUGHPUT="700" # Max throughput for EBS gp3 volumes (not applicable for "io1" | "io2" | "instance-store") diff --git a/lib/base/test/base-ethereum-l1-node.test.ts b/lib/base/test/base-ethereum-l1-node.test.ts new file mode 100644 index 00000000..25ba1bf6 --- /dev/null +++ b/lib/base/test/base-ethereum-l1-node.test.ts @@ -0,0 +1,57 @@ +import {Match, Template} from "aws-cdk-lib/assertions"; +import * as cdk from "aws-cdk-lib"; +import * as dotenv from 'dotenv'; + +dotenv.config({path: './test/.env-test'}); +import * as config from "../lib/config/baseConfig"; +import {BaseAMBEthereumSingleNodeStack} from "../lib/amb-ethereum-single-node-stack"; + +describe("BaseAMBEthereumSingleNodeStack", () => { + let app: cdk.App; + let baseAMBEthereumSingleNode: BaseAMBEthereumSingleNodeStack; + let template: Template; + beforeAll(() => { + app = new cdk.App(); + + // Create the BaseAMBEthereumSingleNodeStack. + + baseAMBEthereumSingleNode = new BaseAMBEthereumSingleNodeStack(app, "base-ethereum-l1-node", { + stackName: `base-amb-ethereum-single-node-${config.baseNodeConfig.nodeConfiguration}`, + env: { account: config.baseConfig.accountId, region: config.baseConfig.region }, + + ambEthereumNodeNetworkId: config.baseNodeConfig.ambEntereumNodeNetworkId, + ambEthereumNodeInstanceType: config.baseNodeConfig.ambEntereumNodeInstanceType, + }); + + template = Template.fromStack(baseAMBEthereumSingleNode); + }); + + test("Check Node URL is correct", () => { + template.hasOutput("ambethnoderpcurlbillingtoken", { + Value: { + "Fn::Join": [ + "", + [ + "https://", + { + "Fn::GetAtt": [ + Match.anyValue(), + "NodeId" + ] + }, + ".t.ethereum.managedblockchain.us-east-1.amazonaws.com?billingtoken=", + { + "Fn::GetAtt": [ + Match.anyValue(), + "BillingToken" + ] + } + ] + ] + }, + "Export": { + "Name": "AmbEthereumNodeRpcUrlWithBillingToken" + } + }) + }); +}); diff --git a/lib/base/test/base-single-node.test.ts b/lib/base/test/base-single-node.test.ts new file mode 100644 index 00000000..a8e1562c --- /dev/null +++ b/lib/base/test/base-single-node.test.ts @@ -0,0 +1,123 @@ +import {Match, Template} from "aws-cdk-lib/assertions"; +import * as cdk from "aws-cdk-lib"; +import * as dotenv from 'dotenv'; + +dotenv.config({path: './test/.env-test'}); +import * as config from "../lib/config/baseConfig"; +import {BaseSingleNodeStack} from "../lib/single-node-stack"; + +describe("BaseSingleNodeStack", () => { + let app: cdk.App; + let baseSingleNodeStack: BaseSingleNodeStack; + let template: Template; + beforeAll(() => { + app = new cdk.App(); + + // Create the BaseSingleNodeStack. + baseSingleNodeStack = new BaseSingleNodeStack(app, "base-single-node", { + stackName: `base-single-node-${config.baseConfig.accountId}`, + env: {account: config.baseConfig.accountId, region: config.baseConfig.region}, + + instanceType: config.baseNodeConfig.instanceType, + instanceCpuType: config.baseNodeConfig.instanceCpuType, + baseVersion: config.baseNodeConfig.baseVersion, + nodeConfiguration: config.baseNodeConfig.nodeConfiguration, + dataVolume: config.baseNodeConfig.dataVolume, + baseNetworkId: config.baseNodeConfig.baseNetworkId + }); + + template = Template.fromStack(baseSingleNodeStack); + }); + + test("Check Security Group", () => { + // Has EC2 instance security group. + template.hasResourceProperties("AWS::EC2::SecurityGroup", { + GroupDescription: Match.anyValue(), + VpcId: Match.anyValue(), + SecurityGroupEgress: [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + SecurityGroupIngress: [ + { + "CidrIp": "0.0.0.0/0", + "Description": "P2P", + "FromPort": 9222, + "IpProtocol": "tcp", + "ToPort": 9222 + }, + { + "CidrIp": "0.0.0.0/0", + "Description": "P2P", + "FromPort": 9222, + "IpProtocol": "udp", + "ToPort": 9222 + }, + { + "CidrIp": "1.2.3.4/5", + "Description": "Base Client RPC", + "FromPort": 8545, + "IpProtocol": "tcp", + "ToPort": 8545 + } + ] + }) + }); + + + test("Check EC2 Settings", () => { + // Has EC2 instance with node configuration + template.hasResourceProperties("AWS::EC2::Instance", { + AvailabilityZone: Match.anyValue(), + UserData: Match.anyValue(), + BlockDeviceMappings: [ + { + DeviceName: "/dev/sda1", + Ebs: { + DeleteOnTermination: true, + Encrypted: true, + Iops: 3000, + VolumeSize: 46, + VolumeType: "gp3" + } + } + ], + IamInstanceProfile: Match.anyValue(), + ImageId: Match.anyValue(), + InstanceType: "m6a.2xlarge", + Monitoring: true, + PropagateTagsToVolumeOnCreation: true, + SecurityGroupIds: Match.anyValue(), + SubnetId: Match.anyValue(), + }); + + // // Has EBS data volume. + template.hasResourceProperties("AWS::EC2::Volume", { + AvailabilityZone: Match.anyValue(), + Encrypted: true, + Iops: 3000, + MultiAttachEnabled: false, + Size: 1000, + Throughput: 700, + VolumeType: "gp3" + }) + + // Has EBS data volume attachment. + template.hasResourceProperties("AWS::EC2::VolumeAttachment", { + Device: "/dev/sdf", + InstanceId: Match.anyValue(), + VolumeId: Match.anyValue(), + }) + }); + + test("Check CloudWatch Dashboard", () => { + // Has CloudWatch dashboard. + template.hasResourceProperties("AWS::CloudWatch::Dashboard", { + DashboardBody: Match.anyValue(), + DashboardName: `base-single-node-${config.baseConfig.accountId}` + }) + }); +}); diff --git a/lib/base/tsconfig.json b/lib/base/tsconfig.json new file mode 100644 index 00000000..aaa7dc51 --- /dev/null +++ b/lib/base/tsconfig.json @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": [ + "es2020", + "dom" + ], + "declaration": true, + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "noImplicitThis": true, + "alwaysStrict": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": false, + "inlineSourceMap": true, + "inlineSources": true, + "experimentalDecorators": true, + "strictPropertyInitialization": false, + "typeRoots": [ + "./node_modules/@types" + ] + }, + "exclude": [ + "node_modules", + "cdk.out" + ] +} From 402348ad0fd3a50082b2c691683487ccd014c1ce Mon Sep 17 00:00:00 2001 From: Nikolay Vlasov Date: Thu, 18 Jan 2024 16:53:28 +1100 Subject: [PATCH 02/75] [WIP]Improvements to Base blueprint passing smoke test --- lib/base/README.md | 3 +++ lib/base/app.ts | 1 + lib/base/lib/assets/restore-from-snapshot.sh | 6 +++--- lib/base/lib/assets/user-data/node.sh | 19 ++++++++----------- lib/base/lib/config/baseConfig.interface.ts | 1 + lib/base/lib/config/baseConfig.ts | 1 + lib/base/lib/single-node-stack.ts | 4 +++- lib/base/sample-configs/.env-sample-rpc | 5 +++-- lib/base/test/.env-test | 2 -- lib/base/test/base-ethereum-l1-node.test.ts | 4 ++-- lib/base/test/base-single-node.test.ts | 10 ++++------ 11 files changed, 29 insertions(+), 27 deletions(-) diff --git a/lib/base/README.md b/lib/base/README.md index dac10bb6..a78b2c05 100644 --- a/lib/base/README.md +++ b/lib/base/README.md @@ -208,6 +208,9 @@ A script on the Base node publishes current block and blocks behind metrics to C export AWS_REGION=us-east-1 aws ssm start-session --target $INSTANCE_ID --region $AWS_REGION sudo su bcuser + # Geth logs: + docker logs --tail 50 node_geth_1 -f + # Base logs: docker logs --tail 50 node_node_1 -f ``` 2. How to check the logs from the EC2 user-data script? diff --git a/lib/base/app.ts b/lib/base/app.ts index 8eea3432..a5c048e3 100644 --- a/lib/base/app.ts +++ b/lib/base/app.ts @@ -30,5 +30,6 @@ new BaseSingleNodeStack(app, "base-single-node", { instanceType: config.baseNodeConfig.instanceType, instanceCpuType: config.baseNodeConfig.instanceCpuType, baseNetworkId: config.baseNodeConfig.baseNetworkId, + restoreFromSnapshot: config.baseNodeConfig.restoreFromSnapshot, dataVolume: config.baseNodeConfig.dataVolume, }); diff --git a/lib/base/lib/assets/restore-from-snapshot.sh b/lib/base/lib/assets/restore-from-snapshot.sh index 0460fda8..0d9b14be 100644 --- a/lib/base/lib/assets/restore-from-snapshot.sh +++ b/lib/base/lib/assets/restore-from-snapshot.sh @@ -9,13 +9,13 @@ echo "Sync started at " $(date) SECONDS=0 s5cmd --log error cp s3://base-snapshots-$NETWORK_ID-archive/$LATEST_SNAPSHOT_FILE_NAME /data && \ -tar -I zstd -xf /data/$LATEST_SNAPSHOT_FILE_NAME -C /data && \ +tar -I zstdmt -xf /data/$LATEST_SNAPSHOT_FILE_NAME -C /data && \ mv /data/snapshots/$NETWORK_ID/download/* /data && \ rm -rf /data/snapshots && \ rm -rf /data/$LATEST_SNAPSHOT_FILE_NAME -chown -R bdcuser:bdcuser /data && \ +chown -R bcuser:bcuser /data && \ echo "Sync finished at " $(date) && \ echo "$(($SECONDS / 60)) minutes and $(($SECONDS % 60)) seconds elapsed." && \ -sudo su bdcuser && \ +sudo su bcuser && \ /usr/local/bin/docker-compose -f /home/bcuser/node/docker-compose.yml up -d \ No newline at end of file diff --git a/lib/base/lib/assets/user-data/node.sh b/lib/base/lib/assets/user-data/node.sh index 94d1ad1c..daf9a26e 100644 --- a/lib/base/lib/assets/user-data/node.sh +++ b/lib/base/lib/assets/user-data/node.sh @@ -74,7 +74,7 @@ s5cmd version # Set by Base-specic CDK components and stacks REGION=${_REGION_} STACK_NAME=${_STACK_NAME_} -AUTOSTART_CONTAINER=${_AUTOSTART_CONTAINER_} +RESTORE_FROM_SNAPSHOT=${_RESTORE_FROM_SNAPSHOT_} FORMAT_DISK=${_FORMAT_DISK_} NETWORK_ID=${_NETWORK_ID_} L1_ENDPOINT=${_L1_ENDPOINT_} @@ -165,7 +165,7 @@ case $NETWORK_ID in ;; esac -yq -i '.services.geth.volumes[0] = "/data:/data"' /home/bcuser/node/docker-compose.yml +sed -i "s#GETH_HOST_DATA_DIR=./geth-data#GETH_HOST_DATA_DIR=/data/geth#g" /home/bcuser/node/.env chown -R bcuser:bcuser /home/bcuser/node @@ -212,17 +212,14 @@ lsblk -d chown -R bcuser:bcuser /data chmod -R 755 /data -if [ "$AUTOSTART_CONTAINER" == "false" ]; then - echo "Sync node. Autostart disabled. Start docker-compose manually!" -else - echo "Sync node. Autostart enabled. Starting docker-compose in 3 min." +if [ "$RESTORE_FROM_SNAPSHOT" == "false" ]; then + echo "Skipping restoration from snapshot. Starting docker-compose in 3 min." cd /home/bcuser/node echo "sudo su bcuser && /usr/local/bin/docker-compose -f /home/bcuser/node/docker-compose.yml up -d" | at now +3 minutes +else + echo "Restoring data from snapshot" + chmod 766 /opt/restore-from-snapshot.sh + echo "/opt/restore-from-snapshot.sh" | at now +3 minutes fi -# TODO: Add copy data from S3 script -# echo "Restoring data from snapshot" -# chmod 766 /opt/restore-from-snapshot.sh -# echo "/opt/restore-from-snapshot.sh" | at now +3 minutes - echo "All Done!!" diff --git a/lib/base/lib/config/baseConfig.interface.ts b/lib/base/lib/config/baseConfig.interface.ts index b6a695ba..6dbf1e33 100644 --- a/lib/base/lib/config/baseConfig.interface.ts +++ b/lib/base/lib/config/baseConfig.interface.ts @@ -19,4 +19,5 @@ export interface BaseBaseNodeConfig extends configTypes.BaseNodeConfig { ambEntereumNodeInstanceType: string; baseNetworkId: BaseNetworkId; dataVolume: BaseDataVolumeConfig; + restoreFromSnapshot: boolean; } diff --git a/lib/base/lib/config/baseConfig.ts b/lib/base/lib/config/baseConfig.ts index a8a314ea..44ed175c 100644 --- a/lib/base/lib/config/baseConfig.ts +++ b/lib/base/lib/config/baseConfig.ts @@ -29,6 +29,7 @@ export const baseNodeConfig: configTypes.BaseBaseNodeConfig = { instanceType: new ec2.InstanceType(process.env.BASE_INSTANCE_TYPE ? process.env.BASE_INSTANCE_TYPE : "m6a.2xlarge"), instanceCpuType: process.env.BASE_CPU_TYPE?.toLowerCase() == "x86_64" ? ec2.AmazonLinuxCpuType.X86_64 : ec2.AmazonLinuxCpuType.ARM_64, baseNetworkId: process.env.BASE_NETWORK_ID || "mainnet", + restoreFromSnapshot: process.env.BASE_RESTORE_FROM_SNAPSHOT?.toLowerCase() == "true" ? true : false, dataVolume: { sizeGiB: process.env.BASE_DATA_VOL_SIZE ? parseInt(process.env.BASE_DATA_VOL_SIZE): 1000, type: parseDataVolumeType(process.env.BASE_DATA_VOL_TYPE?.toLowerCase() ? process.env.BASE_DATA_VOL_TYPE?.toLowerCase() : "gp3"), diff --git a/lib/base/lib/single-node-stack.ts b/lib/base/lib/single-node-stack.ts index e7e64dbc..814edac3 100644 --- a/lib/base/lib/single-node-stack.ts +++ b/lib/base/lib/single-node-stack.ts @@ -17,6 +17,7 @@ export interface BaseSingleNodeStackProps extends cdk.StackProps { instanceType: ec2.InstanceType; instanceCpuType: ec2.AmazonLinuxCpuType; baseNetworkId: configTypes.BaseNetworkId; + restoreFromSnapshot: boolean; dataVolume: configTypes.BaseDataVolumeConfig; } @@ -36,6 +37,7 @@ export class BaseSingleNodeStack extends cdk.Stack { instanceType, instanceCpuType, baseNetworkId, + restoreFromSnapshot, dataVolume, } = props; @@ -98,7 +100,7 @@ export class BaseSingleNodeStack extends cdk.Stack { _NETWORK_ID_: baseNetworkId, _LIFECYCLE_HOOK_NAME_: constants.NoneValue, _AUTOSCALING_GROUP_NAME_: constants.NoneValue, - _AUTOSTART_CONTAINER_: "true", + _RESTORE_FROM_SNAPSHOT_: restoreFromSnapshot.toString(), _FORMAT_DISK_: "true", _L1_ENDPOINT_: ambEthereumNodeRpcUrlWithBillingToken, }); diff --git a/lib/base/sample-configs/.env-sample-rpc b/lib/base/sample-configs/.env-sample-rpc index 579e2e69..daca3b70 100644 --- a/lib/base/sample-configs/.env-sample-rpc +++ b/lib/base/sample-configs/.env-sample-rpc @@ -15,6 +15,7 @@ BASE_INSTANCE_TYPE="m6a.2xlarge" BASE_CPU_TYPE="x86_64" # All options: "x86_64", "ARM_64". IMPORTANT: Make sure the CPU type matches the instance type used # Data volume configuration BASE_DATA_VOL_TYPE="gp3" # Other options: "io1" | "io2" | "gp3" | "instance-store" . IMPORTANT: Use "instance-store" option only with instance types that support that feature, like popular for node im4gn, d3, i3en, and i4i instance families -BASE_DATA_VOL_SIZE="1000" # Current required data size to keep both snapshot archive and unarchived version of it -BASE_DATA_VOL_IOPS="3000" # Max IOPS for EBS volumes (not applicable for "instance-store") +BASE_DATA_VOL_SIZE="3000" # Current required data size to keep both snapshot archive and unarchived version of it +BASE_DATA_VOL_IOPS="5000" # Max IOPS for EBS volumes (not applicable for "instance-store") BASE_DATA_VOL_THROUGHPUT="700" # Max throughput for EBS gp3 volumes (not applicable for "io1" | "io2" | "instance-store") +BASE_RESTORE_FROM_SNAPSHOT="false" # Download snapshot to speed up statup time \ No newline at end of file diff --git a/lib/base/test/.env-test b/lib/base/test/.env-test index fb62be98..70552ed3 100644 --- a/lib/base/test/.env-test +++ b/lib/base/test/.env-test @@ -10,8 +10,6 @@ AWS_REGION="us-east-1" # Regions supported by Amazon Ma AMB_ETHEREUM_NODE_NETWORK_ID="mainnet" # All options: "mainnet", "goerli" AMB_ETHEREUM_NODE_INSTANCE_TYPE="bc.m5.xlarge" # For available options see: https://aws.amazon.com/managed-blockchain/instance-types/ BASE_NETWORK_ID="mainnet" # All options: "mainnet" -BASE_NODE_CONFIGURATION="full" # All options: "full" -BASE_VERSION="TODO: TBA" # Current required version of Docker Compose file for Base node BASE_INSTANCE_TYPE="m6a.2xlarge" BASE_CPU_TYPE="x86_64" # All options: "x86_64", "ARM_64". IMPORTANT: Make sure the CPU type matches the instance type used diff --git a/lib/base/test/base-ethereum-l1-node.test.ts b/lib/base/test/base-ethereum-l1-node.test.ts index 25ba1bf6..bdab1a90 100644 --- a/lib/base/test/base-ethereum-l1-node.test.ts +++ b/lib/base/test/base-ethereum-l1-node.test.ts @@ -16,7 +16,7 @@ describe("BaseAMBEthereumSingleNodeStack", () => { // Create the BaseAMBEthereumSingleNodeStack. baseAMBEthereumSingleNode = new BaseAMBEthereumSingleNodeStack(app, "base-ethereum-l1-node", { - stackName: `base-amb-ethereum-single-node-${config.baseNodeConfig.nodeConfiguration}`, + stackName: `base-amb-ethereum-single-node-${config.baseNodeConfig.baseNetworkId}`, env: { account: config.baseConfig.accountId, region: config.baseConfig.region }, ambEthereumNodeNetworkId: config.baseNodeConfig.ambEntereumNodeNetworkId, @@ -50,7 +50,7 @@ describe("BaseAMBEthereumSingleNodeStack", () => { ] }, "Export": { - "Name": "AmbEthereumNodeRpcUrlWithBillingToken" + "Name": "BaseAmbEthereumNodeRpcUrlWithBillingToken" } }) }); diff --git a/lib/base/test/base-single-node.test.ts b/lib/base/test/base-single-node.test.ts index a8e1562c..e7f6dbac 100644 --- a/lib/base/test/base-single-node.test.ts +++ b/lib/base/test/base-single-node.test.ts @@ -15,15 +15,13 @@ describe("BaseSingleNodeStack", () => { // Create the BaseSingleNodeStack. baseSingleNodeStack = new BaseSingleNodeStack(app, "base-single-node", { - stackName: `base-single-node-${config.baseConfig.accountId}`, + stackName: `base-single-node-${config.baseNodeConfig.baseNetworkId}`, env: {account: config.baseConfig.accountId, region: config.baseConfig.region}, instanceType: config.baseNodeConfig.instanceType, instanceCpuType: config.baseNodeConfig.instanceCpuType, - baseVersion: config.baseNodeConfig.baseVersion, - nodeConfiguration: config.baseNodeConfig.nodeConfiguration, + baseNetworkId: config.baseNodeConfig.baseNetworkId, dataVolume: config.baseNodeConfig.dataVolume, - baseNetworkId: config.baseNodeConfig.baseNetworkId }); template = Template.fromStack(baseSingleNodeStack); @@ -75,7 +73,7 @@ describe("BaseSingleNodeStack", () => { UserData: Match.anyValue(), BlockDeviceMappings: [ { - DeviceName: "/dev/sda1", + DeviceName: "/dev/xvda", Ebs: { DeleteOnTermination: true, Encrypted: true, @@ -117,7 +115,7 @@ describe("BaseSingleNodeStack", () => { // Has CloudWatch dashboard. template.hasResourceProperties("AWS::CloudWatch::Dashboard", { DashboardBody: Match.anyValue(), - DashboardName: `base-single-node-${config.baseConfig.accountId}` + DashboardName: `base-single-node-${config.baseNodeConfig.baseNetworkId}` }) }); }); From d8947610b12dbb06632506b04f1489399d9d0afa Mon Sep 17 00:00:00 2001 From: Nikolay Vlasov Date: Thu, 25 Jan 2024 18:00:14 +1100 Subject: [PATCH 03/75] Base code changes after e2e testing --- lib/base/README.md | 2 +- lib/base/app.ts | 1 + lib/base/lib/assets/node-cw-dashboard.ts | 60 +++++++++++++++---- .../assets/sync-checker/syncchecker-base.sh | 37 ++++++++---- lib/base/lib/config/baseConfig.interface.ts | 1 + lib/base/lib/config/baseConfig.ts | 1 + lib/base/lib/single-node-stack.ts | 10 +++- lib/base/sample-configs/.env-sample-rpc | 3 +- 8 files changed, 87 insertions(+), 28 deletions(-) diff --git a/lib/base/README.md b/lib/base/README.md index a78b2c05..c8bb4b74 100644 --- a/lib/base/README.md +++ b/lib/base/README.md @@ -123,7 +123,7 @@ We will use AWS Cloud9 to execute the subsequent commands. Follow the instructio > cdk bootstrap aws://ACCOUNT-NUMBER/REGION > ``` -5. Deploy Amazon Managed Blockchain (AMB) Access Ethereum node and wait about 35-70 minutes for the node to sync +5. (Optional) For L1 node you you can set your own URL in `BASE_L1_ENDPOINT` property of `.env` file or use the command below to deploy Amazon Managed Blockchain (AMB) Access Ethereum node. It takes about 35-70 minutes for the AMB Access node to sync. ```bash pwd diff --git a/lib/base/app.ts b/lib/base/app.ts index a5c048e3..da467559 100644 --- a/lib/base/app.ts +++ b/lib/base/app.ts @@ -31,5 +31,6 @@ new BaseSingleNodeStack(app, "base-single-node", { instanceCpuType: config.baseNodeConfig.instanceCpuType, baseNetworkId: config.baseNodeConfig.baseNetworkId, restoreFromSnapshot: config.baseNodeConfig.restoreFromSnapshot, + l1Endpoint: config.baseNodeConfig.l1Endpoint, dataVolume: config.baseNodeConfig.dataVolume, }); diff --git a/lib/base/lib/assets/node-cw-dashboard.ts b/lib/base/lib/assets/node-cw-dashboard.ts index fd8b5462..2d2c926e 100644 --- a/lib/base/lib/assets/node-cw-dashboard.ts +++ b/lib/base/lib/assets/node-cw-dashboard.ts @@ -127,14 +127,14 @@ export const SyncNodeCWDashboardJSON = { } }, { - "height": 5, + "height": 4, "width": 6, - "y": 5, + "y": 0, "x": 12, "type": "metric", "properties": { "metrics": [ - [ "CWAgent", "elc_sync_block", "InstanceId", "${INSTANCE_ID}", { "label": "${INSTANCE_ID}-${INSTANCE_NAME}" } ] + [ "CWAgent", "l2_current_block", "InstanceId", "${INSTANCE_ID}", { "label": "${INSTANCE_ID}-${INSTANCE_NAME}" } ] ], "sparkline": true, "view": "timeSeries", @@ -142,13 +142,13 @@ export const SyncNodeCWDashboardJSON = { "region": "${REGION}", "stat": "Maximum", "period": 60, - "title": "Base Client Block Height" + "title": "L2 Current Block" } }, { - "height": 5, + "height": 4, "width": 6, - "y": 10, + "y": 4, "x": 12, "type": "metric", "properties": { @@ -159,9 +159,47 @@ export const SyncNodeCWDashboardJSON = { "stat": "Maximum", "period": 60, "metrics": [ - [ "CWAgent", "elc_blocks_behind", "InstanceId", "${INSTANCE_ID}", { "label": "${INSTANCE_ID}-${INSTANCE_NAME}" } ] + [ "CWAgent", "l2_blocks_behind", "InstanceId", "${INSTANCE_ID}", { "label": "${INSTANCE_ID}-${INSTANCE_NAME}" } ] + ], + "title": "L2 Blocks Behind" + } + }, + { + "height": 3, + "width": 6, + "y": 8, + "x": 12, + "type": "metric", + "properties": { + "metrics": [ + [ "CWAgent", "l1_current_block", "InstanceId", "${INSTANCE_ID}", { "label": "${INSTANCE_ID}-${INSTANCE_NAME}" } ] ], - "title": "Base Client Blocks Behind" + "sparkline": true, + "view": "timeSeries", + "stacked": false, + "region": "${REGION}", + "stat": "Maximum", + "period": 60, + "title": "L1 Current Block" + } + }, + { + "height": 4, + "width": 6, + "y": 11, + "x": 12, + "type": "metric", + "properties": { + "sparkline": true, + "view": "timeSeries", + "stacked": false, + "region": "${REGION}", + "stat": "Maximum", + "period": 60, + "metrics": [ + [ "CWAgent", "l1_blocks_behind", "InstanceId", "${INSTANCE_ID}", { "label": "${INSTANCE_ID}-${INSTANCE_NAME}" } ] + ], + "title": "L1 Blocks Behind" } }, { @@ -214,10 +252,10 @@ export const SyncNodeCWDashboardJSON = { } }, { - "height": 5, + "height": 3, "width": 6, - "y": 0, - "x": 12, + "y": 15, + "x": 6, "type": "metric", "properties": { "metrics": [ diff --git a/lib/base/lib/assets/sync-checker/syncchecker-base.sh b/lib/base/lib/assets/sync-checker/syncchecker-base.sh index a084df3b..fabc6d09 100644 --- a/lib/base/lib/assets/sync-checker/syncchecker-base.sh +++ b/lib/base/lib/assets/sync-checker/syncchecker-base.sh @@ -1,18 +1,25 @@ #!/bin/bash +source /etc/environment -BASE_SYNC_STATS=$(curl -s -X POST -H "Content-Type: application/json" --data '{"jsonrpc":"2.0","method":"eth_syncing","params":[],"id":1}' http://localhost:8545 | jq -r ".result") +OPTIMISM_SYNC_STATUS=$(curl -s -X POST -H "Content-Type: application/json" --data '{"id":0,"jsonrpc":"2.0","method":"optimism_syncStatus"}' http://localhost:7545 | jq -r ".result") -if [[ "$BASE_SYNC_STATS" == "false" ]]; then - BASE_SYNC_BLOCK_HEX=$(curl -s -X POST -H "Content-Type: application/json" --data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' http://localhost:8545 | jq -r ".result") - BASE_HIGHEST_BLOCK_HEX=$BASE_SYNC_BLOCK_HEX -else - BASE_SYNC_BLOCK_HEX=$(echo $BASE_SYNC_STATS | jq -r ".currentBlock") - BASE_HIGHEST_BLOCK_HEX=$(echo $BASE_SYNC_STATS | jq -r ".highestBlock") -fi +# L1 client stats +L1_CLIENT_HEAD=$(echo $OPTIMISM_SYNC_STATUS | jq -r ".head_l1.number") +L1_CLIENT_CURRENT=$(echo $OPTIMISM_SYNC_STATUS | jq -r ".current_l1.number") +L1_CLIENT_BLOCKS_BEHIND="$((L1_CLIENT_HEAD-L1_CLIENT_CURRENT))" -BASE_HIGHEST_BLOCK=$(echo $((${BASE_HIGHEST_BLOCK_HEX}))) -BASE_SYNC_BLOCK=$(echo $((${BASE_SYNC_BLOCK_HEX}))) -BASE_BLOCKS_BEHIND="$((BASE_HIGHEST_BLOCK-BASE_SYNC_BLOCK))" +# L2 client stats +L2_CLIENT_HEAD=$(echo $OPTIMISM_SYNC_STATUS | jq -r ".queued_unsafe_l2.number") +L2_CLIENT_CURRENT=$(echo $OPTIMISM_SYNC_STATUS | jq -r ".unsafe_l2.number") +L2_CLIENT_BLOCKS_BEHIND="$((L2_CLIENT_HEAD-L2_CLIENT_CURRENT))" + +echo "L1_CLIENT_HEAD="$L1_CLIENT_HEAD +echo "L1_CLIENT_CURRENT="$L1_CLIENT_CURRENT +echo "L1_CLIENT_BLOCKS_BEHIND="$L1_CLIENT_BLOCKS_BEHIND + +echo "L2_CLIENT_HEAD="$L2_CLIENT_HEAD +echo "L2_CLIENT_CURRENT="$L2_CLIENT_CURRENT +echo "L2_CLIENT_BLOCKS_BEHIND="$L2_CLIENT_BLOCKS_BEHIND # Sending data to CloudWatch TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600") @@ -20,5 +27,9 @@ INSTANCE_ID=$(curl -H "X-aws-ec2-metadata-token: $TOKEN" -s http://169.254.169.2 REGION=$(curl -H "X-aws-ec2-metadata-token: $TOKEN" -s http://169.254.169.254/latest/dynamic/instance-identity/document | jq .region -r) TIMESTAMP=$(date +"%Y-%m-%dT%H:%M:%S%:z") -aws cloudwatch put-metric-data --metric-name elc_sync_block --namespace CWAgent --value $BASE_SYNC_BLOCK --timestamp $TIMESTAMP --dimensions InstanceId=$INSTANCE_ID --region $REGION -aws cloudwatch put-metric-data --metric-name elc_blocks_behind --namespace CWAgent --value $BASE_BLOCKS_BEHIND --timestamp $TIMESTAMP --dimensions InstanceId=$INSTANCE_ID --region $REGION +aws cloudwatch put-metric-data --metric-name l1_current_block --namespace CWAgent --value $L1_CLIENT_CURRENT --timestamp $TIMESTAMP --dimensions InstanceId=$INSTANCE_ID --region $REGION +aws cloudwatch put-metric-data --metric-name l1_blocks_behind --namespace CWAgent --value $L1_CLIENT_BLOCKS_BEHIND --timestamp $TIMESTAMP --dimensions InstanceId=$INSTANCE_ID --region $REGION + +aws cloudwatch put-metric-data --metric-name l2_current_block --namespace CWAgent --value $L2_CLIENT_CURRENT --timestamp $TIMESTAMP --dimensions InstanceId=$INSTANCE_ID --region $REGION +aws cloudwatch put-metric-data --metric-name l2_blocks_behind --namespace CWAgent --value $L2_CLIENT_BLOCKS_BEHIND --timestamp $TIMESTAMP --dimensions InstanceId=$INSTANCE_ID --region $REGION + diff --git a/lib/base/lib/config/baseConfig.interface.ts b/lib/base/lib/config/baseConfig.interface.ts index 6dbf1e33..89290d0f 100644 --- a/lib/base/lib/config/baseConfig.interface.ts +++ b/lib/base/lib/config/baseConfig.interface.ts @@ -20,4 +20,5 @@ export interface BaseBaseNodeConfig extends configTypes.BaseNodeConfig { baseNetworkId: BaseNetworkId; dataVolume: BaseDataVolumeConfig; restoreFromSnapshot: boolean; + l1Endpoint: string; } diff --git a/lib/base/lib/config/baseConfig.ts b/lib/base/lib/config/baseConfig.ts index 44ed175c..73ba97c7 100644 --- a/lib/base/lib/config/baseConfig.ts +++ b/lib/base/lib/config/baseConfig.ts @@ -30,6 +30,7 @@ export const baseNodeConfig: configTypes.BaseBaseNodeConfig = { instanceCpuType: process.env.BASE_CPU_TYPE?.toLowerCase() == "x86_64" ? ec2.AmazonLinuxCpuType.X86_64 : ec2.AmazonLinuxCpuType.ARM_64, baseNetworkId: process.env.BASE_NETWORK_ID || "mainnet", restoreFromSnapshot: process.env.BASE_RESTORE_FROM_SNAPSHOT?.toLowerCase() == "true" ? true : false, + l1Endpoint: process.env.BASE_L1_ENDPOINT || constants.NoneValue, dataVolume: { sizeGiB: process.env.BASE_DATA_VOL_SIZE ? parseInt(process.env.BASE_DATA_VOL_SIZE): 1000, type: parseDataVolumeType(process.env.BASE_DATA_VOL_TYPE?.toLowerCase() ? process.env.BASE_DATA_VOL_TYPE?.toLowerCase() : "gp3"), diff --git a/lib/base/lib/single-node-stack.ts b/lib/base/lib/single-node-stack.ts index 814edac3..5c6c6827 100644 --- a/lib/base/lib/single-node-stack.ts +++ b/lib/base/lib/single-node-stack.ts @@ -18,6 +18,7 @@ export interface BaseSingleNodeStackProps extends cdk.StackProps { instanceCpuType: ec2.AmazonLinuxCpuType; baseNetworkId: configTypes.BaseNetworkId; restoreFromSnapshot: boolean; + l1Endpoint: string; dataVolume: configTypes.BaseDataVolumeConfig; } @@ -38,6 +39,7 @@ export class BaseSingleNodeStack extends cdk.Stack { instanceCpuType, baseNetworkId, restoreFromSnapshot, + l1Endpoint, dataVolume, } = props; @@ -56,8 +58,12 @@ export class BaseSingleNodeStack extends cdk.Stack { // Getting the IAM role ARN from the common stack const importedInstanceRoleArn = cdk.Fn.importValue("BaseNodeInstanceRoleArn"); - const ambEthereumNodeRpcUrlWithBillingToken = cdk.Fn.importValue("BaseAmbEthereumNodeRpcUrlWithBillingToken"); + // If user has not supplied the URL for L1, attempting to use AMB node URL + let l1EndpointURL = l1Endpoint; + if (l1EndpointURL === constants.NoneValue){ + l1EndpointURL = cdk.Fn.importValue("BaseAmbEthereumNodeRpcUrlWithBillingToken"); + } const instanceRole = iam.Role.fromRoleArn(this, "iam-role", importedInstanceRoleArn); // Making sure our instance will be able to read the assets @@ -102,7 +108,7 @@ export class BaseSingleNodeStack extends cdk.Stack { _AUTOSCALING_GROUP_NAME_: constants.NoneValue, _RESTORE_FROM_SNAPSHOT_: restoreFromSnapshot.toString(), _FORMAT_DISK_: "true", - _L1_ENDPOINT_: ambEthereumNodeRpcUrlWithBillingToken, + _L1_ENDPOINT_: l1EndpointURL, }); node.instance.addUserData(modifiedInitNodeScript); diff --git a/lib/base/sample-configs/.env-sample-rpc b/lib/base/sample-configs/.env-sample-rpc index daca3b70..538606da 100644 --- a/lib/base/sample-configs/.env-sample-rpc +++ b/lib/base/sample-configs/.env-sample-rpc @@ -18,4 +18,5 @@ BASE_DATA_VOL_TYPE="gp3" # Other options: "io1" | "io2" | "gp3" | BASE_DATA_VOL_SIZE="3000" # Current required data size to keep both snapshot archive and unarchived version of it BASE_DATA_VOL_IOPS="5000" # Max IOPS for EBS volumes (not applicable for "instance-store") BASE_DATA_VOL_THROUGHPUT="700" # Max throughput for EBS gp3 volumes (not applicable for "io1" | "io2" | "instance-store") -BASE_RESTORE_FROM_SNAPSHOT="false" # Download snapshot to speed up statup time \ No newline at end of file +BASE_RESTORE_FROM_SNAPSHOT="false" # Download snapshot to speed up statup time +BASE_L1_ENDPOINT="none" # Leave as "none" if you use Amazon Managed Blockchain (AMB) Access Ethereum node or set your own L1 URL \ No newline at end of file From e2a6a0412a6e9e40cc138c0884b73458150267d4 Mon Sep 17 00:00:00 2001 From: Nikolay Vlasov Date: Mon, 29 Jan 2024 17:04:26 +1100 Subject: [PATCH 04/75] WIP Base node bug fixes --- lib/base/README.md | 47 +------------------ .../assets/sync-checker/syncchecker-base.sh | 7 ++- lib/base/lib/single-node-stack.ts | 5 -- lib/base/sample-configs/.env-sample-rpc | 4 +- 4 files changed, 9 insertions(+), 54 deletions(-) diff --git a/lib/base/README.md b/lib/base/README.md index c8bb4b74..7fe3e959 100644 --- a/lib/base/README.md +++ b/lib/base/README.md @@ -16,36 +16,6 @@ ## Additional materials -

- -Review the for pros and cons of this solution. - -### Well-Architected Checklist - -This is the Well-Architected checklist for AWS Blockchain Node Runner app. This checklist takes into account questions from the [AWS Well-Architected Framework](https://aws.amazon.com/architecture/well-architected/) which are relevant to this workload. Please feel free to add more checks from the framework if required for your workload. - -| Pillar | Control | Question/Check | Remarks | -|:------------------------|:----------------------------------|:---------------------------------------------------------------------------------|:-----------------| -| Security | Network protection | Are there unnecessary open ports in security groups? | Please note that ports 9222 (TCP/UDP) for Base are open to public to support P2P protocols. We have to rely on the protection mechanisms built into the Base software to protect those ports. | -| | | Traffic inspection | AWS WAF could be implemented for traffic inspection. Additional charges will apply. | -| | Compute protection | Reduce attack surface | This solution uses Amazon Linux AMI. You may choose to run hardening scripts on it. | -| | | Enable people to perform actions at a distance | This solution uses AWS Systems Manager for terminal session, not ssh ports. | -| | Data protection at rest | Use encrypted Amazon Elastic Block Store (Amazon EBS) volumes | This solution uses encrypted Amazon EBS volumes. | -| | Data protection in transit | Use TLS | By design TLS is not used in Base RPC and P2P protocols because the data is considered public. To protect RPC traffic we expose the port only for internal use. | -| | Authorization and access control | Use instance profile with Amazon Elastic Compute Cloud (Amazon EC2) instances | This solution uses AWS Identity and Access Management (AWS IAM) role instead of IAM user. | -| | | Following principle of least privilege access | In the node, root user is not used (using special user "ubuntu" instead). | -| | Application security | Security focused development practices | cdk-nag is being used with documented suppressions. | -| Cost optimization | Service selection | Use cost effective resources | Base nodes currently doesn't provide binaries for ARM architecture, so AMD-powered EC2 instance type for better cost effectiveness. | -| | Cost awareness | Estimate costs | One Base node on m6a.2xlarge and 1T EBS gp3 volume will cost around US$367.21 per month in the US East (N. Virginia) region. Additionally the AMB Access Ethereum on bc.m5.xlarge will cost additional ~US$202 per month in the US East (N. Virginia) region. Approximately the total cost will be US$367.21 + US$202 = US$569.21 per month. | -| Reliability | Resiliency implementation | Withstand component failures | This solution currently does not have high availability and is deployed to a single availability zone. | -| | Data backup | How is data backed up? | The data is not specially backed up. The node will have to re-sync its state from other nodes in the Base network to recover. | -| | Resource monitoring | How are workload resources monitored? | Resources are being monitored using Amazon CloudWatch dashboards. Amazon CloudWatch custom metrics are being pushed via CloudWatch Agent. | -| Performance efficiency | Compute selection | How is compute solution selected? | Compute solution is selected based on the recommendations the from Base community to provide stable and cost-effective operations. | -| | Storage selection | How is storage solution selected? | Storage solution is selected based on the recommendations the from Base community to provide stable and cost-effective operations. | -| | Architecture selection | How is the best performance architecture selected? | In this solution we try to balance price and performance to achieve better cost efficiency, but not necessarily the best performance. | -| Operational excellence | Workload health | How is health of workload determined? | We rely on the standard EC2 instance monitoring tool to detect stalled instances. | -| Sustainability | Hardware & services | Select most efficient hardware for your workload | Base nodes currently doesn't provide binaries for ARM architecture, so AMD-powered EC2 instance type for better cost effectiveness. | -
Recommended Infrastructure @@ -61,11 +31,6 @@ This is the Well-Architected checklist for AWS Blockchain Node Runner app. This - Instance type [m6a.2xlarge](https://aws.amazon.com/ec2/instance-types/m6a/). - 2500GB EBS gp3 storage with at least 6000 IOPS.` -**Amazon Managed Blockchain Ethereum L1** - -- Minimum instance type: [bc.m5.xlarge](https://aws.amazon.com/managed-blockchain/instance-types/) -- Recommended instance type: [bc.m5.2xlarge](https://aws.amazon.com/managed-blockchain/instance-types/) -
## Setup Instructions @@ -123,14 +88,7 @@ We will use AWS Cloud9 to execute the subsequent commands. Follow the instructio > cdk bootstrap aws://ACCOUNT-NUMBER/REGION > ``` -5. (Optional) For L1 node you you can set your own URL in `BASE_L1_ENDPOINT` property of `.env` file or use the command below to deploy Amazon Managed Blockchain (AMB) Access Ethereum node. It takes about 35-70 minutes for the AMB Access node to sync. - - ```bash - pwd - # Make sure you are in aws-blockchain-node-runners/lib/base - npx cdk deploy base-ethereum-l1-node --json --outputs-file base-ethereum-l1-node.json - ``` - To watch the progress, open the [AMB Web UI](https://console.aws.amazon.com/managedblockchain/home), click the name of your target network from the list (Mainnet, Goerly, etc.) and watch the status of the node to change from `Creating` to `Available`. +5. For L1 node you you can set your own URL in `BASE_L1_ENDPOINT` property of `.env` file. It can be one of [the providers recommended by Base](https://docs.base.org/tools/node-providers) or run your own Ethereum node [with Node Runner blueprint](https://aws-samples.github.io/aws-blockchain-node-runners/docs/Blueprints/Ethereum). 6. Deploy Base RPC Node and wait for another 10-20 minutes for it to sync @@ -184,9 +142,6 @@ A script on the Base node publishes current block and blocks behind metrics to C # Undeploy Single Node npx cdk destroy base-single-node - # Undeploy AMB Etheruem node - npx cdk destroy base-ethereum-l1-node - # Delete all common components like IAM role and Security Group npx cdk destroy base-common ``` diff --git a/lib/base/lib/assets/sync-checker/syncchecker-base.sh b/lib/base/lib/assets/sync-checker/syncchecker-base.sh index fabc6d09..b912a637 100644 --- a/lib/base/lib/assets/sync-checker/syncchecker-base.sh +++ b/lib/base/lib/assets/sync-checker/syncchecker-base.sh @@ -6,7 +6,12 @@ OPTIMISM_SYNC_STATUS=$(curl -s -X POST -H "Content-Type: application/json" --dat # L1 client stats L1_CLIENT_HEAD=$(echo $OPTIMISM_SYNC_STATUS | jq -r ".head_l1.number") L1_CLIENT_CURRENT=$(echo $OPTIMISM_SYNC_STATUS | jq -r ".current_l1.number") -L1_CLIENT_BLOCKS_BEHIND="$((L1_CLIENT_HEAD-L1_CLIENT_CURRENT))" + +if [ $L1_CLIENT_HEAD -eq 0 ]; then + L1_CLIENT_BLOCKS_BEHIND=0 +else + L1_CLIENT_BLOCKS_BEHIND="$((L1_CLIENT_HEAD-L1_CLIENT_CURRENT))" +fi # L2 client stats L2_CLIENT_HEAD=$(echo $OPTIMISM_SYNC_STATUS | jq -r ".queued_unsafe_l2.number") diff --git a/lib/base/lib/single-node-stack.ts b/lib/base/lib/single-node-stack.ts index 5c6c6827..0292d9f0 100644 --- a/lib/base/lib/single-node-stack.ts +++ b/lib/base/lib/single-node-stack.ts @@ -69,11 +69,6 @@ export class BaseSingleNodeStack extends cdk.Stack { // Making sure our instance will be able to read the assets asset.bucket.grantRead(instanceRole); - // Setting up the node using generic Single Node constract - if (instanceCpuType === ec2.AmazonLinuxCpuType.ARM_64) { - throw new Error("ARM_64 is not yet supported"); - } - const node = new SingleNodeConstruct(this, "rpc-node", { instanceName: STACK_NAME, instanceType, diff --git a/lib/base/sample-configs/.env-sample-rpc b/lib/base/sample-configs/.env-sample-rpc index 538606da..2edc9750 100644 --- a/lib/base/sample-configs/.env-sample-rpc +++ b/lib/base/sample-configs/.env-sample-rpc @@ -11,8 +11,8 @@ AMB_ETHEREUM_NODE_NETWORK_ID="mainnet" # All options: "mainnet", "goerl AMB_ETHEREUM_NODE_INSTANCE_TYPE="bc.m5.xlarge" # For available options see: https://aws.amazon.com/managed-blockchain/instance-types/ BASE_NETWORK_ID="mainnet" # All options: "mainnet", "goerli" -BASE_INSTANCE_TYPE="m6a.2xlarge" -BASE_CPU_TYPE="x86_64" # All options: "x86_64", "ARM_64". IMPORTANT: Make sure the CPU type matches the instance type used +BASE_INSTANCE_TYPE="m7g.2xlarge" +BASE_CPU_TYPE="ARM_64" # All options: "x86_64", "ARM_64". IMPORTANT: Make sure the CPU type matches the instance type used # Data volume configuration BASE_DATA_VOL_TYPE="gp3" # Other options: "io1" | "io2" | "gp3" | "instance-store" . IMPORTANT: Use "instance-store" option only with instance types that support that feature, like popular for node im4gn, d3, i3en, and i4i instance families BASE_DATA_VOL_SIZE="3000" # Current required data size to keep both snapshot archive and unarchived version of it From a42d8ecfd60551e35a606d47738a0be748556b30 Mon Sep 17 00:00:00 2001 From: Nikolay Vlasov Date: Fri, 2 Feb 2024 17:35:34 +1100 Subject: [PATCH 05/75] Base updates to README --- lib/base/README.md | 209 ++++++++++-------- .../doc/assets/Architecture-SingleNode-v3.png | Bin 0 -> 67677 bytes .../doc/assets/Architecture-SingleNode.drawio | 6 +- lib/base/lib/single-node-stack.ts | 2 +- lib/base/sample-configs/.env-sample-rpc | 2 +- 5 files changed, 116 insertions(+), 103 deletions(-) create mode 100644 lib/base/doc/assets/Architecture-SingleNode-v3.png diff --git a/lib/base/README.md b/lib/base/README.md index 7fe3e959..48c4b453 100644 --- a/lib/base/README.md +++ b/lib/base/README.md @@ -10,10 +10,9 @@ 1. A Base node deployed in the [Default VPC](https://docs.aws.amazon.com/vpc/latest/userguide/default-vpc.html) continuously synchronizes with the rest of nodes on Base blockchain network through [Internet Gateway](https://docs.aws.amazon.com/vpc/latest/userguide/VPC_Internet_Gateway.html). 2. The Base node is used by dApps or development tools internally from within the Default VPC. JSON RPC API is not exposed to the Internet directly to protect nodes from unauthorized access. -3. You will need access to a fully-synced Ethereum Mainnet RPC endpoint before running. +3. Your Base node needs access to a fully-synced [Ethereum Mainnet or Sepolia RPC endpoint](https://docs.base.org/tools/node-providers) . 4. The Base node sends various monitoring metrics for both EC2 and Base nodes to Amazon CloudWatch. - ## Additional materials
@@ -23,12 +22,12 @@ **Minimum for Base node** -- Instance type [m6a.xlarge](https://aws.amazon.com/ec2/instance-types/m6a/). +- Instance type [m7g.2xlarge](https://aws.amazon.com/ec2/instance-types/m6a/). - 2500GB EBS gp3 storage with at least 6000 IOPS. **Recommended for Base node** -- Instance type [m6a.2xlarge](https://aws.amazon.com/ec2/instance-types/m6a/). +- Instance type [m7g.2xlarge](https://aws.amazon.com/ec2/instance-types/m6a/). - 2500GB EBS gp3 storage with at least 6000 IOPS.`
@@ -39,7 +38,11 @@ We will use AWS Cloud9 to execute the subsequent commands. Follow the instructions in [Cloud9 Setup](../../docs/setup-cloud9.md) -### Clone this repository and install dependencies +### Make sure you have access to Ethereum L1 node + +Base node needs a URL to a Full Ethereum Node to validate blocks it receives. You can run your own with [Ethereum node blueprint](https://aws-samples.github.io/aws-blockchain-node-runners/docs/Blueprints/Ethereum) or use [one of Base partners](https://docs.base.org/tools/node-providers). + +### On your Cloud9: Clone this repository and install dependencies ```bash git clone https://github.com/alickwong/aws-blockchain-node-runners @@ -47,15 +50,15 @@ We will use AWS Cloud9 to execute the subsequent commands. Follow the instructio npm install ``` -### Deploy Single Node +### From your Cloud9: Deploy required dependencies 1. Make sure you are in the root directory of the cloned repository 2. If you have deleted or don't have the default VPC, create default VPC - ```bash - aws ec2 create-default-vpc - ``` + ```bash + aws ec2 create-default-vpc + ``` > NOTE: > You may see the following error if the default VPC already exists: `An error occurred (DefaultVpcAlreadyExists) when calling the CreateDefaultVpc operation: A Default VPC already exists for this account in this region.`. That means you can just continue with the following steps. @@ -63,24 +66,24 @@ We will use AWS Cloud9 to execute the subsequent commands. Follow the instructio 3. Configure your setup Create your own copy of `.env` file and edit it to update with your AWS Account ID and Region: - ```bash - # Make sure you are in aws-blockchain-node-runners/lib/base - cd lib/base - npm install - pwd - cp ./sample-configs/.env-sample-rpc .env - nano .env - ``` +```bash +# Make sure you are in aws-blockchain-node-runners/lib/base +cd lib/base +npm install +pwd +cp ./sample-configs/.env-sample-rpc .env +nano .env +``` > NOTE: > Example configuration parameters are set in the local `.env-sample` file. You can find more examples inside `sample-configs` directory. 4. Deploy common components such as IAM role - ```bash - pwd - # Make sure you are in aws-blockchain-node-runners/lib/base - npx cdk deploy base-common - ``` +```bash +pwd +# Make sure you are in aws-blockchain-node-runners/lib/base +npx cdk deploy base-common +``` > IMPORTANT: > All AWS CDK v2 deployments use dedicated AWS resources to hold data during deployment. Therefore, your AWS account and Region must be [bootstrapped](https://docs.aws.amazon.com/cdk/v2/guide/bootstrapping.html) to create these resources before you can deploy. If you haven't already bootstrapped, issue the following command: @@ -88,63 +91,73 @@ We will use AWS Cloud9 to execute the subsequent commands. Follow the instructio > cdk bootstrap aws://ACCOUNT-NUMBER/REGION > ``` -5. For L1 node you you can set your own URL in `BASE_L1_ENDPOINT` property of `.env` file. It can be one of [the providers recommended by Base](https://docs.base.org/tools/node-providers) or run your own Ethereum node [with Node Runner blueprint](https://aws-samples.github.io/aws-blockchain-node-runners/docs/Blueprints/Ethereum). - -6. Deploy Base RPC Node and wait for another 10-20 minutes for it to sync - - ```bash - pwd - # Make sure you are in aws-blockchain-node-runners/lib/base - npx cdk deploy base-single-node --json --outputs-file single-node-deploy.json - ``` - After starting the node you will need to wait for the initial synchronization process to finish.To see the progress, you may use SSM to connect into EC2 first and watch the log like this: - - ```bash - export INSTANCE_ID=$(cat single-node-deploy.json | jq -r '..|.node-instance-id? | select(. != null)') - echo "INSTANCE_ID=" $INSTANCE_ID - export AWS_REGION=us-east-1 - aws ssm start-session --target $INSTANCE_ID --region $AWS_REGION - echo Latest synced block behind by: $((($(date +%s)-$( \ - curl -d '{"id":0,"jsonrpc":"2.0","method":"optimism_syncStatus"}' \ - -H "Content-Type: application/json" http://localhost:7545 | \ - jq -r .result.unsafe_l2.timestamp))/60)) minutes - ``` - -7. Test Base RPC API +### From your Cloud9: Deploy Single Node + +1. For L1 node you you can set your own URL in `BASE_L1_ENDPOINT` property of `.env` file. It can be one of [the providers recommended by Base](https://docs.base.org/tools/node-providers) or run your own Ethereum node [with Node Runner blueprint](https://aws-samples.github.io/aws-blockchain-node-runners/docs/Blueprints/Ethereum). For example: + +```bash +#For Mainnet: +BASE_L1_ENDPOINT=https://1rpc.io/eth + +#For Sepolia: +BASE_L1_ENDPOINT=https://rpc.sepolia.org +``` + +2. Deploy Base RPC Node and wait for it to sync. For Mainnet it might take less than an hour when using snapshots (default) or multiple days if syncing from block 0. + +```bash +pwd +# Make sure you are in aws-blockchain-node-runners/lib/base +npx cdk deploy base-single-node --json --outputs-file single-node-deploy.json +``` +After starting the node you will need to wait for the initial synchronization process to finish.To see the progress, you may use SSM to connect into EC2 first and watch the log like this: + +```bash +export INSTANCE_ID=$(cat single-node-deploy.json | jq -r '..|.node-instance-id? | select(. != null)') +echo "INSTANCE_ID=" $INSTANCE_ID +export AWS_REGION=us-east-1 +aws ssm start-session --target $INSTANCE_ID --region $AWS_REGION +echo Latest synced block behind by: $((($(date +%s)-$( \ +curl -d '{"id":0,"jsonrpc":"2.0","method":"optimism_syncStatus"}' \ +-H "Content-Type: application/json" http://localhost:7545 | \ +jq -r .result.unsafe_l2.timestamp))/60)) minutes +``` + +3. Test Base RPC API [TODO: Is there an address we can query balance from?] Use curl to query from within the node instance: - ```bash - export INSTANCE_ID=$(cat single-node-deploy.json | jq -r '..|.node-instance-id? | select(. != null)') - echo "INSTANCE_ID=" $INSTANCE_ID - export AWS_REGION=us-east-1 - aws ssm start-session --target $INSTANCE_ID --region $AWS_REGION +```bash +export INSTANCE_ID=$(cat single-node-deploy.json | jq -r '..|.node-instance-id? | select(. != null)') +echo "INSTANCE_ID=" $INSTANCE_ID +export AWS_REGION=us-east-1 +aws ssm start-session --target $INSTANCE_ID --region $AWS_REGION - curl -s -X POST -H "Content-Type: application/json" --data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' http://localhost:8545 - ``` +curl -s -X POST -H "Content-Type: application/json" --data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' http://localhost:8545 +``` ### Monitoring A script on the Base node publishes current block and blocks behind metrics to CloudWatch metrics every 5 minutes. When the node is fully synced the blocks behind metric should get to 0, which might take about 1.5 days. To see the metrics: - Navigate to CloudWatch service (make sure you are in the region you have specified for AWS_REGION) -- Open Dashboards and select `base-single-node` from the list of dashboards. +- Open Dashboards and select `base-single-node-` from the list of dashboards. -## Clear up and undeploy everything +## From your Cloud9: Clear up and undeploy everything 1. Undeploy all Nodes and Common stacks - ```bash - # Setting the AWS account id and region in case local .env file is lost - export AWS_ACCOUNT_ID= - export AWS_REGION= +```bash +# Setting the AWS account id and region in case local .env file is lost +export AWS_ACCOUNT_ID= +export AWS_REGION= - pwd - # Make sure you are in aws-blockchain-node-runners/lib/base +pwd +# Make sure you are in aws-blockchain-node-runners/lib/base - # Undeploy Single Node - npx cdk destroy base-single-node +# Undeploy Single Node +npx cdk destroy base-single-node - # Delete all common components like IAM role and Security Group - npx cdk destroy base-common - ``` +# Delete all common components like IAM role and Security Group +npx cdk destroy base-common +``` 2. Follow steps to delete the Cloud9 instance in [Cloud9 Setup](../../doc/setup-cloud9.md) @@ -154,44 +167,44 @@ A script on the Base node publishes current block and blocks behind metrics to C **Note:** In this tutorial we chose not to use SSH and use Session Manager instead. That allows you to log all sessions in AWS CloudTrail to see who logged into the server and when. If you receive an error similar to `SessionManagerPlugin is not found`, [install Session Manager plugin for AWS CLI](https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-working-with-install-plugin.html) - ```bash - pwd - # Make sure you are in aws-blockchain-node-runners/lib/base - - export INSTANCE_ID=$(cat single-node-deploy.json | jq -r '..|.node-instance-id? | select(. != null)') - echo "INSTANCE_ID=" $INSTANCE_ID - export AWS_REGION=us-east-1 - aws ssm start-session --target $INSTANCE_ID --region $AWS_REGION - sudo su bcuser - # Geth logs: - docker logs --tail 50 node_geth_1 -f - # Base logs: - docker logs --tail 50 node_node_1 -f - ``` +```bash +pwd +# Make sure you are in aws-blockchain-node-runners/lib/base + +export INSTANCE_ID=$(cat single-node-deploy.json | jq -r '..|.node-instance-id? | select(. != null)') +echo "INSTANCE_ID=" $INSTANCE_ID +export AWS_REGION=us-east-1 +aws ssm start-session --target $INSTANCE_ID --region $AWS_REGION +sudo su bcuser +# Geth logs: +docker logs --tail 50 node_geth_1 -f +# Base logs: +docker logs --tail 50 node_node_1 -f +``` 2. How to check the logs from the EC2 user-data script? - ```bash - pwd - # Make sure you are in aws-blockchain-node-runners/lib/base - - export INSTANCE_ID=$(cat single-node-deploy.json | jq -r '..|.node-instance-id? | select(. != null)') - echo "INSTANCE_ID=" $INSTANCE_ID - export AWS_REGION=us-east-1 - aws ssm start-session --target $INSTANCE_ID --region $AWS_REGION - sudo cat /var/log/cloud-init-output.log - ``` +```bash +pwd +# Make sure you are in aws-blockchain-node-runners/lib/base + +export INSTANCE_ID=$(cat single-node-deploy.json | jq -r '..|.node-instance-id? | select(. != null)') +echo "INSTANCE_ID=" $INSTANCE_ID +export AWS_REGION=us-east-1 +aws ssm start-session --target $INSTANCE_ID --region $AWS_REGION +sudo cat /var/log/cloud-init-output.log +``` 3. How can I restart the Base node? - ``` bash - export INSTANCE_ID=$(cat single-node-deploy.json | jq -r '..|.node-instance-id? | select(. != null)') - echo "INSTANCE_ID=" $INSTANCE_ID - export AWS_REGION=us-east-1 - aws ssm start-session --target $INSTANCE_ID --region $AWS_REGION - sudo su bcuser - /usr/local/bin/docker-compose -f /home/bcuser/node/docker-compose.yml down && \ - /usr/local/bin/docker-compose -f /home/bcuser/node/docker-compose.yml up -d - ``` +``` bash +export INSTANCE_ID=$(cat single-node-deploy.json | jq -r '..|.node-instance-id? | select(. != null)') +echo "INSTANCE_ID=" $INSTANCE_ID +export AWS_REGION=us-east-1 +aws ssm start-session --target $INSTANCE_ID --region $AWS_REGION +sudo su bcuser +/usr/local/bin/docker-compose -f /home/bcuser/node/docker-compose.yml down && \ +/usr/local/bin/docker-compose -f /home/bcuser/node/docker-compose.yml up -d +``` 4. Where to find the key Base client directories? - The data directory is `/data` diff --git a/lib/base/doc/assets/Architecture-SingleNode-v3.png b/lib/base/doc/assets/Architecture-SingleNode-v3.png new file mode 100644 index 0000000000000000000000000000000000000000..35191bf7a10dd44ab14b42b439c4bf9ebeeb1f1e GIT binary patch literal 67677 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

t<(c_=yk;Cj)sg-%aTeR>0Qe`#?dQ>sp5#p6XO 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 SRqozg5!gIov6im>dFbC-Q80-B From d7c6b226d1816c7aeed56fe9deee7ba2c7a37a3b Mon Sep 17 00:00:00 2001 From: Nikolay Vlasov Date: Wed, 7 Feb 2024 16:28:55 +1100 Subject: [PATCH 07/75] Base fixed sync checker L2 blocks behind metric bug --- lib/base/README.md | 35 +++++++++++++++++-- .../assets/sync-checker/syncchecker-base.sh | 7 +++- lib/base/sample-configs/.env-sample-rpc | 16 +++++---- 3 files changed, 48 insertions(+), 10 deletions(-) diff --git a/lib/base/README.md b/lib/base/README.md index 9041e23b..07a9fad2 100644 --- a/lib/base/README.md +++ b/lib/base/README.md @@ -15,6 +15,35 @@ ## Additional materials +

Review the for pros and cons of this solution. + +### Well-Architected Checklist + +This is the Well-Architected checklist for Ethereum nodes implementation of the AWS Blockchain Node Runner app. This checklist takes into account questions from the [AWS Well-Architected Framework](https://aws.amazon.com/architecture/well-architected/) which are relevant to this workload. Please feel free to add more checks from the framework if required for your workload. + +| Pillar | Control | Question/Check | Remarks | +|:------------------------|:----------------------------------|:---------------------------------------------------------------------------------|:-----------------| +| Security | Network protection | Are there unnecessary open ports in security groups? | Please note that port 9222 (TCP/UDP) for Base are open to public to support P2P protocols. We have to rely on the protection mechanisms built into the Base software to protect those ports. | +| | | Traffic inspection | AWS WAF could be implemented for traffic inspection. Additional charges will apply. | +| | Compute protection | Reduce attack surface | This solution uses Amazon Linux 2 AMI. You may choose to run hardening scripts on it. | +| | | Enable people to perform actions at a distance | This solution uses AWS Systems Manager for terminal session, not ssh ports. | +| | Data protection at rest | Use encrypted Amazon Elastic Block Store (Amazon EBS) volumes | This solution uses encrypted Amazon EBS volumes. | +| | Data protection in transit | Use TLS | By design TLS is not used in Base RPC and P2P protocols because the data is considered public. To protect RPC traffic we expose the port only for internal use. | +| | Authorization and access control | Use instance profile with Amazon Elastic Compute Cloud (Amazon EC2) instances | This solution uses AWS Identity and Access Management (AWS IAM) role instead of IAM user. | +| | | Following principle of least privilege access | In the node, root user is not used (using special user "bcuser" instead). | +| | Application security | Security focused development practices | cdk-nag is being used with documented suppressions. | +| Cost optimization | Service selection | Use cost effective resources | Base nodes works well on ARM architecture and we use Graviton3-powered EC2 instances for better cost effectiveness. | +| | Cost awareness | Estimate costs | One Base node on m7g.2xlarge and 3TiB EBS gp3 volume will cost around US$503.27 per month in the US East (N. Virginia) region. Additional charges will be applied for Ethereum L1 node and might vary between US$200 and US$500 per month. Approximately the total cost will be US$503.27 + US$500 = US$1003.27 per month. | +| Reliability | Resiliency implementation | Withstand component failures | This solution currently does not have high availability and is deployed to a single availability zone. | +| | Data backup | How is data backed up? | The data is not specially backed up. The node will have to re-sync its state from other nodes in the Base network to recover. | +| | Resource monitoring | How are workload resources monitored? | Resources are being monitored using Amazon CloudWatch dashboards. Amazon CloudWatch custom metrics are being pushed via CloudWatch Agent. | +| Performance efficiency | Compute selection | How is compute solution selected? | Compute solution is selected based on the recommendations the from Base community to provide stable and cost-effective operations. | +| | Storage selection | How is storage solution selected? | Storage solution is selected based on the recommendations the from Base community to provide stable and cost-effective operations. | +| | Architecture selection | How is the best performance architecture selected? | In this solution we try to balance price and performance to achieve better cost efficiency, but not necessarily the best performance. | +| Operational excellence | Workload health | How is health of workload determined? | We rely on the standard EC2 instance monitoring tool to detect stalled instances. | +| Sustainability | Hardware & services | Select most efficient hardware for your workload | We use ARM-powered EC2 instance type for better cost/performance balance. | + +
Recommended Infrastructure @@ -22,12 +51,12 @@ **Minimum for Base node** -- Instance type [m7g.2xlarge](https://aws.amazon.com/ec2/instance-types/m6a/). -- 2500GB EBS gp3 storage with at least 6000 IOPS. +- Instance type [m7g.2xlarge](https://aws.amazon.com/ec2/instance-types/m7g/). +- 2500GB EBS gp3 storage with at least 5000 IOPS. **Recommended for Base node** -- Instance type [m7g.2xlarge](https://aws.amazon.com/ec2/instance-types/m6a/). +- Instance type [m7g.4xlarge](https://aws.amazon.com/ec2/instance-types/m7g/). - 2500GB EBS gp3 storage with at least 6000 IOPS.`
diff --git a/lib/base/lib/assets/sync-checker/syncchecker-base.sh b/lib/base/lib/assets/sync-checker/syncchecker-base.sh index b912a637..fee8e2b9 100644 --- a/lib/base/lib/assets/sync-checker/syncchecker-base.sh +++ b/lib/base/lib/assets/sync-checker/syncchecker-base.sh @@ -16,7 +16,12 @@ fi # L2 client stats L2_CLIENT_HEAD=$(echo $OPTIMISM_SYNC_STATUS | jq -r ".queued_unsafe_l2.number") L2_CLIENT_CURRENT=$(echo $OPTIMISM_SYNC_STATUS | jq -r ".unsafe_l2.number") -L2_CLIENT_BLOCKS_BEHIND="$((L2_CLIENT_HEAD-L2_CLIENT_CURRENT))" + +if [ "$L2_CLIENT_HEAD" == "null" ]; then + L2_CLIENT_BLOCKS_BEHIND=0 +else + L2_CLIENT_BLOCKS_BEHIND="$((L2_CLIENT_HEAD-L2_CLIENT_CURRENT))" +fi echo "L1_CLIENT_HEAD="$L1_CLIENT_HEAD echo "L1_CLIENT_CURRENT="$L1_CLIENT_CURRENT diff --git a/lib/base/sample-configs/.env-sample-rpc b/lib/base/sample-configs/.env-sample-rpc index a4b86c35..fc09f4c9 100644 --- a/lib/base/sample-configs/.env-sample-rpc +++ b/lib/base/sample-configs/.env-sample-rpc @@ -7,16 +7,20 @@ AWS_ACCOUNT_ID="xxxxxxxx" AWS_REGION="us-east-1" # Regions supported by Amazon Managed Blockchain Access Ethereum: https://docs.aws.amazon.com/general/latest/gr/managedblockchain.html#managedblockchain-access ## Common configuration parameters ## -AMB_ETHEREUM_NODE_NETWORK_ID="mainnet" # All options: "mainnet", "goerli" -AMB_ETHEREUM_NODE_INSTANCE_TYPE="bc.m5.xlarge" # For available options see: https://aws.amazon.com/managed-blockchain/instance-types/ BASE_NETWORK_ID="mainnet" # All options: "mainnet", "goerli" - BASE_INSTANCE_TYPE="m7g.2xlarge" BASE_CPU_TYPE="ARM_64" # All options: "x86_64", "ARM_64". IMPORTANT: Make sure the CPU type matches the instance type used + # Data volume configuration BASE_DATA_VOL_TYPE="gp3" # Other options: "io1" | "io2" | "gp3" | "instance-store" . IMPORTANT: Use "instance-store" option only with instance types that support that feature, like popular for node im4gn, d3, i3en, and i4i instance families -BASE_DATA_VOL_SIZE="3000" # Current required data size to keep both snapshot archive and unarchived version of it +BASE_DATA_VOL_SIZE="4100" # Current required data size to keep both snapshot archive and unarchived version of it BASE_DATA_VOL_IOPS="5000" # Max IOPS for EBS volumes (not applicable for "instance-store") BASE_DATA_VOL_THROUGHPUT="700" # Max throughput for EBS gp3 volumes (not applicable for "io1" | "io2" | "instance-store") -BASE_RESTORE_FROM_SNAPSHOT="false" # Download snapshot to speed up statup time -BASE_L1_ENDPOINT="none" # Set your own URL to Ethereum L1 node: https://docs.base.org/tools/node-providers \ No newline at end of file +BASE_RESTORE_FROM_SNAPSHOT="true" # Download snapshot to speed up statup time +BASE_L1_ENDPOINT="none" # Set your own URL to Ethereum L1 node: https://docs.base.org/tools/node-providers + +# Example L1 URLs: +# For Mainnet: +#BASE_L1_ENDPOINT=https://1rpc.io/eth +# For Sepolia: +#BASE_L1_ENDPOINT=https://rpc.sepolia.org \ No newline at end of file From 6207ccdfbaa32f821c61f193e604c53f7b03be64 Mon Sep 17 00:00:00 2001 From: Nikolay Vlasov Date: Thu, 8 Feb 2024 13:39:41 +1100 Subject: [PATCH 08/75] Base Removed AMB node stack for now --- lib/base/app.ts | 9 --- .../lib/amb-ethereum-single-node-stack.ts | 43 -------------- lib/base/lib/config/baseConfig.interface.ts | 2 - lib/base/lib/config/baseConfig.ts | 2 - lib/base/test/.env-test | 2 - lib/base/test/base-ethereum-l1-node.test.ts | 57 ------------------- lib/base/test/base-single-node.test.ts | 16 +++++- 7 files changed, 14 insertions(+), 117 deletions(-) delete mode 100644 lib/base/lib/amb-ethereum-single-node-stack.ts delete mode 100644 lib/base/test/base-ethereum-l1-node.test.ts diff --git a/lib/base/app.ts b/lib/base/app.ts index da467559..81529827 100644 --- a/lib/base/app.ts +++ b/lib/base/app.ts @@ -4,7 +4,6 @@ import 'source-map-support/register'; import * as cdk from 'aws-cdk-lib'; import * as config from "./lib/config/baseConfig"; import {BaseCommonStack} from "./lib/common-stack"; -import {BaseAMBEthereumSingleNodeStack} from "./lib/amb-ethereum-single-node-stack"; import {BaseSingleNodeStack} from "./lib/single-node-stack"; const app = new cdk.App(); @@ -15,14 +14,6 @@ new BaseCommonStack(app, "base-common", { env: { account: config.baseConfig.accountId, region: config.baseConfig.region }, }); -new BaseAMBEthereumSingleNodeStack(app, "base-ethereum-l1-node", { - stackName: `base-amb-ethereum-single-node-${config.baseNodeConfig.baseNetworkId}`, - env: { account: config.baseConfig.accountId, region: config.baseConfig.region }, - - ambEthereumNodeNetworkId: config.baseNodeConfig.ambEntereumNodeNetworkId, - ambEthereumNodeInstanceType: config.baseNodeConfig.ambEntereumNodeInstanceType, -}); - new BaseSingleNodeStack(app, "base-single-node", { stackName: `base-single-node-${config.baseNodeConfig.baseNetworkId}`, env: { account: config.baseConfig.accountId, region: config.baseConfig.region }, diff --git a/lib/base/lib/amb-ethereum-single-node-stack.ts b/lib/base/lib/amb-ethereum-single-node-stack.ts deleted file mode 100644 index 614a171f..00000000 --- a/lib/base/lib/amb-ethereum-single-node-stack.ts +++ /dev/null @@ -1,43 +0,0 @@ -import * as cdk from "aws-cdk-lib"; -import * as cdkConstructs from "constructs"; -import * as configTypes from "./config/baseConfig.interface"; -import { SingleNodeAMBEthereumConstruct } from "../../constructs/amb-ethereum-single-node"; - -export interface BaseAMBEthereumSingleNodeStackProps extends cdk.StackProps { - ambEthereumNodeNetworkId: configTypes.AMBEthereumNodeNetworkId, - ambEthereumNodeInstanceType: string, -} - -export class BaseAMBEthereumSingleNodeStack extends cdk.Stack { - constructor(scope: cdkConstructs.Construct, id: string, props: BaseAMBEthereumSingleNodeStackProps) { - super(scope, id, props); - - // Setting up necessary environment variables - const availabilityZones = cdk.Stack.of(this).availabilityZones; - const chosenAvailabilityZone = availabilityZones.slice(0, 1)[0]; - - // Getting our config from initialization properties - const { - ambEthereumNodeNetworkId, - ambEthereumNodeInstanceType, - } = props; - - // Setting up L1 Ethereum node with AMB Ethereum node construct - - const ambEthereumNode = new SingleNodeAMBEthereumConstruct(this, "base-amb-ethereum-l1-single-node", { - instanceType: ambEthereumNodeInstanceType, - availabilityZone: chosenAvailabilityZone, - ethNetworkId: ambEthereumNodeNetworkId, - }) - - new cdk.CfnOutput(this, "amb-eth-node-id", { - value: ambEthereumNode.nodeId, - exportName: "BaseAmbEthereumNodeId" - }); - - new cdk.CfnOutput(this, "amb-eth-node-rpc-url-billing-token", { - value: ambEthereumNode.rpcUrlWithBillingToken, - exportName: "BaseAmbEthereumNodeRpcUrlWithBillingToken", - }); - } -} diff --git a/lib/base/lib/config/baseConfig.interface.ts b/lib/base/lib/config/baseConfig.interface.ts index 89290d0f..dabce7c9 100644 --- a/lib/base/lib/config/baseConfig.interface.ts +++ b/lib/base/lib/config/baseConfig.interface.ts @@ -15,8 +15,6 @@ export interface BaseBaseConfig extends configTypes.BaseConfig { } export interface BaseBaseNodeConfig extends configTypes.BaseNodeConfig { - ambEntereumNodeNetworkId: configTypes.AMBEthereumNodeNetworkId; - ambEntereumNodeInstanceType: string; baseNetworkId: BaseNetworkId; dataVolume: BaseDataVolumeConfig; restoreFromSnapshot: boolean; diff --git a/lib/base/lib/config/baseConfig.ts b/lib/base/lib/config/baseConfig.ts index 73ba97c7..ae3cf87b 100644 --- a/lib/base/lib/config/baseConfig.ts +++ b/lib/base/lib/config/baseConfig.ts @@ -24,8 +24,6 @@ export const baseConfig: configTypes.BaseBaseConfig = { } export const baseNodeConfig: configTypes.BaseBaseNodeConfig = { - ambEntereumNodeNetworkId: process.env.AMB_ENTEREUM_NODE_NETWORK_ID || "mainnet", - ambEntereumNodeInstanceType: process.env.AMB_ETHEREUM_NODE_INSTANCE_TYPE || "bc.m5.xlarge", instanceType: new ec2.InstanceType(process.env.BASE_INSTANCE_TYPE ? process.env.BASE_INSTANCE_TYPE : "m6a.2xlarge"), instanceCpuType: process.env.BASE_CPU_TYPE?.toLowerCase() == "x86_64" ? ec2.AmazonLinuxCpuType.X86_64 : ec2.AmazonLinuxCpuType.ARM_64, baseNetworkId: process.env.BASE_NETWORK_ID || "mainnet", diff --git a/lib/base/test/.env-test b/lib/base/test/.env-test index 70552ed3..dae5ca49 100644 --- a/lib/base/test/.env-test +++ b/lib/base/test/.env-test @@ -7,8 +7,6 @@ AWS_ACCOUNT_ID="347616198663" AWS_REGION="us-east-1" # Regions supported by Amazon Managed Blockchain Access Ethereum: https://docs.aws.amazon.com/general/latest/gr/managedblockchain.html#managedblockchain-access ## Common configuration parameters ## -AMB_ETHEREUM_NODE_NETWORK_ID="mainnet" # All options: "mainnet", "goerli" -AMB_ETHEREUM_NODE_INSTANCE_TYPE="bc.m5.xlarge" # For available options see: https://aws.amazon.com/managed-blockchain/instance-types/ BASE_NETWORK_ID="mainnet" # All options: "mainnet" BASE_INSTANCE_TYPE="m6a.2xlarge" diff --git a/lib/base/test/base-ethereum-l1-node.test.ts b/lib/base/test/base-ethereum-l1-node.test.ts deleted file mode 100644 index bdab1a90..00000000 --- a/lib/base/test/base-ethereum-l1-node.test.ts +++ /dev/null @@ -1,57 +0,0 @@ -import {Match, Template} from "aws-cdk-lib/assertions"; -import * as cdk from "aws-cdk-lib"; -import * as dotenv from 'dotenv'; - -dotenv.config({path: './test/.env-test'}); -import * as config from "../lib/config/baseConfig"; -import {BaseAMBEthereumSingleNodeStack} from "../lib/amb-ethereum-single-node-stack"; - -describe("BaseAMBEthereumSingleNodeStack", () => { - let app: cdk.App; - let baseAMBEthereumSingleNode: BaseAMBEthereumSingleNodeStack; - let template: Template; - beforeAll(() => { - app = new cdk.App(); - - // Create the BaseAMBEthereumSingleNodeStack. - - baseAMBEthereumSingleNode = new BaseAMBEthereumSingleNodeStack(app, "base-ethereum-l1-node", { - stackName: `base-amb-ethereum-single-node-${config.baseNodeConfig.baseNetworkId}`, - env: { account: config.baseConfig.accountId, region: config.baseConfig.region }, - - ambEthereumNodeNetworkId: config.baseNodeConfig.ambEntereumNodeNetworkId, - ambEthereumNodeInstanceType: config.baseNodeConfig.ambEntereumNodeInstanceType, - }); - - template = Template.fromStack(baseAMBEthereumSingleNode); - }); - - test("Check Node URL is correct", () => { - template.hasOutput("ambethnoderpcurlbillingtoken", { - Value: { - "Fn::Join": [ - "", - [ - "https://", - { - "Fn::GetAtt": [ - Match.anyValue(), - "NodeId" - ] - }, - ".t.ethereum.managedblockchain.us-east-1.amazonaws.com?billingtoken=", - { - "Fn::GetAtt": [ - Match.anyValue(), - "BillingToken" - ] - } - ] - ] - }, - "Export": { - "Name": "BaseAmbEthereumNodeRpcUrlWithBillingToken" - } - }) - }); -}); diff --git a/lib/base/test/base-single-node.test.ts b/lib/base/test/base-single-node.test.ts index e7f6dbac..80c48d47 100644 --- a/lib/base/test/base-single-node.test.ts +++ b/lib/base/test/base-single-node.test.ts @@ -16,11 +16,13 @@ describe("BaseSingleNodeStack", () => { // Create the BaseSingleNodeStack. baseSingleNodeStack = new BaseSingleNodeStack(app, "base-single-node", { stackName: `base-single-node-${config.baseNodeConfig.baseNetworkId}`, - env: {account: config.baseConfig.accountId, region: config.baseConfig.region}, + env: { account: config.baseConfig.accountId, region: config.baseConfig.region }, instanceType: config.baseNodeConfig.instanceType, instanceCpuType: config.baseNodeConfig.instanceCpuType, baseNetworkId: config.baseNodeConfig.baseNetworkId, + restoreFromSnapshot: config.baseNodeConfig.restoreFromSnapshot, + l1Endpoint: config.baseNodeConfig.l1Endpoint, dataVolume: config.baseNodeConfig.dataVolume, }); @@ -115,7 +117,17 @@ describe("BaseSingleNodeStack", () => { // Has CloudWatch dashboard. template.hasResourceProperties("AWS::CloudWatch::Dashboard", { DashboardBody: Match.anyValue(), - DashboardName: `base-single-node-${config.baseNodeConfig.baseNetworkId}` + DashboardName: { + "Fn::Join": [ + "", + [ + "base-single-node-mainnet-", + { + "Ref": Match.anyValue() + } + ] + ] + } }) }); }); From d8801d5e8ba03cb7632b617f0effd4d43b96787f Mon Sep 17 00:00:00 2001 From: Danyal Prout Date: Tue, 13 Feb 2024 21:52:46 -0600 Subject: [PATCH 09/75] Remove Goerli / update docs --- lib/base/README.md | 2 +- lib/base/lib/assets/user-data/node.sh | 4 ---- lib/base/sample-configs/.env-sample-rpc | 2 +- 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/lib/base/README.md b/lib/base/README.md index 07a9fad2..3397f6c6 100644 --- a/lib/base/README.md +++ b/lib/base/README.md @@ -74,7 +74,7 @@ Base node needs a URL to a Full Ethereum Node to validate blocks it receives. Yo ### On your Cloud9: Clone this repository and install dependencies ```bash - git clone https://github.com/alickwong/aws-blockchain-node-runners + git clone https://github.com/aws-samples/aws-blockchain-node-runners cd aws-blockchain-node-runners npm install ``` diff --git a/lib/base/lib/assets/user-data/node.sh b/lib/base/lib/assets/user-data/node.sh index daf9a26e..9a7f2749 100644 --- a/lib/base/lib/assets/user-data/node.sh +++ b/lib/base/lib/assets/user-data/node.sh @@ -151,10 +151,6 @@ case $NETWORK_ID in sed -i "s#OP_NODE_L1_ETH_RPC=https://1rpc.io/eth#OP_NODE_L1_ETH_RPC=$L1_ENDPOINT#g" /home/bcuser/node/.env.mainnet sed -i '/.env.mainnet/s/^#//g' /home/bcuser/node/docker-compose.yml ;; - "goerli") - sed -i "s#OP_NODE_L1_ETH_RPC=https://Base-goerli-rpc.allthatnode.com#OP_NODE_L1_ETH_RPC=$L1_ENDPOINT#g" /home/bcuser/node/.env.goerli - sed -i "s/.env.goerli/s/^#//g" /home/bcuser/node/docker-compose.yml - ;; "sepolia") sed -i "s#OP_NODE_L1_ETH_RPC=https://rpc.sepolia.org#OP_NODE_L1_ETH_RPC=$L1_ENDPOINT#g" /home/bcuser/node/.env.sepolia sed -i "s/.env.sepolia/s/^#//g" /home/bcuser/node/docker-compose.yml diff --git a/lib/base/sample-configs/.env-sample-rpc b/lib/base/sample-configs/.env-sample-rpc index fc09f4c9..11c5d445 100644 --- a/lib/base/sample-configs/.env-sample-rpc +++ b/lib/base/sample-configs/.env-sample-rpc @@ -7,7 +7,7 @@ AWS_ACCOUNT_ID="xxxxxxxx" AWS_REGION="us-east-1" # Regions supported by Amazon Managed Blockchain Access Ethereum: https://docs.aws.amazon.com/general/latest/gr/managedblockchain.html#managedblockchain-access ## Common configuration parameters ## -BASE_NETWORK_ID="mainnet" # All options: "mainnet", "goerli" +BASE_NETWORK_ID="mainnet" # All options: "mainnet", "sepolia" BASE_INSTANCE_TYPE="m7g.2xlarge" BASE_CPU_TYPE="ARM_64" # All options: "x86_64", "ARM_64". IMPORTANT: Make sure the CPU type matches the instance type used From 78980724ff11bc1797272e793560850192e6f49d Mon Sep 17 00:00:00 2001 From: Nikolay Vlasov Date: Mon, 4 Mar 2024 17:36:39 +1100 Subject: [PATCH 10/75] Base. Added newly required param for L1 Consensus URL --- lib/base/README.md | 22 +++++++++++---------- lib/base/app.ts | 3 ++- lib/base/lib/assets/user-data/node.sh | 17 ++++++++++------ lib/base/lib/config/baseConfig.interface.ts | 3 ++- lib/base/lib/config/baseConfig.ts | 5 +++-- lib/base/lib/single-node-stack.ts | 21 ++++++++++++-------- lib/base/sample-configs/.env-sample-rpc | 22 +++++++++++---------- lib/base/tsconfig.json | 2 +- 8 files changed, 56 insertions(+), 39 deletions(-) diff --git a/lib/base/README.md b/lib/base/README.md index 3397f6c6..08a0d08c 100644 --- a/lib/base/README.md +++ b/lib/base/README.md @@ -1,6 +1,10 @@ # Sample AWS Blockchain Node Runner app for Base Nodes -[Base](https://base.org/) is a "Layer 2" scaling solution for Ethereum. This blueprint helps to deploy Base RPC nodes on AWS and use [Amazon Managed Blockchain Access Ethereum](https://docs.aws.amazon.com/managed-blockchain/latest/ethereum-dev/ethereum-concepts.html) node for "Layer 1". It is meant to be used for development, testing or Proof of Concept purposes. +[Base](https://base.org/) is a "Layer 2" scaling solution for Ethereum. This blueprint helps to deploy Base RPC nodes on AWS. It is meant to be used for development, testing or Proof of Concept purposes. + +| Contributed by | +|:---------------| +|[@frbrkoala](https://github.com/frbrkoala), [@danyalprout](https://github.com/danyalprout)| ## Overview of Deployment Architectures for Single Node setups @@ -15,6 +19,7 @@ ## Additional materials +
Review the for pros and cons of this solution. ### Well-Architected Checklist @@ -33,7 +38,7 @@ This is the Well-Architected checklist for Ethereum nodes implementation of the | | | Following principle of least privilege access | In the node, root user is not used (using special user "bcuser" instead). | | | Application security | Security focused development practices | cdk-nag is being used with documented suppressions. | | Cost optimization | Service selection | Use cost effective resources | Base nodes works well on ARM architecture and we use Graviton3-powered EC2 instances for better cost effectiveness. | -| | Cost awareness | Estimate costs | One Base node on m7g.2xlarge and 3TiB EBS gp3 volume will cost around US$503.27 per month in the US East (N. Virginia) region. Additional charges will be applied for Ethereum L1 node and might vary between US$200 and US$500 per month. Approximately the total cost will be US$503.27 + US$500 = US$1003.27 per month. | +| | Cost awareness | Estimate costs | One Base node with on-Demand priced m7g.2xlarge and 3TiB EBS gp3 volume will cost around US$599.27 per month in the US East (N. Virginia) region. Additional charges will apply for Ethereum L1 node and will depend on the service used. | | Reliability | Resiliency implementation | Withstand component failures | This solution currently does not have high availability and is deployed to a single availability zone. | | | Data backup | How is data backed up? | The data is not specially backed up. The node will have to re-sync its state from other nodes in the Base network to recover. | | | Resource monitoring | How are workload resources monitored? | Resources are being monitored using Amazon CloudWatch dashboards. Amazon CloudWatch custom metrics are being pushed via CloudWatch Agent. | @@ -54,10 +59,10 @@ This is the Well-Architected checklist for Ethereum nodes implementation of the - Instance type [m7g.2xlarge](https://aws.amazon.com/ec2/instance-types/m7g/). - 2500GB EBS gp3 storage with at least 5000 IOPS. -**Recommended for Base node** +**Recommended for Base node on maiinet** - Instance type [m7g.4xlarge](https://aws.amazon.com/ec2/instance-types/m7g/). -- 2500GB EBS gp3 storage with at least 6000 IOPS.` +- 4100GB EBS gp3 storage with at least 6000 IOPS.`
@@ -69,7 +74,7 @@ We will use AWS Cloud9 to execute the subsequent commands. Follow the instructio ### Make sure you have access to Ethereum L1 node -Base node needs a URL to a Full Ethereum Node to validate blocks it receives. You can run your own with [Ethereum node blueprint](https://aws-samples.github.io/aws-blockchain-node-runners/docs/Blueprints/Ethereum) or use [one of Base partners](https://docs.base.org/tools/node-providers). +Base node needs a URL to a Full Ethereum Node to validate blocks it receives. You can run your own with [Ethereum node blueprint](https://aws-samples.github.io/aws-blockchain-node-runners/docs/Blueprints/Ethereum) or use [one of partners of Base](https://docs.base.org/tools/node-providers). ### On your Cloud9: Clone this repository and install dependencies @@ -98,13 +103,10 @@ Base node needs a URL to a Full Ethereum Node to validate blocks it receives. Yo ```bash # Make sure you are in aws-blockchain-node-runners/lib/base cd lib/base -npm install pwd cp ./sample-configs/.env-sample-rpc .env nano .env ``` - > NOTE: - > Example configuration parameters are set in the local `.env-sample` file. You can find more examples inside `sample-configs` directory. 4. Deploy common components such as IAM role @@ -139,7 +141,7 @@ pwd # Make sure you are in aws-blockchain-node-runners/lib/base npx cdk deploy base-single-node --json --outputs-file single-node-deploy.json ``` -After starting the node you will need to wait for the initial synchronization process to finish.To see the progress, you may use SSM to connect into EC2 first and watch the log like this: +A node connected to Sepolia network should start within an hour, while syncing nodes with Mainnet might take a while. Although you can force the node to use snapshots provided by Base team by setting `BASE_RESTORE_FROM_SNAPSHOT="true"` in `.env` file, you might still need to watch your node ifinish synchronizing. You can watch the progress with CloudWatch dashboard (see [Monitoring](#monitoring)) or check the progress manually. For manual access, use SSM to connect into EC2 first and watch the log like this: ```bash export INSTANCE_ID=$(cat single-node-deploy.json | jq -r '..|.node-instance-id? | select(. != null)') @@ -167,7 +169,7 @@ curl -s -X POST -H "Content-Type: application/json" --data '{"jsonrpc":"2.0","me A script on the Base node publishes current block and blocks behind metrics to CloudWatch metrics every 5 minutes. When the node is fully synced the blocks behind metric should get to 0, which might take about 1.5 days. To see the metrics: - Navigate to CloudWatch service (make sure you are in the region you have specified for AWS_REGION) -- Open Dashboards and select `base-single-node-` from the list of dashboards. +- Open Dashboards and select `base-single-node--` from the list of dashboards. ## From your Cloud9: Clear up and undeploy everything diff --git a/lib/base/app.ts b/lib/base/app.ts index 81529827..eb61a5cd 100644 --- a/lib/base/app.ts +++ b/lib/base/app.ts @@ -22,6 +22,7 @@ new BaseSingleNodeStack(app, "base-single-node", { instanceCpuType: config.baseNodeConfig.instanceCpuType, baseNetworkId: config.baseNodeConfig.baseNetworkId, restoreFromSnapshot: config.baseNodeConfig.restoreFromSnapshot, - l1Endpoint: config.baseNodeConfig.l1Endpoint, + l1ExecutionEndpoint: config.baseNodeConfig.l1ExecutionEndpoint, + l1ConsensusEndpoint: config.baseNodeConfig.l1ConsensusEndpoint, dataVolume: config.baseNodeConfig.dataVolume, }); diff --git a/lib/base/lib/assets/user-data/node.sh b/lib/base/lib/assets/user-data/node.sh index 9a7f2749..4f7d1679 100644 --- a/lib/base/lib/assets/user-data/node.sh +++ b/lib/base/lib/assets/user-data/node.sh @@ -77,15 +77,16 @@ STACK_NAME=${_STACK_NAME_} RESTORE_FROM_SNAPSHOT=${_RESTORE_FROM_SNAPSHOT_} FORMAT_DISK=${_FORMAT_DISK_} NETWORK_ID=${_NETWORK_ID_} -L1_ENDPOINT=${_L1_ENDPOINT_} +L1_EXECUTION_ENDPOINT=${_L1_EXECUTION_ENDPOINT_} +L1_CONSENSUS_ENDPOINT=${_L1_CONSENSUS_ENDPOINT_} echo "REGION=$REGION" >> /etc/environment echo "NETWORK_ID=$NETWORK_ID" >> /etc/environment -echo "L1_ENDPOINT=$L1_ENDPOINT" >> /etc/environment +echo "L1_EXECUTION_ENDPOINT=$L1_EXECUTION_ENDPOINT" >> /etc/environment +echo "L1_CONSENSUS_ENDPOINT=$L1_CONSENSUS_ENDPOINT" >> /etc/environment GIT_URL=https://github.com/base-org/node.git SYNC_CHECKER_FILE_NAME=syncchecker-base.sh -SNAPSHOT_S3_PATH=s3://base-snapshots-$NETWORK_ID-archive.s3.amazonaws.com/$(curl https://base-snapshots-$NETWORK_ID-archive.s3.amazonaws.com/latest) yum -y install docker python3-pip cronie cronie-anacron gcc python3-devel git yum -y remove python-requests @@ -148,12 +149,16 @@ echo "Configuring node" case $NETWORK_ID in "mainnet") - sed -i "s#OP_NODE_L1_ETH_RPC=https://1rpc.io/eth#OP_NODE_L1_ETH_RPC=$L1_ENDPOINT#g" /home/bcuser/node/.env.mainnet + sed -i "s#OP_NODE_L1_ETH_RPC=https://1rpc.io/eth#OP_NODE_L1_ETH_RPC=$L1_EXECUTION_ENDPOINT#g" /home/bcuser/node/.env.mainnet sed -i '/.env.mainnet/s/^#//g' /home/bcuser/node/docker-compose.yml + sed -i '/OP_NODE_L1_BEACON/s/^#//g' /home/bcuser/node/.env.mainnet + sed -i "s#OP_NODE_L1_BEACON=https://your.sepolia.beacon.node/endpoint-here#OP_NODE_L1_BEACON=$L1_CONSENSUS_ENDPOINT#g" /home/bcuser/node/.env.mainnet ;; "sepolia") - sed -i "s#OP_NODE_L1_ETH_RPC=https://rpc.sepolia.org#OP_NODE_L1_ETH_RPC=$L1_ENDPOINT#g" /home/bcuser/node/.env.sepolia - sed -i "s/.env.sepolia/s/^#//g" /home/bcuser/node/docker-compose.yml + sed -i "s#OP_NODE_L1_ETH_RPC=https://rpc.sepolia.org#OP_NODE_L1_ETH_RPC=$L1_EXECUTION_ENDPOINT#g" /home/bcuser/node/.env.sepolia + sed -i "/.env.sepolia/s/^#//g" /home/bcuser/node/docker-compose.yml + sed -i '/OP_NODE_L1_BEACON/s/^#//g' /home/bcuser/node/.env.sepolia + sed -i "s#OP_NODE_L1_BEACON=https://your.sepolia.beacon.node/endpoint-here#OP_NODE_L1_BEACON=$L1_CONSENSUS_ENDPOINT#g" /home/bcuser/node/.env.sepolia ;; *) echo "Network id is not valid." diff --git a/lib/base/lib/config/baseConfig.interface.ts b/lib/base/lib/config/baseConfig.interface.ts index dabce7c9..24539158 100644 --- a/lib/base/lib/config/baseConfig.interface.ts +++ b/lib/base/lib/config/baseConfig.interface.ts @@ -18,5 +18,6 @@ export interface BaseBaseNodeConfig extends configTypes.BaseNodeConfig { baseNetworkId: BaseNetworkId; dataVolume: BaseDataVolumeConfig; restoreFromSnapshot: boolean; - l1Endpoint: string; + l1ExecutionEndpoint: string; + l1ConsensusEndpoint: string; } diff --git a/lib/base/lib/config/baseConfig.ts b/lib/base/lib/config/baseConfig.ts index ae3cf87b..9bfd345e 100644 --- a/lib/base/lib/config/baseConfig.ts +++ b/lib/base/lib/config/baseConfig.ts @@ -24,11 +24,12 @@ export const baseConfig: configTypes.BaseBaseConfig = { } export const baseNodeConfig: configTypes.BaseBaseNodeConfig = { - instanceType: new ec2.InstanceType(process.env.BASE_INSTANCE_TYPE ? process.env.BASE_INSTANCE_TYPE : "m6a.2xlarge"), + instanceType: new ec2.InstanceType(process.env.BASE_INSTANCE_TYPE ? process.env.BASE_INSTANCE_TYPE : "m7g.2xlarge"), instanceCpuType: process.env.BASE_CPU_TYPE?.toLowerCase() == "x86_64" ? ec2.AmazonLinuxCpuType.X86_64 : ec2.AmazonLinuxCpuType.ARM_64, baseNetworkId: process.env.BASE_NETWORK_ID || "mainnet", restoreFromSnapshot: process.env.BASE_RESTORE_FROM_SNAPSHOT?.toLowerCase() == "true" ? true : false, - l1Endpoint: process.env.BASE_L1_ENDPOINT || constants.NoneValue, + l1ExecutionEndpoint: process.env.BASE_L1_EXECUTION_ENDPOINT || constants.NoneValue, + l1ConsensusEndpoint: process.env.BASE_L1_CONSENSUS_ENDPOINT || constants.NoneValue, dataVolume: { sizeGiB: process.env.BASE_DATA_VOL_SIZE ? parseInt(process.env.BASE_DATA_VOL_SIZE): 1000, type: parseDataVolumeType(process.env.BASE_DATA_VOL_TYPE?.toLowerCase() ? process.env.BASE_DATA_VOL_TYPE?.toLowerCase() : "gp3"), diff --git a/lib/base/lib/single-node-stack.ts b/lib/base/lib/single-node-stack.ts index 07854438..b32fa3cc 100644 --- a/lib/base/lib/single-node-stack.ts +++ b/lib/base/lib/single-node-stack.ts @@ -18,7 +18,8 @@ export interface BaseSingleNodeStackProps extends cdk.StackProps { instanceCpuType: ec2.AmazonLinuxCpuType; baseNetworkId: configTypes.BaseNetworkId; restoreFromSnapshot: boolean; - l1Endpoint: string; + l1ExecutionEndpoint: string, + l1ConsensusEndpoint: string, dataVolume: configTypes.BaseDataVolumeConfig; } @@ -39,10 +40,18 @@ export class BaseSingleNodeStack extends cdk.Stack { instanceCpuType, baseNetworkId, restoreFromSnapshot, - l1Endpoint, + l1ExecutionEndpoint, + l1ConsensusEndpoint, dataVolume, } = props; + if (l1ExecutionEndpoint === constants.NoneValue){ + throw new Error("L1 Execution Endpoint cannot be set to None. Set BASE_L1_EXECUTION_ENDPOINT "); + } + if (l1ConsensusEndpoint === constants.NoneValue){ + throw new Error("L1 Consensus Endpoint cannot be set to None. Set BASE_L1_CONSENSUS_ENDPOINT "); + } + // Using default VPC const vpc = ec2.Vpc.fromLookup(this, "vpc", { isDefault: true }); @@ -59,11 +68,6 @@ export class BaseSingleNodeStack extends cdk.Stack { // Getting the IAM role ARN from the common stack const importedInstanceRoleArn = cdk.Fn.importValue("BaseNodeInstanceRoleArn"); - // If user has not supplied the URL for L1, attempting to use AMB node URL - let l1EndpointURL = l1Endpoint; - if (l1EndpointURL === constants.NoneValue){ - l1EndpointURL = cdk.Fn.importValue("BaseAmbEthereumNodeRpcUrlWithBillingToken"); - } const instanceRole = iam.Role.fromRoleArn(this, "iam-role", importedInstanceRoleArn); // Making sure our instance will be able to read the assets @@ -103,7 +107,8 @@ export class BaseSingleNodeStack extends cdk.Stack { _AUTOSCALING_GROUP_NAME_: constants.NoneValue, _RESTORE_FROM_SNAPSHOT_: restoreFromSnapshot.toString(), _FORMAT_DISK_: "true", - _L1_ENDPOINT_: l1EndpointURL, + _L1_EXECUTION_ENDPOINT_: l1ExecutionEndpoint, + _L1_CONSENSUS_ENDPOINT_: l1ConsensusEndpoint, }); node.instance.addUserData(modifiedInitNodeScript); diff --git a/lib/base/sample-configs/.env-sample-rpc b/lib/base/sample-configs/.env-sample-rpc index 11c5d445..de38914b 100644 --- a/lib/base/sample-configs/.env-sample-rpc +++ b/lib/base/sample-configs/.env-sample-rpc @@ -4,23 +4,25 @@ ## Set the AWS account is and region for your environment ## AWS_ACCOUNT_ID="xxxxxxxx" -AWS_REGION="us-east-1" # Regions supported by Amazon Managed Blockchain Access Ethereum: https://docs.aws.amazon.com/general/latest/gr/managedblockchain.html#managedblockchain-access +AWS_REGION="us-east-1" ## Common configuration parameters ## -BASE_NETWORK_ID="mainnet" # All options: "mainnet", "sepolia" +BASE_NETWORK_ID="sepolia" # All options: "mainnet", "sepolia" BASE_INSTANCE_TYPE="m7g.2xlarge" BASE_CPU_TYPE="ARM_64" # All options: "x86_64", "ARM_64". IMPORTANT: Make sure the CPU type matches the instance type used # Data volume configuration BASE_DATA_VOL_TYPE="gp3" # Other options: "io1" | "io2" | "gp3" | "instance-store" . IMPORTANT: Use "instance-store" option only with instance types that support that feature, like popular for node im4gn, d3, i3en, and i4i instance families -BASE_DATA_VOL_SIZE="4100" # Current required data size to keep both snapshot archive and unarchived version of it +BASE_DATA_VOL_SIZE="4100" # Current required data size in GB to keep both snapshot archive and unarchived version of it BASE_DATA_VOL_IOPS="5000" # Max IOPS for EBS volumes (not applicable for "instance-store") BASE_DATA_VOL_THROUGHPUT="700" # Max throughput for EBS gp3 volumes (not applicable for "io1" | "io2" | "instance-store") -BASE_RESTORE_FROM_SNAPSHOT="true" # Download snapshot to speed up statup time -BASE_L1_ENDPOINT="none" # Set your own URL to Ethereum L1 node: https://docs.base.org/tools/node-providers +BASE_RESTORE_FROM_SNAPSHOT="true" # Download snapshot to speed up statup time +BASE_L1_EXECUTION_ENDPOINT="https://ethereum-sepolia-rpc.publicnode.com" # Set your own URL to Ethereum L1 node: https://docs.base.org/tools/node-providers +BASE_L1_CONSENSUS_ENDPOINT=https://ethereum-sepolia-beacon-api.publicnode.com -# Example L1 URLs: -# For Mainnet: -#BASE_L1_ENDPOINT=https://1rpc.io/eth -# For Sepolia: -#BASE_L1_ENDPOINT=https://rpc.sepolia.org \ No newline at end of file +# Example for Sepolia: +#BASE_L1_EXECUTION_ENDPOINT=https://ethereum-sepolia-rpc.publicnode.com +#BASE_L1_CONSENSUS_ENDPOINT=https://ethereum-sepolia-beacon-api.publicnode.com +# Example for Mainnet and with Ethereum Blueprint with Geth-Lighthouse client combination and private IP: +#BASE_L1_EXECUTION_ENDPOINT=http://172.31.15.220:8545 +#BASE_L1_CONSENSUS_ENDPOINT=http://172.31.15.220:5052 \ No newline at end of file diff --git a/lib/base/tsconfig.json b/lib/base/tsconfig.json index aaa7dc51..8e1979f3 100644 --- a/lib/base/tsconfig.json +++ b/lib/base/tsconfig.json @@ -21,7 +21,7 @@ "experimentalDecorators": true, "strictPropertyInitialization": false, "typeRoots": [ - "./node_modules/@types" + "../../node_modules/@types" ] }, "exclude": [ From e7543ccb42cf41d70461204e8e862f323de9fcce Mon Sep 17 00:00:00 2001 From: Nikolay Vlasov Date: Tue, 5 Mar 2024 11:41:46 +1100 Subject: [PATCH 11/75] Base. Corrected unite tests after adding new configuration prams --- lib/base/sample-configs/.env-sample-rpc | 2 +- lib/base/test/.env-test | 18 +++-- lib/base/test/base-common.test.ts | 98 +++++++++++++++++++++++++ lib/base/test/base-single-node.test.ts | 9 ++- 4 files changed, 117 insertions(+), 10 deletions(-) create mode 100644 lib/base/test/base-common.test.ts diff --git a/lib/base/sample-configs/.env-sample-rpc b/lib/base/sample-configs/.env-sample-rpc index de38914b..5e9eb2be 100644 --- a/lib/base/sample-configs/.env-sample-rpc +++ b/lib/base/sample-configs/.env-sample-rpc @@ -13,7 +13,7 @@ BASE_CPU_TYPE="ARM_64" # All options: "x86_64", "ARM_64". IMPORT # Data volume configuration BASE_DATA_VOL_TYPE="gp3" # Other options: "io1" | "io2" | "gp3" | "instance-store" . IMPORTANT: Use "instance-store" option only with instance types that support that feature, like popular for node im4gn, d3, i3en, and i4i instance families -BASE_DATA_VOL_SIZE="4100" # Current required data size in GB to keep both snapshot archive and unarchived version of it +BASE_DATA_VOL_SIZE="5100" # Current required data size in GB to keep both snapshot archive and unarchived version of it. For Sepolia 1000 will be sufficient. BASE_DATA_VOL_IOPS="5000" # Max IOPS for EBS volumes (not applicable for "instance-store") BASE_DATA_VOL_THROUGHPUT="700" # Max throughput for EBS gp3 volumes (not applicable for "io1" | "io2" | "instance-store") BASE_RESTORE_FROM_SNAPSHOT="true" # Download snapshot to speed up statup time diff --git a/lib/base/test/.env-test b/lib/base/test/.env-test index dae5ca49..f9a14ccd 100644 --- a/lib/base/test/.env-test +++ b/lib/base/test/.env-test @@ -3,16 +3,24 @@ ############################################################# ## Set the AWS account is and region for your environment ## -AWS_ACCOUNT_ID="347616198663" +AWS_ACCOUNT_ID="xxxxxxxxxxxx" AWS_REGION="us-east-1" # Regions supported by Amazon Managed Blockchain Access Ethereum: https://docs.aws.amazon.com/general/latest/gr/managedblockchain.html#managedblockchain-access ## Common configuration parameters ## BASE_NETWORK_ID="mainnet" # All options: "mainnet" -BASE_INSTANCE_TYPE="m6a.2xlarge" -BASE_CPU_TYPE="x86_64" # All options: "x86_64", "ARM_64". IMPORTANT: Make sure the CPU type matches the instance type used # Data volume configuration BASE_DATA_VOL_TYPE="gp3" # Other options: "io1" | "io2" | "gp3" | "instance-store" . IMPORTANT: Use "instance-store" option only with instance types that support that feature, like popular for node im4gn, d3, i3en, and i4i instance families -BASE_DATA_VOL_SIZE="1000" # Current required data size to keep both snapshot archive and unarchived version of it -BASE_DATA_VOL_IOPS="3000" # Max IOPS for EBS volumes (not applicable for "instance-store") +BASE_DATA_VOL_SIZE="5100" # Current required data size in GB to keep both snapshot archive and unarchived version of it. For Sepolia 1000 will be sufficient. +BASE_DATA_VOL_IOPS="5000" # Max IOPS for EBS volumes (not applicable for "instance-store") BASE_DATA_VOL_THROUGHPUT="700" # Max throughput for EBS gp3 volumes (not applicable for "io1" | "io2" | "instance-store") +BASE_RESTORE_FROM_SNAPSHOT="true" # Download snapshot to speed up statup time +BASE_L1_EXECUTION_ENDPOINT="https://ethereum-sepolia-rpc.publicnode.com" # Set your own URL to Ethereum L1 node: https://docs.base.org/tools/node-providers +BASE_L1_CONSENSUS_ENDPOINT=https://ethereum-sepolia-beacon-api.publicnode.com + +# Example for Sepolia: +#BASE_L1_EXECUTION_ENDPOINT=https://ethereum-sepolia-rpc.publicnode.com +#BASE_L1_CONSENSUS_ENDPOINT=https://ethereum-sepolia-beacon-api.publicnode.com +# Example for Mainnet and with Ethereum Blueprint with Geth-Lighthouse client combination and private IP: +#BASE_L1_EXECUTION_ENDPOINT=http://172.31.15.220:8545 +#BASE_L1_CONSENSUS_ENDPOINT=http://172.31.15.220:5052 diff --git a/lib/base/test/base-common.test.ts b/lib/base/test/base-common.test.ts new file mode 100644 index 00000000..4b9e657d --- /dev/null +++ b/lib/base/test/base-common.test.ts @@ -0,0 +1,98 @@ +import {Match, Template} from "aws-cdk-lib/assertions"; +import * as cdk from "aws-cdk-lib"; +import * as dotenv from 'dotenv'; + +dotenv.config({path: './test/.env-test'}); +import * as config from "../lib/config/baseConfig"; +import {BaseCommonStack} from "../lib/common-stack"; + +describe("BaseCommonStack", () => { + let app: cdk.App; + let baseCommonStack: BaseCommonStack; + let template: Template; + beforeAll(() => { + app = new cdk.App(); + + // Create the BaseCommonStack. + baseCommonStack = new BaseCommonStack(app, "base-single-node", { + stackName: `base-nodes-common`, + env: { account: config.baseConfig.accountId, region: config.baseConfig.region }, + }); + + template = Template.fromStack(baseCommonStack); + }); + + test("Check Node Instance Role", () => { + // Has EC2 instance security group. + template.hasResourceProperties("AWS::IAM::Role", { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: "sts:AssumeRole", + Effect: "Allow", + Principal: { + Service: "ec2.amazonaws.com" + } + } + ], + Version: "2012-10-17" + }, + ManagedPolicyArns: [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/AmazonSSMManagedInstanceCore" + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/CloudWatchAgentServerPolicy" + ] + ] + } + ] + }); + }); + + test("Check Node Instance Role Policy", () => { + // Has EC2 instance security group. + template.hasResourceProperties("AWS::IAM::Policy", { + PolicyDocument: { + Statement: [ + { + Action: "cloudformation:SignalResource", + Effect: "Allow", + Resource: "*" + }, + { + Action: "autoscaling:CompleteLifecycleAction", + Effect: "Allow", + Resource: "arn:aws:autoscaling:us-east-1:xxxxxxxxxxxx:autoScalingGroup:*:autoScalingGroupName/base-*" + }, + { + Action: "s3:*Object", + Effect: "Allow", + Resource: [ + "arn:aws:s3:::base-snapshots-*-archive", + "arn:aws:s3:::base-snapshots-*-archive/*" + ] + } + ], + "Version": "2012-10-17" + } + }); + }); + +}); diff --git a/lib/base/test/base-single-node.test.ts b/lib/base/test/base-single-node.test.ts index 80c48d47..ffdb7c94 100644 --- a/lib/base/test/base-single-node.test.ts +++ b/lib/base/test/base-single-node.test.ts @@ -22,7 +22,8 @@ describe("BaseSingleNodeStack", () => { instanceCpuType: config.baseNodeConfig.instanceCpuType, baseNetworkId: config.baseNodeConfig.baseNetworkId, restoreFromSnapshot: config.baseNodeConfig.restoreFromSnapshot, - l1Endpoint: config.baseNodeConfig.l1Endpoint, + l1ExecutionEndpoint: config.baseNodeConfig.l1ExecutionEndpoint, + l1ConsensusEndpoint: config.baseNodeConfig.l1ConsensusEndpoint, dataVolume: config.baseNodeConfig.dataVolume, }); @@ -87,7 +88,7 @@ describe("BaseSingleNodeStack", () => { ], IamInstanceProfile: Match.anyValue(), ImageId: Match.anyValue(), - InstanceType: "m6a.2xlarge", + InstanceType: "m7g.2xlarge", Monitoring: true, PropagateTagsToVolumeOnCreation: true, SecurityGroupIds: Match.anyValue(), @@ -98,9 +99,9 @@ describe("BaseSingleNodeStack", () => { template.hasResourceProperties("AWS::EC2::Volume", { AvailabilityZone: Match.anyValue(), Encrypted: true, - Iops: 3000, + Iops: 5000, MultiAttachEnabled: false, - Size: 1000, + Size: 5100, Throughput: 700, VolumeType: "gp3" }) From e1ee12a42ceede513566b1f6d289fcde93ee22b2 Mon Sep 17 00:00:00 2001 From: Nikolay Vlasov Date: Mon, 25 Mar 2024 12:43:18 +1100 Subject: [PATCH 12/75] Base. WIP. Modifications after testing --- lib/base/README.md | 12 +- lib/base/lib/assets/user-data/node.sh | 4 +- lib/base/package-lock.json | 641 ------------------------ lib/base/sample-configs/.env-sample-rpc | 2 +- 4 files changed, 7 insertions(+), 652 deletions(-) delete mode 100644 lib/base/package-lock.json diff --git a/lib/base/README.md b/lib/base/README.md index 08a0d08c..490d6f0d 100644 --- a/lib/base/README.md +++ b/lib/base/README.md @@ -124,14 +124,12 @@ npx cdk deploy base-common ### From your Cloud9: Deploy Single Node -1. For L1 node you you can set your own URL in `BASE_L1_ENDPOINT` property of `.env` file. It can be one of [the providers recommended by Base](https://docs.base.org/tools/node-providers) or run your own Ethereum node [with Node Runner blueprint](https://aws-samples.github.io/aws-blockchain-node-runners/docs/Blueprints/Ethereum). For example: +1. For L1 node you you can set your own URLs in `BASE_L1_EXECUTION_ENDPOINT` and `BASE_L1_CONSENSUS_ENDPOINT` properties of `.env` file. It can be one of [the providers recommended by Base](https://docs.base.org/tools/node-providers) or you can run your own Ethereum node [with Node Runner blueprint](https://aws-samples.github.io/aws-blockchain-node-runners/docs/Blueprints/Ethereum). For example: ```bash -#For Mainnet: -BASE_L1_ENDPOINT=https://1rpc.io/eth - #For Sepolia: -BASE_L1_ENDPOINT=https://rpc.sepolia.org +BASE_L1_EXECUTION_ENDPOINT="https://ethereum-sepolia-rpc.publicnode.com" +BASE_L1_CONSENSUS_ENDPOINT="https://ethereum-sepolia-beacon-api.publicnode.com" ``` 2. Deploy Base RPC Node and wait for it to sync. For Mainnet it might take less than an hour when using snapshots (default) or multiple days if syncing from block 0. @@ -141,7 +139,7 @@ pwd # Make sure you are in aws-blockchain-node-runners/lib/base npx cdk deploy base-single-node --json --outputs-file single-node-deploy.json ``` -A node connected to Sepolia network should start within an hour, while syncing nodes with Mainnet might take a while. Although you can force the node to use snapshots provided by Base team by setting `BASE_RESTORE_FROM_SNAPSHOT="true"` in `.env` file, you might still need to watch your node ifinish synchronizing. You can watch the progress with CloudWatch dashboard (see [Monitoring](#monitoring)) or check the progress manually. For manual access, use SSM to connect into EC2 first and watch the log like this: +A node connected to Sepolia network should start within an hour, while syncing nodes with Mainnet might take a while. Although you can use snapshots provided by the Base team by setting `BASE_RESTORE_FROM_SNAPSHOT="true"` in `.env` file, you might still have wait for your node to finish synchronizing. You can watch the progress with CloudWatch dashboard (see [Monitoring](#monitoring)) or check the progress manually. For manual access, use SSM to connect into EC2 first and watch the log like this: ```bash export INSTANCE_ID=$(cat single-node-deploy.json | jq -r '..|.node-instance-id? | select(. != null)') @@ -154,7 +152,7 @@ curl -d '{"id":0,"jsonrpc":"2.0","method":"optimism_syncStatus"}' \ jq -r .result.unsafe_l2.timestamp))/60)) minutes ``` -3. Test Base RPC API [TODO: Is there an address we can query balance from?] +3. Test Base RPC API Use curl to query from within the node instance: ```bash export INSTANCE_ID=$(cat single-node-deploy.json | jq -r '..|.node-instance-id? | select(. != null)') diff --git a/lib/base/lib/assets/user-data/node.sh b/lib/base/lib/assets/user-data/node.sh index 4f7d1679..c26bff4f 100644 --- a/lib/base/lib/assets/user-data/node.sh +++ b/lib/base/lib/assets/user-data/node.sh @@ -132,9 +132,7 @@ groupadd -g 1002 bcuser useradd -u 1002 -g 1002 -m -s /bin/bash bcuser usermod -a -G docker bcuser usermod -a -G docker ec2-user -chown -R bcuser:bcuser /secrets chmod -R 755 /home/bcuser -chmod -R 755 /secrets echo "Starting docker" service docker start @@ -152,7 +150,7 @@ case $NETWORK_ID in sed -i "s#OP_NODE_L1_ETH_RPC=https://1rpc.io/eth#OP_NODE_L1_ETH_RPC=$L1_EXECUTION_ENDPOINT#g" /home/bcuser/node/.env.mainnet sed -i '/.env.mainnet/s/^#//g' /home/bcuser/node/docker-compose.yml sed -i '/OP_NODE_L1_BEACON/s/^#//g' /home/bcuser/node/.env.mainnet - sed -i "s#OP_NODE_L1_BEACON=https://your.sepolia.beacon.node/endpoint-here#OP_NODE_L1_BEACON=$L1_CONSENSUS_ENDPOINT#g" /home/bcuser/node/.env.mainnet + sed -i "s#OP_NODE_L1_BEACON=https://your.mainet.beacon.node/endpoint-here#OP_NODE_L1_BEACON=$L1_CONSENSUS_ENDPOINT#g" /home/bcuser/node/.env.mainnet ;; "sepolia") sed -i "s#OP_NODE_L1_ETH_RPC=https://rpc.sepolia.org#OP_NODE_L1_ETH_RPC=$L1_EXECUTION_ENDPOINT#g" /home/bcuser/node/.env.sepolia diff --git a/lib/base/package-lock.json b/lib/base/package-lock.json deleted file mode 100644 index 5d2c6444..00000000 --- a/lib/base/package-lock.json +++ /dev/null @@ -1,641 +0,0 @@ -{ - "name": "aws-blockchain-node-runners-base", - "version": "0.1.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "aws-blockchain-node-runners-base", - "version": "0.1.0", - "dependencies": { - "@types/node": "^20.10.0" - }, - "devDependencies": { - "@types/jest": "^29.5.11" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", - "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", - "dev": true, - "dependencies": { - "@babel/highlight": "^7.23.4", - "chalk": "^2.4.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/code-frame/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/code-frame/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/code-frame/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@babel/code-frame/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "node_modules/@babel/code-frame/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@babel/code-frame/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/code-frame/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", - "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", - "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", - "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.22.20", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@babel/highlight/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "node_modules/@babel/highlight/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@babel/highlight/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@jest/expect-utils": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", - "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", - "dev": true, - "dependencies": { - "jest-get-type": "^29.6.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/schemas": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", - "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "dev": true, - "dependencies": { - "@sinclair/typebox": "^0.27.8" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/types": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", - "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", - "dev": true, - "dependencies": { - "@jest/schemas": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@sinclair/typebox": { - "version": "0.27.8", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", - "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", - "dev": true - }, - "node_modules/@types/istanbul-lib-coverage": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", - "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", - "dev": true - }, - "node_modules/@types/istanbul-lib-report": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", - "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", - "dev": true, - "dependencies": { - "@types/istanbul-lib-coverage": "*" - } - }, - "node_modules/@types/istanbul-reports": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", - "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", - "dev": true, - "dependencies": { - "@types/istanbul-lib-report": "*" - } - }, - "node_modules/@types/jest": { - "version": "29.5.11", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.11.tgz", - "integrity": "sha512-S2mHmYIVe13vrm6q4kN6fLYYAka15ALQki/vgDC3mIukEOx8WJlv0kQPM+d4w8Gp6u0uSdKND04IlTXBv0rwnQ==", - "dev": true, - "dependencies": { - "expect": "^29.0.0", - "pretty-format": "^29.0.0" - } - }, - "node_modules/@types/node": { - "version": "20.10.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.5.tgz", - "integrity": "sha512-nNPsNE65wjMxEKI93yOP+NPGGBJz/PoN3kZsVLee0XMiJolxSekEVD8wRwBUBqkwc7UWop0edW50yrCQW4CyRw==", - "dependencies": { - "undici-types": "~5.26.4" - } - }, - "node_modules/@types/stack-utils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", - "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", - "dev": true - }, - "node_modules/@types/yargs": { - "version": "17.0.32", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", - "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", - "dev": true, - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/@types/yargs-parser": { - "version": "21.0.3", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", - "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", - "dev": true - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "dependencies": { - "fill-range": "^7.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/ci-info": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", - "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "engines": { - "node": ">=8" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/diff-sequences": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", - "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", - "dev": true, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/escape-string-regexp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/expect": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", - "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", - "dev": true, - "dependencies": { - "@jest/expect-utils": "^29.7.0", - "jest-get-type": "^29.6.3", - "jest-matcher-utils": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/jest-diff": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", - "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", - "dev": true, - "dependencies": { - "chalk": "^4.0.0", - "diff-sequences": "^29.6.3", - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-get-type": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", - "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", - "dev": true, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-matcher-utils": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", - "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", - "dev": true, - "dependencies": { - "chalk": "^4.0.0", - "jest-diff": "^29.7.0", - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-message-util": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", - "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.12.13", - "@jest/types": "^29.6.3", - "@types/stack-utils": "^2.0.0", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "micromatch": "^4.0.4", - "pretty-format": "^29.7.0", - "slash": "^3.0.0", - "stack-utils": "^2.0.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-util": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", - "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", - "dev": true, - "dependencies": { - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "graceful-fs": "^4.2.9", - "picomatch": "^2.2.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true - }, - "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", - "dev": true, - "dependencies": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, - "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/pretty-format/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/react-is": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", - "dev": true - }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/stack-utils": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", - "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", - "dev": true, - "dependencies": { - "escape-string-regexp": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" - } - } -} diff --git a/lib/base/sample-configs/.env-sample-rpc b/lib/base/sample-configs/.env-sample-rpc index 5e9eb2be..e0960664 100644 --- a/lib/base/sample-configs/.env-sample-rpc +++ b/lib/base/sample-configs/.env-sample-rpc @@ -18,7 +18,7 @@ BASE_DATA_VOL_IOPS="5000" # Max IOPS for EBS volumes (not applicabl BASE_DATA_VOL_THROUGHPUT="700" # Max throughput for EBS gp3 volumes (not applicable for "io1" | "io2" | "instance-store") BASE_RESTORE_FROM_SNAPSHOT="true" # Download snapshot to speed up statup time BASE_L1_EXECUTION_ENDPOINT="https://ethereum-sepolia-rpc.publicnode.com" # Set your own URL to Ethereum L1 node: https://docs.base.org/tools/node-providers -BASE_L1_CONSENSUS_ENDPOINT=https://ethereum-sepolia-beacon-api.publicnode.com +BASE_L1_CONSENSUS_ENDPOINT="https://ethereum-sepolia-beacon-api.publicnode.com" # Example for Sepolia: #BASE_L1_EXECUTION_ENDPOINT=https://ethereum-sepolia-rpc.publicnode.com From a3462d93f4e28b1651b7c15cc385900cf52bfe19 Mon Sep 17 00:00:00 2001 From: Nikolay Vlasov Date: Thu, 28 Mar 2024 17:00:02 +1100 Subject: [PATCH 13/75] Base Bug fixes in mainnet configuration after testing --- lib/base/README.md | 4 ++-- lib/base/lib/assets/node-cw-dashboard.ts | 4 ++-- .../assets/sync-checker/syncchecker-base.sh | 23 +++++++++---------- lib/base/lib/assets/user-data/node.sh | 2 +- 4 files changed, 16 insertions(+), 17 deletions(-) diff --git a/lib/base/README.md b/lib/base/README.md index 490d6f0d..3a083a3b 100644 --- a/lib/base/README.md +++ b/lib/base/README.md @@ -1,11 +1,11 @@ # Sample AWS Blockchain Node Runner app for Base Nodes -[Base](https://base.org/) is a "Layer 2" scaling solution for Ethereum. This blueprint helps to deploy Base RPC nodes on AWS. It is meant to be used for development, testing or Proof of Concept purposes. - | Contributed by | |:---------------| |[@frbrkoala](https://github.com/frbrkoala), [@danyalprout](https://github.com/danyalprout)| +[Base](https://base.org/) is a "Layer 2" scaling solution for Ethereum. This blueprint helps to deploy Base RPC nodes on AWS. It is meant to be used for development, testing or Proof of Concept purposes. + ## Overview of Deployment Architectures for Single Node setups ### Single node setup diff --git a/lib/base/lib/assets/node-cw-dashboard.ts b/lib/base/lib/assets/node-cw-dashboard.ts index 2d2c926e..7dbabdad 100644 --- a/lib/base/lib/assets/node-cw-dashboard.ts +++ b/lib/base/lib/assets/node-cw-dashboard.ts @@ -159,9 +159,9 @@ export const SyncNodeCWDashboardJSON = { "stat": "Maximum", "period": 60, "metrics": [ - [ "CWAgent", "l2_blocks_behind", "InstanceId", "${INSTANCE_ID}", { "label": "${INSTANCE_ID}-${INSTANCE_NAME}" } ] + [ "CWAgent", "l2_minutes_behind", "InstanceId", "${INSTANCE_ID}", { "label": "minutes" } ] ], - "title": "L2 Blocks Behind" + "title": "L2 Minutes Behind" } }, { diff --git a/lib/base/lib/assets/sync-checker/syncchecker-base.sh b/lib/base/lib/assets/sync-checker/syncchecker-base.sh index fee8e2b9..b7c4aff4 100644 --- a/lib/base/lib/assets/sync-checker/syncchecker-base.sh +++ b/lib/base/lib/assets/sync-checker/syncchecker-base.sh @@ -14,22 +14,21 @@ else fi # L2 client stats -L2_CLIENT_HEAD=$(echo $OPTIMISM_SYNC_STATUS | jq -r ".queued_unsafe_l2.number") L2_CLIENT_CURRENT=$(echo $OPTIMISM_SYNC_STATUS | jq -r ".unsafe_l2.number") +L2_CLIENT_CURRENT_BLOCK_TIMESTAMP=$(echo $OPTIMISM_SYNC_STATUS | jq -r ".unsafe_l2.timestamp") +L2_CLIENT_CURRENT_BLOCK_MINUTES_BEHIND="$((($(date +%s) - L2_CLIENT_CURRENT_BLOCK_TIMESTAMP)/60))" -if [ "$L2_CLIENT_HEAD" == "null" ]; then - L2_CLIENT_BLOCKS_BEHIND=0 -else - L2_CLIENT_BLOCKS_BEHIND="$((L2_CLIENT_HEAD-L2_CLIENT_CURRENT))" +if [ "$L2_CLIENT_CURRENT" == "null" ]; then + L2_CLIENT_CURRENT=0 fi -echo "L1_CLIENT_HEAD="$L1_CLIENT_HEAD -echo "L1_CLIENT_CURRENT="$L1_CLIENT_CURRENT -echo "L1_CLIENT_BLOCKS_BEHIND="$L1_CLIENT_BLOCKS_BEHIND +# echo "L1_CLIENT_HEAD="$L1_CLIENT_HEAD +# echo "L1_CLIENT_CURRENT="$L1_CLIENT_CURRENT +# echo "L1_CLIENT_BLOCKS_BEHIND="$L1_CLIENT_BLOCKS_BEHIND -echo "L2_CLIENT_HEAD="$L2_CLIENT_HEAD -echo "L2_CLIENT_CURRENT="$L2_CLIENT_CURRENT -echo "L2_CLIENT_BLOCKS_BEHIND="$L2_CLIENT_BLOCKS_BEHIND +# echo "L2_CLIENT_CURRENT="$L2_CLIENT_CURRENT +# echo "L2_CLIENT_CURRENT_BLOCK_TIMESTAMP="$L2_CLIENT_CURRENT_BLOCK_TIMESTAMP +# echo "L2_CLIENT_CURRENT_BLOCK_MINUTES_BEHIND="$L2_CLIENT_CURRENT_BLOCK_MINUTES_BEHIND # Sending data to CloudWatch TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600") @@ -41,5 +40,5 @@ aws cloudwatch put-metric-data --metric-name l1_current_block --namespace CWAgen aws cloudwatch put-metric-data --metric-name l1_blocks_behind --namespace CWAgent --value $L1_CLIENT_BLOCKS_BEHIND --timestamp $TIMESTAMP --dimensions InstanceId=$INSTANCE_ID --region $REGION aws cloudwatch put-metric-data --metric-name l2_current_block --namespace CWAgent --value $L2_CLIENT_CURRENT --timestamp $TIMESTAMP --dimensions InstanceId=$INSTANCE_ID --region $REGION -aws cloudwatch put-metric-data --metric-name l2_blocks_behind --namespace CWAgent --value $L2_CLIENT_BLOCKS_BEHIND --timestamp $TIMESTAMP --dimensions InstanceId=$INSTANCE_ID --region $REGION +aws cloudwatch put-metric-data --metric-name l2_minutes_behind --namespace CWAgent --value $L2_CLIENT_CURRENT_BLOCK_MINUTES_BEHIND --timestamp $TIMESTAMP --dimensions InstanceId=$INSTANCE_ID --region $REGION diff --git a/lib/base/lib/assets/user-data/node.sh b/lib/base/lib/assets/user-data/node.sh index c26bff4f..7086a1e4 100644 --- a/lib/base/lib/assets/user-data/node.sh +++ b/lib/base/lib/assets/user-data/node.sh @@ -150,7 +150,7 @@ case $NETWORK_ID in sed -i "s#OP_NODE_L1_ETH_RPC=https://1rpc.io/eth#OP_NODE_L1_ETH_RPC=$L1_EXECUTION_ENDPOINT#g" /home/bcuser/node/.env.mainnet sed -i '/.env.mainnet/s/^#//g' /home/bcuser/node/docker-compose.yml sed -i '/OP_NODE_L1_BEACON/s/^#//g' /home/bcuser/node/.env.mainnet - sed -i "s#OP_NODE_L1_BEACON=https://your.mainet.beacon.node/endpoint-here#OP_NODE_L1_BEACON=$L1_CONSENSUS_ENDPOINT#g" /home/bcuser/node/.env.mainnet + sed -i "s#OP_NODE_L1_BEACON=https://your.mainnet.beacon.node/endpoint-here#OP_NODE_L1_BEACON=$L1_CONSENSUS_ENDPOINT#g" /home/bcuser/node/.env.mainnet ;; "sepolia") sed -i "s#OP_NODE_L1_ETH_RPC=https://rpc.sepolia.org#OP_NODE_L1_ETH_RPC=$L1_EXECUTION_ENDPOINT#g" /home/bcuser/node/.env.sepolia From 206ee3ae1236613acdb038fd600711fcb4fc164c Mon Sep 17 00:00:00 2001 From: Nikolay Vlasov Date: Mon, 8 Apr 2024 14:29:40 +1000 Subject: [PATCH 14/75] Base. Updates after e2e test --- lib/base/README.md | 26 +++++++++---------- .../constructs/base-node-security-group.ts | 5 ++++ 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/lib/base/README.md b/lib/base/README.md index 3a083a3b..3cc8713c 100644 --- a/lib/base/README.md +++ b/lib/base/README.md @@ -54,15 +54,15 @@ This is the Well-Architected checklist for Ethereum nodes implementation of the ## Hardware Requirements -**Minimum for Base node** +**Minimum for Base node sepolia** - Instance type [m7g.2xlarge](https://aws.amazon.com/ec2/instance-types/m7g/). -- 2500GB EBS gp3 storage with at least 5000 IOPS. +- 1500GB EBS gp3 storage with at least 5000 IOPS. -**Recommended for Base node on maiinet** +**Recommended for Base node on mainnet** -- Instance type [m7g.4xlarge](https://aws.amazon.com/ec2/instance-types/m7g/). -- 4100GB EBS gp3 storage with at least 6000 IOPS.` +- Instance type [m7g.2xlarge](https://aws.amazon.com/ec2/instance-types/m7g/). +- 4100GB EBS gp3 storage with at least 5000 IOPS.` @@ -124,7 +124,7 @@ npx cdk deploy base-common ### From your Cloud9: Deploy Single Node -1. For L1 node you you can set your own URLs in `BASE_L1_EXECUTION_ENDPOINT` and `BASE_L1_CONSENSUS_ENDPOINT` properties of `.env` file. It can be one of [the providers recommended by Base](https://docs.base.org/tools/node-providers) or you can run your own Ethereum node [with Node Runner blueprint](https://aws-samples.github.io/aws-blockchain-node-runners/docs/Blueprints/Ethereum). For example: +1. For L1 node you you can set your own URLs in `BASE_L1_EXECUTION_ENDPOINT` and `BASE_L1_CONSENSUS_ENDPOINT` properties of `.env` file. It can be one of [the providers recommended by Base](https://docs.base.org/tools/node-providers) or you can run your own Ethereum node [with Node Runner Ethereum blueprint](https://aws-samples.github.io/aws-blockchain-node-runners/docs/Blueprints/Ethereum) (tested with geth-lighthouse combination). For example: ```bash #For Sepolia: @@ -132,14 +132,14 @@ BASE_L1_EXECUTION_ENDPOINT="https://ethereum-sepolia-rpc.publicnode.com" BASE_L1_CONSENSUS_ENDPOINT="https://ethereum-sepolia-beacon-api.publicnode.com" ``` -2. Deploy Base RPC Node and wait for it to sync. For Mainnet it might take less than an hour when using snapshots (default) or multiple days if syncing from block 0. +2. Deploy Base RPC Node and wait for it to sync. For Mainnet it might a day when using snapshots or about a week if syncing from block 0. You can use snapshots provided by the Base team by setting `BASE_RESTORE_FROM_SNAPSHOT="true"` in `.env` file. ```bash pwd # Make sure you are in aws-blockchain-node-runners/lib/base npx cdk deploy base-single-node --json --outputs-file single-node-deploy.json ``` -A node connected to Sepolia network should start within an hour, while syncing nodes with Mainnet might take a while. Although you can use snapshots provided by the Base team by setting `BASE_RESTORE_FROM_SNAPSHOT="true"` in `.env` file, you might still have wait for your node to finish synchronizing. You can watch the progress with CloudWatch dashboard (see [Monitoring](#monitoring)) or check the progress manually. For manual access, use SSM to connect into EC2 first and watch the log like this: +After deployment you can watch the progress with CloudWatch dashboard (see [Monitoring](#monitoring)) or check the progress manually. For manual access, use SSM to connect into EC2 first and watch the log like this: ```bash export INSTANCE_ID=$(cat single-node-deploy.json | jq -r '..|.node-instance-id? | select(. != null)') @@ -164,7 +164,7 @@ curl -s -X POST -H "Content-Type: application/json" --data '{"jsonrpc":"2.0","me ``` ### Monitoring -A script on the Base node publishes current block and blocks behind metrics to CloudWatch metrics every 5 minutes. When the node is fully synced the blocks behind metric should get to 0, which might take about 1.5 days. To see the metrics: +Every 5 minutes a script on the Base node publishes to CloudWatch service the metrics for current block for L1/L2 clients as well as blocks behind metric for L1 and minutes buehind for L2. When the node is fully synced the blocks behind metric should get to 4 and minutes behind should get down to 0. To see the metrics: - Navigate to CloudWatch service (make sure you are in the region you have specified for AWS_REGION) - Open Dashboards and select `base-single-node--` from the list of dashboards. @@ -194,13 +194,13 @@ npx cdk destroy base-common 1. How to check the logs of the clients running on my Base node? - **Note:** In this tutorial we chose not to use SSH and use Session Manager instead. That allows you to log all sessions in AWS CloudTrail to see who logged into the server and when. If you receive an error similar to `SessionManagerPlugin is not found`, [install Session Manager plugin for AWS CLI](https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-working-with-install-plugin.html) + **Note:** In this tutorial we chose not to use SSH and use Session Manager instead. That allows you to log all sessions in AWS CloudTrail to see who logged into the server and when. If you receive an error saying `SessionManagerPlugin is not found`, [install Session Manager plugin for AWS CLI](https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-working-with-install-plugin.html) ```bash pwd # Make sure you are in aws-blockchain-node-runners/lib/base -export INSTANCE_ID=$(cat single-node-deploy.json | jq -r '..|.node-instance-id? | select(. != null)') +export INSTANCE_ID=$(cat single-node-deploy.json | jq -r '..|.nodeinstanceid? | select(. != null)') echo "INSTANCE_ID=" $INSTANCE_ID export AWS_REGION=us-east-1 aws ssm start-session --target $INSTANCE_ID --region $AWS_REGION @@ -216,7 +216,7 @@ docker logs --tail 50 node_node_1 -f pwd # Make sure you are in aws-blockchain-node-runners/lib/base -export INSTANCE_ID=$(cat single-node-deploy.json | jq -r '..|.node-instance-id? | select(. != null)') +export INSTANCE_ID=$(cat single-node-deploy.json | jq -r '..|.nodeinstanceid? | select(. != null)') echo "INSTANCE_ID=" $INSTANCE_ID export AWS_REGION=us-east-1 aws ssm start-session --target $INSTANCE_ID --region $AWS_REGION @@ -226,7 +226,7 @@ sudo cat /var/log/cloud-init-output.log 3. How can I restart the Base node? ``` bash -export INSTANCE_ID=$(cat single-node-deploy.json | jq -r '..|.node-instance-id? | select(. != null)') +export INSTANCE_ID=$(cat single-node-deploy.json | jq -r '..|.nodeinstanceid? | select(. != null)') echo "INSTANCE_ID=" $INSTANCE_ID export AWS_REGION=us-east-1 aws ssm start-session --target $INSTANCE_ID --region $AWS_REGION diff --git a/lib/base/lib/constructs/base-node-security-group.ts b/lib/base/lib/constructs/base-node-security-group.ts index 41e43bb1..cbe14aec 100644 --- a/lib/base/lib/constructs/base-node-security-group.ts +++ b/lib/base/lib/constructs/base-node-security-group.ts @@ -29,6 +29,11 @@ export interface BaseNodeSecurityGroupConstructProps { // Private port sg.addIngressRule(ec2.Peer.ipv4(vpc.vpcCidrBlock), ec2.Port.tcp(8545), "Base Client RPC"); + sg.addEgressRule(ec2.Peer.anyIpv4(), ec2.Port.tcpRange(0, 12999), "All outbound connections except 13000"); + sg.addEgressRule(ec2.Peer.anyIpv4(), ec2.Port.tcpRange(13001, 65535), "All outbound connections except 13000"); + sg.addEgressRule(ec2.Peer.anyIpv4(), ec2.Port.udpRange(0, 12999), "All outbound connections except 13000"); + sg.addEgressRule(ec2.Peer.anyIpv4(), ec2.Port.udpRange(13001, 65535), "All outbound connections except 13000"); + this.securityGroup = sg } } From 93b911546326bab9ab544c1b925fcf56dbbbd55a Mon Sep 17 00:00:00 2001 From: Nikolay Vlasov Date: Mon, 8 Apr 2024 14:40:53 +1000 Subject: [PATCH 15/75] Base. Fixed tests after security group changes --- .../constructs/base-node-security-group.ts | 2 +- lib/base/test/base-single-node.test.ts | 29 +++++++++++++++++-- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/lib/base/lib/constructs/base-node-security-group.ts b/lib/base/lib/constructs/base-node-security-group.ts index cbe14aec..ac71fccb 100644 --- a/lib/base/lib/constructs/base-node-security-group.ts +++ b/lib/base/lib/constructs/base-node-security-group.ts @@ -19,7 +19,7 @@ export interface BaseNodeSecurityGroupConstructProps { const sg = new ec2.SecurityGroup(this, `rpc-node-security-group`, { vpc, description: "Security Group for Blockchain nodes", - allowAllOutbound: true, + allowAllOutbound: false, }); // Public ports diff --git a/lib/base/test/base-single-node.test.ts b/lib/base/test/base-single-node.test.ts index ffdb7c94..e945f320 100644 --- a/lib/base/test/base-single-node.test.ts +++ b/lib/base/test/base-single-node.test.ts @@ -38,9 +38,32 @@ describe("BaseSingleNodeStack", () => { SecurityGroupEgress: [ { "CidrIp": "0.0.0.0/0", - "Description": "Allow all outbound traffic by default", - "IpProtocol": "-1" - } + "Description": "All outbound connections except 13000", + "FromPort": 0, + "IpProtocol": "tcp", + "ToPort": 12999 + }, + { + "CidrIp": "0.0.0.0/0", + "Description": "All outbound connections except 13000", + "FromPort": 13001, + "IpProtocol": "tcp", + "ToPort": 65535 + }, + { + "CidrIp": "0.0.0.0/0", + "Description": "All outbound connections except 13000", + "FromPort": 0, + "IpProtocol": "udp", + "ToPort": 12999 + }, + { + "CidrIp": "0.0.0.0/0", + "Description": "All outbound connections except 13000", + "FromPort": 13001, + "IpProtocol": "udp", + "ToPort": 65535 + } ], SecurityGroupIngress: [ { From e2ef039645481cc64a8b87d135c0ff3400aa4a7e Mon Sep 17 00:00:00 2001 From: Nikolay Vlasov Date: Mon, 8 Apr 2024 14:45:23 +1000 Subject: [PATCH 16/75] Base. Added README to the website --- website/docs/Blueprints/Base.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 website/docs/Blueprints/Base.md diff --git a/website/docs/Blueprints/Base.md b/website/docs/Blueprints/Base.md new file mode 100644 index 00000000..a7999de3 --- /dev/null +++ b/website/docs/Blueprints/Base.md @@ -0,0 +1,9 @@ +--- +sidebar_position: 6 +sidebar_label: Base +--- +# + +import Readme from '../../../lib/Base/README.md'; + + From 14ee007f759c7adfc888a6e82faa48979fb998b0 Mon Sep 17 00:00:00 2001 From: Nikolay Vlasov Date: Mon, 8 Apr 2024 15:04:03 +1000 Subject: [PATCH 17/75] Base. Updates after running code scanning tools --- .pre-commit-config.yaml | 4 ++++ docs/pre-merge-tools.md | 2 +- lib/base/README.md | 2 +- lib/base/lib/assets/restore-from-snapshot.sh | 2 +- lib/base/lib/assets/sync-checker/syncchecker-base.sh | 5 ++--- lib/base/sample-configs/.env-sample-rpc | 4 ++-- 6 files changed, 11 insertions(+), 8 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e64ba280..cf3c4123 100755 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,3 +11,7 @@ repos: - id: detect-aws-credentials args: ['--allow-missing-credentials'] - id: forbid-submodules + - repo: https://github.com/iamthefij/docker-pre-commit + rev: master + hooks: + - id: docker-compose-check diff --git a/docs/pre-merge-tools.md b/docs/pre-merge-tools.md index a154ea8c..a15cbc1e 100644 --- a/docs/pre-merge-tools.md +++ b/docs/pre-merge-tools.md @@ -42,4 +42,4 @@ npm run install-pre-commit-mac npm run run-pre-commit ``` -4. Optionally, run [shellcheck](https://github.com/koalaman/shellcheck) to check for common problems in your shell scripts. \ No newline at end of file +4. Optionally, run [shellcheck](https://github.com/koalaman/shellcheck) to check for common problems in your shell scripts. diff --git a/lib/base/README.md b/lib/base/README.md index 3cc8713c..7e9161a0 100644 --- a/lib/base/README.md +++ b/lib/base/README.md @@ -14,7 +14,7 @@ 1. A Base node deployed in the [Default VPC](https://docs.aws.amazon.com/vpc/latest/userguide/default-vpc.html) continuously synchronizes with the rest of nodes on Base blockchain network through [Internet Gateway](https://docs.aws.amazon.com/vpc/latest/userguide/VPC_Internet_Gateway.html). 2. The Base node is used by dApps or development tools internally from within the Default VPC. JSON RPC API is not exposed to the Internet directly to protect nodes from unauthorized access. -3. Your Base node needs access to a fully-synced [Ethereum Mainnet or Sepolia RPC endpoint](https://docs.base.org/tools/node-providers) . +3. Your Base node needs access to a fully-synced [Ethereum Mainnet or Sepolia RPC endpoint](https://docs.base.org/tools/node-providers) . 4. The Base node sends various monitoring metrics for both EC2 and Base nodes to Amazon CloudWatch. ## Additional materials diff --git a/lib/base/lib/assets/restore-from-snapshot.sh b/lib/base/lib/assets/restore-from-snapshot.sh index 0d9b14be..488826f1 100644 --- a/lib/base/lib/assets/restore-from-snapshot.sh +++ b/lib/base/lib/assets/restore-from-snapshot.sh @@ -18,4 +18,4 @@ chown -R bcuser:bcuser /data && \ echo "Sync finished at " $(date) && \ echo "$(($SECONDS / 60)) minutes and $(($SECONDS % 60)) seconds elapsed." && \ sudo su bcuser && \ -/usr/local/bin/docker-compose -f /home/bcuser/node/docker-compose.yml up -d \ No newline at end of file +/usr/local/bin/docker-compose -f /home/bcuser/node/docker-compose.yml up -d diff --git a/lib/base/lib/assets/sync-checker/syncchecker-base.sh b/lib/base/lib/assets/sync-checker/syncchecker-base.sh index b7c4aff4..6be5ca59 100644 --- a/lib/base/lib/assets/sync-checker/syncchecker-base.sh +++ b/lib/base/lib/assets/sync-checker/syncchecker-base.sh @@ -7,7 +7,7 @@ OPTIMISM_SYNC_STATUS=$(curl -s -X POST -H "Content-Type: application/json" --dat L1_CLIENT_HEAD=$(echo $OPTIMISM_SYNC_STATUS | jq -r ".head_l1.number") L1_CLIENT_CURRENT=$(echo $OPTIMISM_SYNC_STATUS | jq -r ".current_l1.number") -if [ $L1_CLIENT_HEAD -eq 0 ]; then +if [ $L1_CLIENT_HEAD -eq 0 ]; then L1_CLIENT_BLOCKS_BEHIND=0 else L1_CLIENT_BLOCKS_BEHIND="$((L1_CLIENT_HEAD-L1_CLIENT_CURRENT))" @@ -18,7 +18,7 @@ L2_CLIENT_CURRENT=$(echo $OPTIMISM_SYNC_STATUS | jq -r ".unsafe_l2.number") L2_CLIENT_CURRENT_BLOCK_TIMESTAMP=$(echo $OPTIMISM_SYNC_STATUS | jq -r ".unsafe_l2.timestamp") L2_CLIENT_CURRENT_BLOCK_MINUTES_BEHIND="$((($(date +%s) - L2_CLIENT_CURRENT_BLOCK_TIMESTAMP)/60))" -if [ "$L2_CLIENT_CURRENT" == "null" ]; then +if [ "$L2_CLIENT_CURRENT" == "null" ]; then L2_CLIENT_CURRENT=0 fi @@ -41,4 +41,3 @@ aws cloudwatch put-metric-data --metric-name l1_blocks_behind --namespace CWAgen aws cloudwatch put-metric-data --metric-name l2_current_block --namespace CWAgent --value $L2_CLIENT_CURRENT --timestamp $TIMESTAMP --dimensions InstanceId=$INSTANCE_ID --region $REGION aws cloudwatch put-metric-data --metric-name l2_minutes_behind --namespace CWAgent --value $L2_CLIENT_CURRENT_BLOCK_MINUTES_BEHIND --timestamp $TIMESTAMP --dimensions InstanceId=$INSTANCE_ID --region $REGION - diff --git a/lib/base/sample-configs/.env-sample-rpc b/lib/base/sample-configs/.env-sample-rpc index e0960664..3aed121e 100644 --- a/lib/base/sample-configs/.env-sample-rpc +++ b/lib/base/sample-configs/.env-sample-rpc @@ -4,7 +4,7 @@ ## Set the AWS account is and region for your environment ## AWS_ACCOUNT_ID="xxxxxxxx" -AWS_REGION="us-east-1" +AWS_REGION="us-east-1" ## Common configuration parameters ## BASE_NETWORK_ID="sepolia" # All options: "mainnet", "sepolia" @@ -25,4 +25,4 @@ BASE_L1_CONSENSUS_ENDPOINT="https://ethereum-sepolia-beacon-api.publicnode.com" #BASE_L1_CONSENSUS_ENDPOINT=https://ethereum-sepolia-beacon-api.publicnode.com # Example for Mainnet and with Ethereum Blueprint with Geth-Lighthouse client combination and private IP: #BASE_L1_EXECUTION_ENDPOINT=http://172.31.15.220:8545 -#BASE_L1_CONSENSUS_ENDPOINT=http://172.31.15.220:5052 \ No newline at end of file +#BASE_L1_CONSENSUS_ENDPOINT=http://172.31.15.220:5052 From 11706ae2fee8717271474f0677d24def8eba0411 Mon Sep 17 00:00:00 2001 From: Nikolay Vlasov Date: Mon, 15 Apr 2024 12:08:41 +1000 Subject: [PATCH 18/75] Base. Fixed the website --- website/docs/Blueprints/Base.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/Blueprints/Base.md b/website/docs/Blueprints/Base.md index a7999de3..833b66c4 100644 --- a/website/docs/Blueprints/Base.md +++ b/website/docs/Blueprints/Base.md @@ -4,6 +4,6 @@ sidebar_label: Base --- # -import Readme from '../../../lib/Base/README.md'; +import Readme from '../../../lib/base/README.md'; From 0ea2ea5a243032c1767a0bd7c8868fd27bd22cb1 Mon Sep 17 00:00:00 2001 From: Nikolay Vlasov Date: Mon, 6 May 2024 15:28:25 +1000 Subject: [PATCH 19/75] Base. Debugging deployments and snapshot download --- lib/base/app.ts | 4 +- lib/base/lib/assets/restore-from-snapshot.sh | 21 ----- lib/base/lib/assets/start-from-snapshot.sh | 59 +++++++++++++ lib/base/lib/assets/user-data/node.sh | 87 ++++++++++++++------ lib/base/lib/config/baseConfig.interface.ts | 6 +- lib/base/lib/config/baseConfig.ts | 2 + lib/base/lib/single-node-stack.ts | 5 ++ lib/base/sample-configs/.env-sample-rpc | 3 + 8 files changed, 138 insertions(+), 49 deletions(-) delete mode 100644 lib/base/lib/assets/restore-from-snapshot.sh create mode 100644 lib/base/lib/assets/start-from-snapshot.sh diff --git a/lib/base/app.ts b/lib/base/app.ts index eb61a5cd..94b55bba 100644 --- a/lib/base/app.ts +++ b/lib/base/app.ts @@ -15,14 +15,16 @@ new BaseCommonStack(app, "base-common", { }); new BaseSingleNodeStack(app, "base-single-node", { - stackName: `base-single-node-${config.baseNodeConfig.baseNetworkId}`, + stackName: `base-single-node-${config.baseNodeConfig.baseNodeConfiguration}-${config.baseNodeConfig.baseNetworkId}`, env: { account: config.baseConfig.accountId, region: config.baseConfig.region }, instanceType: config.baseNodeConfig.instanceType, instanceCpuType: config.baseNodeConfig.instanceCpuType, baseNetworkId: config.baseNodeConfig.baseNetworkId, + baseNodeConfiguration: config.baseNodeConfig.baseNodeConfiguration, restoreFromSnapshot: config.baseNodeConfig.restoreFromSnapshot, l1ExecutionEndpoint: config.baseNodeConfig.l1ExecutionEndpoint, l1ConsensusEndpoint: config.baseNodeConfig.l1ConsensusEndpoint, + snapshotUrl: config.baseNodeConfig.snapshotUrl, dataVolume: config.baseNodeConfig.dataVolume, }); diff --git a/lib/base/lib/assets/restore-from-snapshot.sh b/lib/base/lib/assets/restore-from-snapshot.sh deleted file mode 100644 index 488826f1..00000000 --- a/lib/base/lib/assets/restore-from-snapshot.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/bin/bash - -source /etc/environment -TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600") -INSTANCE_ID=$(curl -H "X-aws-ec2-metadata-token: $TOKEN" -s http://169.254.169.254/latest/meta-data/instance-id) -LATEST_SNAPSHOT_FILE_NAME=$(curl https://base-snapshots-$NETWORK_ID-archive.s3.amazonaws.com/latest) - -echo "Sync started at " $(date) -SECONDS=0 - -s5cmd --log error cp s3://base-snapshots-$NETWORK_ID-archive/$LATEST_SNAPSHOT_FILE_NAME /data && \ -tar -I zstdmt -xf /data/$LATEST_SNAPSHOT_FILE_NAME -C /data && \ -mv /data/snapshots/$NETWORK_ID/download/* /data && \ -rm -rf /data/snapshots && \ -rm -rf /data/$LATEST_SNAPSHOT_FILE_NAME - -chown -R bcuser:bcuser /data && \ -echo "Sync finished at " $(date) && \ -echo "$(($SECONDS / 60)) minutes and $(($SECONDS % 60)) seconds elapsed." && \ -sudo su bcuser && \ -/usr/local/bin/docker-compose -f /home/bcuser/node/docker-compose.yml up -d diff --git a/lib/base/lib/assets/start-from-snapshot.sh b/lib/base/lib/assets/start-from-snapshot.sh new file mode 100644 index 00000000..8583cc6b --- /dev/null +++ b/lib/base/lib/assets/start-from-snapshot.sh @@ -0,0 +1,59 @@ +#!/bin/bash + +source /etc/environment +echo "Downloading snpashot" + +cd /data + +BASE_SNAPSHOT_FILE_NAME=snalshot.tar.gz +BASE_SNAPSHOT_DIR=/data/ +BASE_SNAPSHOT_DOWNLOAD_STATUS=-1 +BASE_LATEST_SNAPSHOT_FILE_NAME=$(curl https://$NETWORK_ID-$NODE_CONFIG-snapshots.base.org/latest) + +if [ "$SNAPSHOT_URL" == "none" ] || [ -z "${SNAPSHOT_URL}" ]; then + SNAPSHOT_URL=https://$NETWORK_ID-$NODE_CONFIG-snapshots.base.org/$BASE_LATEST_SNAPSHOT_FILE_NAME +fi + +while (( BASE_SNAPSHOT_DOWNLOAD_STATUS != 0 )) +do + PIDS=$(pgrep aria2c) + if [ -z "$PIDS" ]; then + aria2c -x3 $SNAPSHOT_URL -d $BASE_SNAPSHOT_DIR -o $BASE_SNAPSHOT_FILE_NAME -l aria2c.log --log-level=warn --allow-piece-length-change=true + fi + BASE_SNAPSHOT_DOWNLOAD_STATUS=$? + pid=$(pidof aria2c) + wait $pid + echo "aria2c exit." + case $BASE_SNAPSHOT_DOWNLOAD_STATUS in + 3) + echo "file not exist." + exit 3 + ;; + 9) + echo "No space left on device." + exit 9 + ;; + *) + continue + ;; + esac +done + +echo "Downloading snapshot succeed" + +sleep 60 +# take about 2 hours to decompress the snapshot +echo "Decompression snapshot start ..." + +tar -I zstdmt -xf $BASE_SNAPSHOT_DIR/$BASE_SNAPSHOT_FILE_NAME -C /data 2>&1 | tee unzip.log && echo "decompression success..." || echo "decompression failed..." >> snapshots-decompression.log +echo "Decompressing snapshot success ..." + +mv /data/snapshots/$NETWORK_ID/download/* /data && \ +rm -rf /data/snapshots && \ +rm -rf /data/$BASE_SNAPSHOT_FILE_NAME + +echo "Snapshot is ready !!!" + +chown -R bcuser:bcuser /data && \ +sudo su bcuser && \ +/usr/local/bin/docker-compose -f /home/bcuser/node/docker-compose.yml up -d diff --git a/lib/base/lib/assets/user-data/node.sh b/lib/base/lib/assets/user-data/node.sh index 7086a1e4..0d2ef2ba 100644 --- a/lib/base/lib/assets/user-data/node.sh +++ b/lib/base/lib/assets/user-data/node.sh @@ -6,9 +6,11 @@ LIFECYCLE_HOOK_NAME=${_LIFECYCLE_HOOK_NAME_} AUTOSCALING_GROUP_NAME=${_AUTOSCALING_GROUP_NAME_} RESOURCE_ID=${_NODE_CF_LOGICAL_ID_} ASSETS_S3_PATH=${_ASSETS_S3_PATH_} -echo "LIFECYCLE_HOOK_NAME=$LIFECYCLE_HOOK_NAME" >> /etc/environment -echo "AUTOSCALING_GROUP_NAME=$AUTOSCALING_GROUP_NAME" >> /etc/environment -echo "ASSETS_S3_PATH=$ASSETS_S3_PATH" >> /etc/environment +{ + echo "LIFECYCLE_HOOK_NAME=$LIFECYCLE_HOOK_NAME" + echo "AUTOSCALING_GROUP_NAME=$AUTOSCALING_GROUP_NAME" + echo "ASSETS_S3_PATH=$ASSETS_S3_PATH" +} >> /etc/environment arch=$(uname -m) @@ -55,19 +57,22 @@ unzip -q awscliv2.zip rm /usr/bin/aws ln /usr/local/bin/aws /usr/bin/aws -aws configure set default.s3.max_concurrent_requests 50 -aws configure set default.s3.multipart_chunksize 256MB - echo 'Installing SSM Agent' yum install -y $SSM_AGENT_BINARY_URI -echo "Installing s5cmd" -cd /opt -wget -q $S5CMD_URI -O s5cmd.tar.gz -tar -xf s5cmd.tar.gz -chmod +x s5cmd -mv s5cmd /usr/bin -s5cmd version +# install aria2 a p2p downloader + +if [ "$arch" == "x86_64" ]; then + wget https://github.com/q3aql/aria2-static-builds/releases/download/v1.36.0/aria2-1.36.0-linux-gnu-64bit-build1.tar.bz2 + tar jxvf aria2-1.36.0-linux-gnu-64bit-build1.tar.bz2 + cd aria2-1.36.0-linux-gnu-64bit-build1/ + make install +else + wget https://github.com/q3aql/aria2-static-builds/releases/download/v1.36.0/aria2-1.36.0-linux-gnu-arm-rbpi-build1.tar.bz2 + tar jxvf aria2-1.36.0-linux-gnu-arm-rbpi-build1.tar.bz2 + cd aria2-1.36.0-linux-gnu-arm-rbpi-build1/ + make install +fi # Base specific setup starts here @@ -77,13 +82,19 @@ STACK_NAME=${_STACK_NAME_} RESTORE_FROM_SNAPSHOT=${_RESTORE_FROM_SNAPSHOT_} FORMAT_DISK=${_FORMAT_DISK_} NETWORK_ID=${_NETWORK_ID_} +NODE_CONFIG=${_NODE_CONFIG_} L1_EXECUTION_ENDPOINT=${_L1_EXECUTION_ENDPOINT_} L1_CONSENSUS_ENDPOINT=${_L1_CONSENSUS_ENDPOINT_} +SNAPSHOT_URL=${_SNAPSHOT_URL_} -echo "REGION=$REGION" >> /etc/environment -echo "NETWORK_ID=$NETWORK_ID" >> /etc/environment -echo "L1_EXECUTION_ENDPOINT=$L1_EXECUTION_ENDPOINT" >> /etc/environment -echo "L1_CONSENSUS_ENDPOINT=$L1_CONSENSUS_ENDPOINT" >> /etc/environment +{ + echo "REGION=$REGION" + echo "NETWORK_ID=$NETWORK_ID" + echo "NODE_CONFIG=$NODE_CONFIG" + echo "L1_EXECUTION_ENDPOINT=$L1_EXECUTION_ENDPOINT" + echo "L1_CONSENSUS_ENDPOINT=$L1_CONSENSUS_ENDPOINT" + echo "SNAPSHOT_URL=$SNAPSHOT_URL" +} >> /etc/environment GIT_URL=https://github.com/base-org/node.git SYNC_CHECKER_FILE_NAME=syncchecker-base.sh @@ -147,16 +158,42 @@ echo "Configuring node" case $NETWORK_ID in "mainnet") - sed -i "s#OP_NODE_L1_ETH_RPC=https://1rpc.io/eth#OP_NODE_L1_ETH_RPC=$L1_EXECUTION_ENDPOINT#g" /home/bcuser/node/.env.mainnet + OP_CONFIG_FILE_PATH=/home/bcuser/node/.env.mainnet + ;; + "sepolia") + OP_CONFIG_FILE_PATH=/home/bcuser/node/.env.sepolia + ;; + *) + echo "Network id is not valid." + exit 1 + ;; +esac + +case $NODE_CONFIG in + "full") + echo "OP_GETH_GCMODE=full" >> $OP_CONFIG_FILE_PATH + ;; + "archive") + echo "OP_GETH_GCMODE=archive" >> $OP_CONFIG_FILE_PATH + ;; + *) + echo "Network id is not valid." + exit 1 + ;; +esac + +case $NETWORK_ID in + "mainnet") + sed -i "s#OP_NODE_L1_ETH_RPC=https://1rpc.io/eth#OP_NODE_L1_ETH_RPC=$L1_EXECUTION_ENDPOINT#g" $OP_CONFIG_FILE_PATH sed -i '/.env.mainnet/s/^#//g' /home/bcuser/node/docker-compose.yml - sed -i '/OP_NODE_L1_BEACON/s/^#//g' /home/bcuser/node/.env.mainnet - sed -i "s#OP_NODE_L1_BEACON=https://your.mainnet.beacon.node/endpoint-here#OP_NODE_L1_BEACON=$L1_CONSENSUS_ENDPOINT#g" /home/bcuser/node/.env.mainnet + sed -i '/OP_NODE_L1_BEACON/s/^#//g' $OP_CONFIG_FILE_PATH + sed -i "s#OP_NODE_L1_BEACON=https://your.mainnet.beacon.node/endpoint-here#OP_NODE_L1_BEACON=$L1_CONSENSUS_ENDPOINT#g" $OP_CONFIG_FILE_PATH ;; "sepolia") - sed -i "s#OP_NODE_L1_ETH_RPC=https://rpc.sepolia.org#OP_NODE_L1_ETH_RPC=$L1_EXECUTION_ENDPOINT#g" /home/bcuser/node/.env.sepolia + sed -i "s#OP_NODE_L1_ETH_RPC=https://rpc.sepolia.org#OP_NODE_L1_ETH_RPC=$L1_EXECUTION_ENDPOINT#g" $OP_CONFIG_FILE_PATH sed -i "/.env.sepolia/s/^#//g" /home/bcuser/node/docker-compose.yml - sed -i '/OP_NODE_L1_BEACON/s/^#//g' /home/bcuser/node/.env.sepolia - sed -i "s#OP_NODE_L1_BEACON=https://your.sepolia.beacon.node/endpoint-here#OP_NODE_L1_BEACON=$L1_CONSENSUS_ENDPOINT#g" /home/bcuser/node/.env.sepolia + sed -i '/OP_NODE_L1_BEACON/s/^#//g' $OP_CONFIG_FILE_PATH + sed -i "s#OP_NODE_L1_BEACON=https://your.sepolia.beacon.node/endpoint-here#OP_NODE_L1_BEACON=$L1_CONSENSUS_ENDPOINT#g" $OP_CONFIG_FILE_PATH ;; *) echo "Network id is not valid." @@ -217,8 +254,8 @@ if [ "$RESTORE_FROM_SNAPSHOT" == "false" ]; then echo "sudo su bcuser && /usr/local/bin/docker-compose -f /home/bcuser/node/docker-compose.yml up -d" | at now +3 minutes else echo "Restoring data from snapshot" - chmod 766 /opt/restore-from-snapshot.sh - echo "/opt/restore-from-snapshot.sh" | at now +3 minutes + chmod 766 /opt/start-from-snapshot.sh + echo "/opt/start-from-snapshot.sh" | at now +3 minutes fi echo "All Done!!" diff --git a/lib/base/lib/config/baseConfig.interface.ts b/lib/base/lib/config/baseConfig.interface.ts index 24539158..e7e327a2 100644 --- a/lib/base/lib/config/baseConfig.interface.ts +++ b/lib/base/lib/config/baseConfig.interface.ts @@ -1,7 +1,7 @@ import * as configTypes from "../../../constructs/config.interface"; -export type BaseNetworkId = "mainnet" ; -export type BaseNodeConfiguration = "full" ; +export type BaseNetworkId = "mainnet" | "sepolia"; +export type BaseNodeConfiguration = "full" | "archive"; export {AMBEthereumNodeNetworkId} from "../../../constructs/config.interface"; @@ -16,8 +16,10 @@ export interface BaseBaseConfig extends configTypes.BaseConfig { export interface BaseBaseNodeConfig extends configTypes.BaseNodeConfig { baseNetworkId: BaseNetworkId; + baseNodeConfiguration: BaseNodeConfiguration; dataVolume: BaseDataVolumeConfig; restoreFromSnapshot: boolean; l1ExecutionEndpoint: string; l1ConsensusEndpoint: string; + snapshotUrl: string; } diff --git a/lib/base/lib/config/baseConfig.ts b/lib/base/lib/config/baseConfig.ts index 9bfd345e..4da2e404 100644 --- a/lib/base/lib/config/baseConfig.ts +++ b/lib/base/lib/config/baseConfig.ts @@ -27,9 +27,11 @@ export const baseNodeConfig: configTypes.BaseBaseNodeConfig = { instanceType: new ec2.InstanceType(process.env.BASE_INSTANCE_TYPE ? process.env.BASE_INSTANCE_TYPE : "m7g.2xlarge"), instanceCpuType: process.env.BASE_CPU_TYPE?.toLowerCase() == "x86_64" ? ec2.AmazonLinuxCpuType.X86_64 : ec2.AmazonLinuxCpuType.ARM_64, baseNetworkId: process.env.BASE_NETWORK_ID || "mainnet", + baseNodeConfiguration: process.env.BASE_NODE_CONFIGURATION || "full", restoreFromSnapshot: process.env.BASE_RESTORE_FROM_SNAPSHOT?.toLowerCase() == "true" ? true : false, l1ExecutionEndpoint: process.env.BASE_L1_EXECUTION_ENDPOINT || constants.NoneValue, l1ConsensusEndpoint: process.env.BASE_L1_CONSENSUS_ENDPOINT || constants.NoneValue, + snapshotUrl: process.env.BASE_SNAPSHOT_URL || constants.NoneValue, dataVolume: { sizeGiB: process.env.BASE_DATA_VOL_SIZE ? parseInt(process.env.BASE_DATA_VOL_SIZE): 1000, type: parseDataVolumeType(process.env.BASE_DATA_VOL_TYPE?.toLowerCase() ? process.env.BASE_DATA_VOL_TYPE?.toLowerCase() : "gp3"), diff --git a/lib/base/lib/single-node-stack.ts b/lib/base/lib/single-node-stack.ts index b32fa3cc..f47e7ac4 100644 --- a/lib/base/lib/single-node-stack.ts +++ b/lib/base/lib/single-node-stack.ts @@ -17,9 +17,11 @@ export interface BaseSingleNodeStackProps extends cdk.StackProps { instanceType: ec2.InstanceType; instanceCpuType: ec2.AmazonLinuxCpuType; baseNetworkId: configTypes.BaseNetworkId; + baseNodeConfiguration: configTypes.BaseNodeConfiguration; restoreFromSnapshot: boolean; l1ExecutionEndpoint: string, l1ConsensusEndpoint: string, + snapshotUrl: string, dataVolume: configTypes.BaseDataVolumeConfig; } @@ -39,6 +41,7 @@ export class BaseSingleNodeStack extends cdk.Stack { instanceType, instanceCpuType, baseNetworkId, + baseNodeConfiguration, restoreFromSnapshot, l1ExecutionEndpoint, l1ConsensusEndpoint, @@ -103,12 +106,14 @@ export class BaseSingleNodeStack extends cdk.Stack { _DATA_VOLUME_TYPE_: dataVolume.type, _DATA_VOLUME_SIZE_: dataVolumeSizeBytes.toString(), _NETWORK_ID_: baseNetworkId, + _NODE_CONFIG_: baseNodeConfiguration, _LIFECYCLE_HOOK_NAME_: constants.NoneValue, _AUTOSCALING_GROUP_NAME_: constants.NoneValue, _RESTORE_FROM_SNAPSHOT_: restoreFromSnapshot.toString(), _FORMAT_DISK_: "true", _L1_EXECUTION_ENDPOINT_: l1ExecutionEndpoint, _L1_CONSENSUS_ENDPOINT_: l1ConsensusEndpoint, + _SNAPSHOT_URL_: props.snapshotUrl, }); node.instance.addUserData(modifiedInitNodeScript); diff --git a/lib/base/sample-configs/.env-sample-rpc b/lib/base/sample-configs/.env-sample-rpc index 3aed121e..2d991859 100644 --- a/lib/base/sample-configs/.env-sample-rpc +++ b/lib/base/sample-configs/.env-sample-rpc @@ -8,6 +8,7 @@ AWS_REGION="us-east-1" ## Common configuration parameters ## BASE_NETWORK_ID="sepolia" # All options: "mainnet", "sepolia" +BASE_NODE_CONFIGURATION="full" # All options: "full", "archive" BASE_INSTANCE_TYPE="m7g.2xlarge" BASE_CPU_TYPE="ARM_64" # All options: "x86_64", "ARM_64". IMPORTANT: Make sure the CPU type matches the instance type used @@ -20,6 +21,8 @@ BASE_RESTORE_FROM_SNAPSHOT="true" # Download snapshot to speed up statup ti BASE_L1_EXECUTION_ENDPOINT="https://ethereum-sepolia-rpc.publicnode.com" # Set your own URL to Ethereum L1 node: https://docs.base.org/tools/node-providers BASE_L1_CONSENSUS_ENDPOINT="https://ethereum-sepolia-beacon-api.publicnode.com" +BASE_SNAPSHOT_URL="none" # Optionally provide the URL to download snpashot: https://docs.base.org/tutorials/run-a-base-node/#snapshots + # Example for Sepolia: #BASE_L1_EXECUTION_ENDPOINT=https://ethereum-sepolia-rpc.publicnode.com #BASE_L1_CONSENSUS_ENDPOINT=https://ethereum-sepolia-beacon-api.publicnode.com From 75d6ecd39d88908520200140ece1ffa5efab34fa Mon Sep 17 00:00:00 2001 From: Nikolay Vlasov Date: Mon, 6 May 2024 15:30:13 +1000 Subject: [PATCH 20/75] Ethereum. Added S3 buckets for Amazon Linux 2023 repos --- lib/ethereum/lib/common-stack.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/ethereum/lib/common-stack.ts b/lib/ethereum/lib/common-stack.ts index ba9abb9d..067e6466 100644 --- a/lib/ethereum/lib/common-stack.ts +++ b/lib/ethereum/lib/common-stack.ts @@ -70,6 +70,8 @@ export class EthCommonStack extends cdk.Stack { resources: [ snapshotsBucket.bucketArn, snapshotsBucket.arnForObjects("*"), + `arn:aws:s3:::al2023-repos-${region}-*`, + `arn:aws:s3:::al2023-repos-${region}-*/*`, `arn:aws:s3:::amazonlinux-2-repos-${region}`, `arn:aws:s3:::amazonlinux-2-repos-${region}/*`, `arn:aws:s3:::${asset.s3BucketName}`, From c80087984226ac707783e48e4abc58997835eacd Mon Sep 17 00:00:00 2001 From: Nikolay Vlasov Date: Tue, 14 May 2024 16:02:06 +1000 Subject: [PATCH 21/75] Base. Moving to different download method --- lib/base/lib/assets/start-from-snapshot.sh | 28 +++++++++++----------- lib/base/lib/assets/user-data/node.sh | 14 ----------- 2 files changed, 14 insertions(+), 28 deletions(-) diff --git a/lib/base/lib/assets/start-from-snapshot.sh b/lib/base/lib/assets/start-from-snapshot.sh index 8583cc6b..543821e6 100644 --- a/lib/base/lib/assets/start-from-snapshot.sh +++ b/lib/base/lib/assets/start-from-snapshot.sh @@ -5,34 +5,34 @@ echo "Downloading snpashot" cd /data -BASE_SNAPSHOT_FILE_NAME=snalshot.tar.gz +BASE_SNAPSHOT_FILE_NAME=snapshot.tar.gz BASE_SNAPSHOT_DIR=/data/ BASE_SNAPSHOT_DOWNLOAD_STATUS=-1 -BASE_LATEST_SNAPSHOT_FILE_NAME=$(curl https://$NETWORK_ID-$NODE_CONFIG-snapshots.base.org/latest) if [ "$SNAPSHOT_URL" == "none" ] || [ -z "${SNAPSHOT_URL}" ]; then + BASE_LATEST_SNAPSHOT_FILE_NAME=$(curl https://$NETWORK_ID-$NODE_CONFIG-snapshots.base.org/latest) SNAPSHOT_URL=https://$NETWORK_ID-$NODE_CONFIG-snapshots.base.org/$BASE_LATEST_SNAPSHOT_FILE_NAME fi while (( BASE_SNAPSHOT_DOWNLOAD_STATUS != 0 )) do - PIDS=$(pgrep aria2c) + PIDS=$(pgrep wget) if [ -z "$PIDS" ]; then - aria2c -x3 $SNAPSHOT_URL -d $BASE_SNAPSHOT_DIR -o $BASE_SNAPSHOT_FILE_NAME -l aria2c.log --log-level=warn --allow-piece-length-change=true + wget --continue --retry-connrefused --waitretry=66 --read-timeout=20 --output-document$BASE_SNAPSHOT_DIR/$BASE_SNAPSHOT_FILE_NAME -o download.log -t 0 $SNAPSHOT_URL fi BASE_SNAPSHOT_DOWNLOAD_STATUS=$? - pid=$(pidof aria2c) + pid=$(pidof wget) wait $pid - echo "aria2c exit." + echo "wget exit." case $BASE_SNAPSHOT_DOWNLOAD_STATUS in + 2) + echo "CLI parsing error. Check variables." + exit 2 + ;; 3) - echo "file not exist." + echo "File I/O error." exit 3 ;; - 9) - echo "No space left on device." - exit 9 - ;; *) continue ;; @@ -45,14 +45,14 @@ sleep 60 # take about 2 hours to decompress the snapshot echo "Decompression snapshot start ..." -tar -I zstdmt -xf $BASE_SNAPSHOT_DIR/$BASE_SNAPSHOT_FILE_NAME -C /data 2>&1 | tee unzip.log && echo "decompression success..." || echo "decompression failed..." >> snapshots-decompression.log -echo "Decompressing snapshot success ..." +tar -zxvf $BASE_SNAPSHOT_DIR/$BASE_SNAPSHOT_FILE_NAME -C /data 2>&1 | tee unzip.log && echo "decompresed successfully..." || echo "decompression failed..." >> snapshots-decompression.log +echo "Decompresed snapshot ..." mv /data/snapshots/$NETWORK_ID/download/* /data && \ rm -rf /data/snapshots && \ rm -rf /data/$BASE_SNAPSHOT_FILE_NAME -echo "Snapshot is ready !!!" +echo "Processed snapshot" chown -R bcuser:bcuser /data && \ sudo su bcuser && \ diff --git a/lib/base/lib/assets/user-data/node.sh b/lib/base/lib/assets/user-data/node.sh index 0d2ef2ba..e61e6cbe 100644 --- a/lib/base/lib/assets/user-data/node.sh +++ b/lib/base/lib/assets/user-data/node.sh @@ -60,20 +60,6 @@ ln /usr/local/bin/aws /usr/bin/aws echo 'Installing SSM Agent' yum install -y $SSM_AGENT_BINARY_URI -# install aria2 a p2p downloader - -if [ "$arch" == "x86_64" ]; then - wget https://github.com/q3aql/aria2-static-builds/releases/download/v1.36.0/aria2-1.36.0-linux-gnu-64bit-build1.tar.bz2 - tar jxvf aria2-1.36.0-linux-gnu-64bit-build1.tar.bz2 - cd aria2-1.36.0-linux-gnu-64bit-build1/ - make install -else - wget https://github.com/q3aql/aria2-static-builds/releases/download/v1.36.0/aria2-1.36.0-linux-gnu-arm-rbpi-build1.tar.bz2 - tar jxvf aria2-1.36.0-linux-gnu-arm-rbpi-build1.tar.bz2 - cd aria2-1.36.0-linux-gnu-arm-rbpi-build1/ - make install -fi - # Base specific setup starts here # Set by Base-specic CDK components and stacks From 05bcdd67bf6db531fe137d263f6d8918557bcf7d Mon Sep 17 00:00:00 2001 From: Nikolay Vlasov Date: Thu, 16 May 2024 10:59:20 +1000 Subject: [PATCH 22/75] Base. Refactoring snapshot download. --- ...-from-snapshot.sh => download-snapshot.sh} | 6 +--- lib/base/lib/assets/user-data/node.sh | 10 +++++- lib/base/sample-configs/.env-sample-archive | 31 +++++++++++++++++++ .../{.env-sample-rpc => .env-sample-full} | 4 +-- 4 files changed, 43 insertions(+), 8 deletions(-) rename lib/base/lib/assets/{start-from-snapshot.sh => download-snapshot.sh} (83%) create mode 100644 lib/base/sample-configs/.env-sample-archive rename lib/base/sample-configs/{.env-sample-rpc => .env-sample-full} (94%) diff --git a/lib/base/lib/assets/start-from-snapshot.sh b/lib/base/lib/assets/download-snapshot.sh similarity index 83% rename from lib/base/lib/assets/start-from-snapshot.sh rename to lib/base/lib/assets/download-snapshot.sh index 543821e6..9f04707c 100644 --- a/lib/base/lib/assets/start-from-snapshot.sh +++ b/lib/base/lib/assets/download-snapshot.sh @@ -18,7 +18,7 @@ while (( BASE_SNAPSHOT_DOWNLOAD_STATUS != 0 )) do PIDS=$(pgrep wget) if [ -z "$PIDS" ]; then - wget --continue --retry-connrefused --waitretry=66 --read-timeout=20 --output-document$BASE_SNAPSHOT_DIR/$BASE_SNAPSHOT_FILE_NAME -o download.log -t 0 $SNAPSHOT_URL + wget --continue --retry-connrefused --waitretry=66 --read-timeout=20 --output-document $BASE_SNAPSHOT_DIR/$BASE_SNAPSHOT_FILE_NAME -nv -o download.log -t 0 $SNAPSHOT_URL fi BASE_SNAPSHOT_DOWNLOAD_STATUS=$? pid=$(pidof wget) @@ -53,7 +53,3 @@ rm -rf /data/snapshots && \ rm -rf /data/$BASE_SNAPSHOT_FILE_NAME echo "Processed snapshot" - -chown -R bcuser:bcuser /data && \ -sudo su bcuser && \ -/usr/local/bin/docker-compose -f /home/bcuser/node/docker-compose.yml up -d diff --git a/lib/base/lib/assets/user-data/node.sh b/lib/base/lib/assets/user-data/node.sh index e61e6cbe..06e803b0 100644 --- a/lib/base/lib/assets/user-data/node.sh +++ b/lib/base/lib/assets/user-data/node.sh @@ -241,7 +241,15 @@ if [ "$RESTORE_FROM_SNAPSHOT" == "false" ]; then else echo "Restoring data from snapshot" chmod 766 /opt/start-from-snapshot.sh - echo "/opt/start-from-snapshot.sh" | at now +3 minutes + /opt/download-snapshot.sh + if [ "$?" == 0 ]; then + echo "Snapshot download successful" + else + echo "Snapshot download failed, falling back to fresh sync" + fi + chown -R bcuser:bcuser /data + sudo su bcuser + /usr/local/bin/docker-compose -f /home/bcuser/node/docker-compose.yml up -d fi echo "All Done!!" diff --git a/lib/base/sample-configs/.env-sample-archive b/lib/base/sample-configs/.env-sample-archive new file mode 100644 index 00000000..12d99bea --- /dev/null +++ b/lib/base/sample-configs/.env-sample-archive @@ -0,0 +1,31 @@ +############################################################# +# Example configuration for Base nodes runner app on AWS # +############################################################# + +## Set the AWS account is and region for your environment ## +AWS_ACCOUNT_ID="xxxxxxxx" +AWS_REGION="us-east-1" + +## Common configuration parameters ## +BASE_NETWORK_ID="mainnet" # All options: "mainnet", "sepolia" +BASE_NODE_CONFIGURATION="archive" # All options: "full", "archive" +BASE_INSTANCE_TYPE="m7g.2xlarge" +BASE_CPU_TYPE="ARM_64" # All options: "x86_64", "ARM_64". IMPORTANT: Make sure the CPU type matches the instance type used + +# Data volume configuration +BASE_DATA_VOL_TYPE="gp3" # Other options: "io1" | "io2" | "gp3" | "instance-store" . IMPORTANT: Use "instance-store" option only with instance types that support that feature, like popular for node im4gn, d3, i3en, and i4i instance families +BASE_DATA_VOL_SIZE="7200" # Current required data size in GB to keep both snapshot archive and unarchived version of it. For Sepolia 1000 will be sufficient. +BASE_DATA_VOL_IOPS="5000" # Max IOPS for EBS volumes (not applicable for "instance-store") +BASE_DATA_VOL_THROUGHPUT="700" # Max throughput for EBS gp3 volumes (not applicable for "io1" | "io2" | "instance-store") +BASE_RESTORE_FROM_SNAPSHOT="true" # Download snapshot to speed up statup time +BASE_L1_EXECUTION_ENDPOINT="https://ethereum-sepolia-rpc.publicnode.com" # Set your own URL to Ethereum L1 node: https://docs.base.org/tools/node-providers +BASE_L1_CONSENSUS_ENDPOINT="https://ethereum-sepolia-beacon-api.publicnode.com" + +BASE_SNAPSHOT_URL="none" # Optionally provide the URL to download snpashot: https://docs.base.org/tutorials/run-a-base-node/#snapshots + +# Example for Sepolia: +#BASE_L1_EXECUTION_ENDPOINT=https://ethereum-sepolia-rpc.publicnode.com +#BASE_L1_CONSENSUS_ENDPOINT=https://ethereum-sepolia-beacon-api.publicnode.com +# Example for Mainnet and with Ethereum Blueprint with Geth-Lighthouse client combination and private IP: +#BASE_L1_EXECUTION_ENDPOINT=http://172.31.15.220:8545 +#BASE_L1_CONSENSUS_ENDPOINT=http://172.31.15.220:5052 diff --git a/lib/base/sample-configs/.env-sample-rpc b/lib/base/sample-configs/.env-sample-full similarity index 94% rename from lib/base/sample-configs/.env-sample-rpc rename to lib/base/sample-configs/.env-sample-full index 2d991859..a99c26b3 100644 --- a/lib/base/sample-configs/.env-sample-rpc +++ b/lib/base/sample-configs/.env-sample-full @@ -7,14 +7,14 @@ AWS_ACCOUNT_ID="xxxxxxxx" AWS_REGION="us-east-1" ## Common configuration parameters ## -BASE_NETWORK_ID="sepolia" # All options: "mainnet", "sepolia" +BASE_NETWORK_ID="mainnet" # All options: "mainnet", "sepolia" BASE_NODE_CONFIGURATION="full" # All options: "full", "archive" BASE_INSTANCE_TYPE="m7g.2xlarge" BASE_CPU_TYPE="ARM_64" # All options: "x86_64", "ARM_64". IMPORTANT: Make sure the CPU type matches the instance type used # Data volume configuration BASE_DATA_VOL_TYPE="gp3" # Other options: "io1" | "io2" | "gp3" | "instance-store" . IMPORTANT: Use "instance-store" option only with instance types that support that feature, like popular for node im4gn, d3, i3en, and i4i instance families -BASE_DATA_VOL_SIZE="5100" # Current required data size in GB to keep both snapshot archive and unarchived version of it. For Sepolia 1000 will be sufficient. +BASE_DATA_VOL_SIZE="4000" # Current required data size in GB to keep both snapshot archive and unarchived version of it. For Sepolia 1000 will be sufficient. BASE_DATA_VOL_IOPS="5000" # Max IOPS for EBS volumes (not applicable for "instance-store") BASE_DATA_VOL_THROUGHPUT="700" # Max throughput for EBS gp3 volumes (not applicable for "io1" | "io2" | "instance-store") BASE_RESTORE_FROM_SNAPSHOT="true" # Download snapshot to speed up statup time From 3f73d1be3885caec2a8d8bc2c38e37e44972db64 Mon Sep 17 00:00:00 2001 From: Nikolay Vlasov Date: Mon, 20 May 2024 16:39:57 +1000 Subject: [PATCH 23/75] Base. Debugging snapshot download --- lib/base/lib/assets/base/node.sh | 10 +++ lib/base/lib/assets/download-snapshot.sh | 55 ----------------- lib/base/lib/assets/restore-from-snapshot.sh | 63 +++++++++++++++++++ lib/base/lib/assets/user-data/node.sh | 65 +++++++++++++++----- 4 files changed, 124 insertions(+), 69 deletions(-) create mode 100644 lib/base/lib/assets/base/node.sh delete mode 100644 lib/base/lib/assets/download-snapshot.sh create mode 100644 lib/base/lib/assets/restore-from-snapshot.sh diff --git a/lib/base/lib/assets/base/node.sh b/lib/base/lib/assets/base/node.sh new file mode 100644 index 00000000..abdd35a5 --- /dev/null +++ b/lib/base/lib/assets/base/node.sh @@ -0,0 +1,10 @@ +#!/bin/bash +set -e +echo "Script is starting..." +ulimit -n 500000 + +# Start the node +cd /home/bcuser/node +/usr/local/bin/docker-compose -f /home/bcuser/node/docker-compose.yml up -d + +echo "Started" \ No newline at end of file diff --git a/lib/base/lib/assets/download-snapshot.sh b/lib/base/lib/assets/download-snapshot.sh deleted file mode 100644 index 9f04707c..00000000 --- a/lib/base/lib/assets/download-snapshot.sh +++ /dev/null @@ -1,55 +0,0 @@ -#!/bin/bash - -source /etc/environment -echo "Downloading snpashot" - -cd /data - -BASE_SNAPSHOT_FILE_NAME=snapshot.tar.gz -BASE_SNAPSHOT_DIR=/data/ -BASE_SNAPSHOT_DOWNLOAD_STATUS=-1 - -if [ "$SNAPSHOT_URL" == "none" ] || [ -z "${SNAPSHOT_URL}" ]; then - BASE_LATEST_SNAPSHOT_FILE_NAME=$(curl https://$NETWORK_ID-$NODE_CONFIG-snapshots.base.org/latest) - SNAPSHOT_URL=https://$NETWORK_ID-$NODE_CONFIG-snapshots.base.org/$BASE_LATEST_SNAPSHOT_FILE_NAME -fi - -while (( BASE_SNAPSHOT_DOWNLOAD_STATUS != 0 )) -do - PIDS=$(pgrep wget) - if [ -z "$PIDS" ]; then - wget --continue --retry-connrefused --waitretry=66 --read-timeout=20 --output-document $BASE_SNAPSHOT_DIR/$BASE_SNAPSHOT_FILE_NAME -nv -o download.log -t 0 $SNAPSHOT_URL - fi - BASE_SNAPSHOT_DOWNLOAD_STATUS=$? - pid=$(pidof wget) - wait $pid - echo "wget exit." - case $BASE_SNAPSHOT_DOWNLOAD_STATUS in - 2) - echo "CLI parsing error. Check variables." - exit 2 - ;; - 3) - echo "File I/O error." - exit 3 - ;; - *) - continue - ;; - esac -done - -echo "Downloading snapshot succeed" - -sleep 60 -# take about 2 hours to decompress the snapshot -echo "Decompression snapshot start ..." - -tar -zxvf $BASE_SNAPSHOT_DIR/$BASE_SNAPSHOT_FILE_NAME -C /data 2>&1 | tee unzip.log && echo "decompresed successfully..." || echo "decompression failed..." >> snapshots-decompression.log -echo "Decompresed snapshot ..." - -mv /data/snapshots/$NETWORK_ID/download/* /data && \ -rm -rf /data/snapshots && \ -rm -rf /data/$BASE_SNAPSHOT_FILE_NAME - -echo "Processed snapshot" diff --git a/lib/base/lib/assets/restore-from-snapshot.sh b/lib/base/lib/assets/restore-from-snapshot.sh new file mode 100644 index 00000000..c73bb927 --- /dev/null +++ b/lib/base/lib/assets/restore-from-snapshot.sh @@ -0,0 +1,63 @@ +#!/bin/bash +set +e + +source /etc/environment + +echo "Downloading Snapshot." + +cd /data + +SNAPSHOT_FILE_NAME=snapshot.tar.gz +SNAPSHOT_DIR=/data +SNAPSHOT_DOWNLOAD_STATUS=-1 + +if [ "$SNAPSHOT_URL" == "none" ] || [ -z "${SNAPSHOT_URL}" ]; then + LATEST_SNAPSHOT_FILE_NAME=$(curl https://$NETWORK_ID-$NODE_CONFIG-snapshots.base.org/latest) + SNAPSHOT_URL=https://$NETWORK_ID-$NODE_CONFIG-snapshots.base.org/$LATEST_SNAPSHOT_FILE_NAME +fi + +# take about 1 hour to download the Snapshot +while (( SNAPSHOT_DOWNLOAD_STATUS != 0 )) +do + PIDS=$(pgrep aria2c) + if [ -z "$PIDS" ]; then + aria2c $SNAPSHOT_URL -d $SNAPSHOT_DIR -o $SNAPSHOT_FILE_NAME -l /data/download.log --log-level=notice --allow-overwrite=true --allow-piece-length-change=true + fi + SNAPSHOT_DOWNLOAD_STATUS=$? + pid=$(pidof aria2c) + wait $pid + echo "aria2c exit." + case $SNAPSHOT_DOWNLOAD_STATUS in + 3) + echo "File does not exist." + exit 3 + ;; + 9) + echo "No space left on device." + exit 9 + ;; + *) + continue + ;; + esac +done +echo "Downloading Snapshot script finished" + +sleep 60 + +echo "Starting snapshot decompression ..." + +tar -zxvf $SNAPSHOT_DIR/$SNAPSHOT_FILE_NAME -C /data 2>&1 | tee unzip.log && echo "decompresed successfully..." || echo "decompression failed..." >> snapshots-decompression.log + +echo "Decompresed snapshot, cleaning up..." + +mv /data/snapshots/$NETWORK_ID/download/* /data && \ +rm -rf /data/snapshots && \ +rm -rf $SNAPSHOT_DIR/$SNAPSHOT_FILE_NAME + +echo "Snapshot is ready, starting the service.." + +chown -R bcuser:bcuser /data + +sudo systemctl daemon-reload +sudo systemctl enable --now base \ No newline at end of file diff --git a/lib/base/lib/assets/user-data/node.sh b/lib/base/lib/assets/user-data/node.sh index 06e803b0..d6f2d332 100644 --- a/lib/base/lib/assets/user-data/node.sh +++ b/lib/base/lib/assets/user-data/node.sh @@ -33,6 +33,20 @@ yum update -y yum -y install amazon-cloudwatch-agent collectd jq yq gcc ncurses-devel aws-cfn-bootstrap zstd wget $YQ_URI -O /usr/bin/yq && chmod +x /usr/bin/yq +# install aria2 a p2p downloader + +if [ "$arch" == "x86_64" ]; then + wget https://github.com/q3aql/aria2-static-builds/releases/download/v1.36.0/aria2-1.36.0-linux-gnu-64bit-build1.tar.bz2 + tar jxvf aria2-1.36.0-linux-gnu-64bit-build1.tar.bz2 + cd aria2-1.36.0-linux-gnu-64bit-build1/ + make install +else + wget https://github.com/q3aql/aria2-static-builds/releases/download/v1.36.0/aria2-1.36.0-linux-gnu-arm-rbpi-build1.tar.bz2 + tar jxvf aria2-1.36.0-linux-gnu-arm-rbpi-build1.tar.bz2 + cd aria2-1.36.0-linux-gnu-arm-rbpi-build1/ + make install +fi + cd /opt echo "Downloading assets zip file" @@ -187,6 +201,8 @@ case $NETWORK_ID in ;; esac +echo "OP_NODE_L1_TRUST_RPC=true" >> $OP_CONFIG_FILE_PATH + sed -i "s#GETH_HOST_DATA_DIR=./geth-data#GETH_HOST_DATA_DIR=/data/geth#g" /home/bcuser/node/.env chown -R bcuser:bcuser /home/bcuser/node @@ -198,6 +214,27 @@ chmod 766 /opt/syncchecker.sh echo "*/5 * * * * /opt/syncchecker.sh" | crontab crontab -l +echo "Configuring node as a service" +mkdir /home/bcuser/bin +mv /opt/base/node.sh /home/bcuser/bin/node.sh +chmod 766 /home/bcuser/bin/node.sh +chown -R bcuser:bcuser /home/bcuser + +sudo bash -c 'cat > /etc/systemd/system/base.service < Date: Tue, 21 May 2024 14:50:08 +1000 Subject: [PATCH 24/75] Base. Removed ulimit from service startup script --- lib/base/lib/assets/base/node.sh | 1 - lib/base/lib/assets/user-data/node.sh | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/base/lib/assets/base/node.sh b/lib/base/lib/assets/base/node.sh index abdd35a5..866c7fcd 100644 --- a/lib/base/lib/assets/base/node.sh +++ b/lib/base/lib/assets/base/node.sh @@ -1,7 +1,6 @@ #!/bin/bash set -e echo "Script is starting..." -ulimit -n 500000 # Start the node cd /home/bcuser/node diff --git a/lib/base/lib/assets/user-data/node.sh b/lib/base/lib/assets/user-data/node.sh index d6f2d332..00bf8c73 100644 --- a/lib/base/lib/assets/user-data/node.sh +++ b/lib/base/lib/assets/user-data/node.sh @@ -273,8 +273,8 @@ chmod -R 755 /data if [ "$RESTORE_FROM_SNAPSHOT" == "false" ]; then echo "Skipping restoration from snapshot. Starting node" - sudo systemctl daemon-reload - sudo systemctl enable --now base + systemctl daemon-reload + systemctl enable --now base else echo "Restoring node from snapshot" chmod +x /opt/restore-from-snapshot.sh From 7cc335f221dd9d055c3b399c864ca6309f07207d Mon Sep 17 00:00:00 2001 From: Nikolay Vlasov Date: Fri, 24 May 2024 20:06:59 +1000 Subject: [PATCH 25/75] Base. Added Instance Store volume option for storage --- lib/base/README.md | 43 ++- lib/base/app.ts | 20 ++ .../assets/setup-instance-store-volumes.sh | 39 +++ lib/base/lib/assets/user-data/node.sh | 54 ++-- lib/base/lib/config/baseConfig.interface.ts | 6 + lib/base/lib/config/baseConfig.ts | 6 + .../node-cw-dashboard.ts | 0 lib/base/lib/ha-nodes-stack.ts | 141 +++++++++ lib/base/lib/single-node-stack.ts | 2 +- lib/base/sample-configs/.env-sample-archive | 11 +- lib/base/sample-configs/.env-sample-full | 9 +- lib/base/single-archive-node-deploy.json | 5 + lib/base/test/.env-test | 10 +- lib/base/test/base-single-node.test.ts | 12 +- lib/base/test/ha-nodes-stack.test.ts | 273 ++++++++++++++++++ 15 files changed, 597 insertions(+), 34 deletions(-) create mode 100644 lib/base/lib/assets/setup-instance-store-volumes.sh rename lib/base/lib/{assets => constructs}/node-cw-dashboard.ts (100%) create mode 100644 lib/base/lib/ha-nodes-stack.ts create mode 100644 lib/base/single-archive-node-deploy.json create mode 100644 lib/base/test/ha-nodes-stack.test.ts diff --git a/lib/base/README.md b/lib/base/README.md index 7e9161a0..a54ea52a 100644 --- a/lib/base/README.md +++ b/lib/base/README.md @@ -122,7 +122,7 @@ npx cdk deploy base-common > cdk bootstrap aws://ACCOUNT-NUMBER/REGION > ``` -### From your Cloud9: Deploy Single Node +### Option 1: Deploy Single Node 1. For L1 node you you can set your own URLs in `BASE_L1_EXECUTION_ENDPOINT` and `BASE_L1_CONSENSUS_ENDPOINT` properties of `.env` file. It can be one of [the providers recommended by Base](https://docs.base.org/tools/node-providers) or you can run your own Ethereum node [with Node Runner Ethereum blueprint](https://aws-samples.github.io/aws-blockchain-node-runners/docs/Blueprints/Ethereum) (tested with geth-lighthouse combination). For example: @@ -163,12 +163,48 @@ aws ssm start-session --target $INSTANCE_ID --region $AWS_REGION curl -s -X POST -H "Content-Type: application/json" --data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' http://localhost:8545 ``` +### Option 2: Highly Available RPC Nodes + +1. For L1 node you you can set your own URLs in `BASE_L1_EXECUTION_ENDPOINT` and `BASE_L1_CONSENSUS_ENDPOINT` properties of `.env` file. It can be one of [the providers recommended by Base](https://docs.base.org/tools/node-providers) or you can run your own Ethereum node [with Node Runner Ethereum blueprint](https://aws-samples.github.io/aws-blockchain-node-runners/docs/Blueprints/Ethereum) (tested with geth-lighthouse combination). For example: + +```bash +#For Sepolia: +BASE_L1_EXECUTION_ENDPOINT="https://ethereum-sepolia-rpc.publicnode.com" +BASE_L1_CONSENSUS_ENDPOINT="https://ethereum-sepolia-beacon-api.publicnode.com" +``` + +2. Deploy Base RPC Node and wait for it to sync. For Mainnet it might a day when using snapshots or about a week if syncing from block 0. You can use snapshots provided by the Base team by setting `BASE_RESTORE_FROM_SNAPSHOT="true"` in `.env` file. + + ```bash + pwd + # Make sure you are in aws-blockchain-node-runners/lib/base + npx cdk deploy base-ha-nodes --json --outputs-file ha-nodes-deploy.json + ``` + +2. Give the new RPC **full** nodes about 2-3 hours (24 hours for **archive** node) to initialize and then run the following query against the load balancer behind the RPC node created + + ```bash + export RPC_ALB_URL=$(cat ha-nodes-deploy.json | jq -r '..|.alburl? | select(. != null)') + echo $RPC_ALB_URL + ``` + + Periodically check [Geth Syncing Status](https://geth.ethereum.org/docs/fundamentals/logs#syncing). Run the following query from within the same VPC and against the private IP of the load balancer fronting your nodes: + + ```bash + curl http://$RPC_ALB_URL:8545 -X POST -H "Content-Type: application/json" \ + --data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' + ``` + +**NOTE:** By default and for security reasons the load balancer is available only from within the default VPC in the region where it is deployed. It is not available from the Internet and is not open for external connections. Before opening it up please make sure you protect your RPC APIs. + ### Monitoring -Every 5 minutes a script on the Base node publishes to CloudWatch service the metrics for current block for L1/L2 clients as well as blocks behind metric for L1 and minutes buehind for L2. When the node is fully synced the blocks behind metric should get to 4 and minutes behind should get down to 0. To see the metrics: +Every 5 minutes a script on the Base node publishes to CloudWatch service the metrics for current block for L1/L2 clients as well as blocks behind metric for L1 and minutes behind for L2. When the node is fully synced the blocks behind metric should get to 4 and minutes behind should get down to 0. To see the metrics for **single node only**: - Navigate to CloudWatch service (make sure you are in the region you have specified for AWS_REGION) - Open Dashboards and select `base-single-node--` from the list of dashboards. +Metrics for **ha nodes** configuration is not yet implemented (contributions are welcome!) + ## From your Cloud9: Clear up and undeploy everything 1. Undeploy all Nodes and Common stacks @@ -184,6 +220,9 @@ pwd # Undeploy Single Node npx cdk destroy base-single-node +# Undeploy HA Nodes +npx cdk destroy base-ha-nodes + # Delete all common components like IAM role and Security Group npx cdk destroy base-common ``` diff --git a/lib/base/app.ts b/lib/base/app.ts index 94b55bba..ca401786 100644 --- a/lib/base/app.ts +++ b/lib/base/app.ts @@ -5,6 +5,7 @@ import * as cdk from 'aws-cdk-lib'; import * as config from "./lib/config/baseConfig"; import {BaseCommonStack} from "./lib/common-stack"; import {BaseSingleNodeStack} from "./lib/single-node-stack"; +import {BaseHANodesStack} from "./lib/ha-nodes-stack"; const app = new cdk.App(); cdk.Tags.of(app).add("Project", "AWSBase"); @@ -28,3 +29,22 @@ new BaseSingleNodeStack(app, "base-single-node", { snapshotUrl: config.baseNodeConfig.snapshotUrl, dataVolume: config.baseNodeConfig.dataVolume, }); + +new BaseHANodesStack(app, "base-ha-nodes", { + stackName: `base-ha-nodes-${config.baseNodeConfig.baseNodeConfiguration}-${config.baseNodeConfig.baseNetworkId}`, + env: { account: config.baseConfig.accountId, region: config.baseConfig.region }, + + instanceType: config.baseNodeConfig.instanceType, + instanceCpuType: config.baseNodeConfig.instanceCpuType, + baseNetworkId: config.baseNodeConfig.baseNetworkId, + baseNodeConfiguration: config.baseNodeConfig.baseNodeConfiguration, + restoreFromSnapshot: config.baseNodeConfig.restoreFromSnapshot, + l1ExecutionEndpoint: config.baseNodeConfig.l1ExecutionEndpoint, + l1ConsensusEndpoint: config.baseNodeConfig.l1ConsensusEndpoint, + snapshotUrl: config.baseNodeConfig.snapshotUrl, + dataVolume: config.baseNodeConfig.dataVolume, + + albHealthCheckGracePeriodMin: config.haNodeConfig.albHealthCheckGracePeriodMin, + heartBeatDelayMin: config.haNodeConfig.heartBeatDelayMin, + numberOfNodes: config.haNodeConfig.numberOfNodes +}); diff --git a/lib/base/lib/assets/setup-instance-store-volumes.sh b/lib/base/lib/assets/setup-instance-store-volumes.sh new file mode 100644 index 00000000..934d19fb --- /dev/null +++ b/lib/base/lib/assets/setup-instance-store-volumes.sh @@ -0,0 +1,39 @@ +#!/bin/bash + +source /etc/environment + +if [[ "$DATA_VOLUME_TYPE" == "instance-store" ]]; then + echo "Data volume type is instance store" + export DATA_VOLUME_ID=/dev/$(lsblk -lnb | awk 'max < $4 {max = $4; vol = $1} END {print vol}') +fi + +if [ -n "$DATA_VOLUME_ID" ]; then + if [ $(df --output=target | grep -c "/data") -lt 1 ]; then + echo "Checking fstab for Data volume" + + mkfs.ext4 $DATA_VOLUME_ID + echo "Data volume formatted. Mounting..." + # Waiting wihtouht using sleep as it sometimes just hangs.... + coproc read -t 10 && wait "$!" || true + DATA_VOLUME_UUID=$(lsblk -fn -o UUID $DATA_VOLUME_ID) + DATA_VOLUME_FSTAB_CONF="UUID=$DATA_VOLUME_UUID /data ext4 defaults 0 2" + echo "DATA_VOLUME_ID="$DATA_VOLUME_ID + echo "DATA_VOLUME_UUID="$DATA_VOLUME_UUID + echo "DATA_VOLUME_FSTAB_CONF="$DATA_VOLUME_FSTAB_CONF + + # Check if data disc is already in fstab and replace the line if it is with the new disc UUID + if [ $(grep -c "data" /etc/fstab) -gt 0 ]; then + SED_REPLACEMENT_STRING="$(grep -n "/data" /etc/fstab | cut -d: -f1)s#.*#$DATA_VOLUME_FSTAB_CONF#" + cp /etc/fstab /etc/fstab.bak + sed -i "$SED_REPLACEMENT_STRING" /etc/fstab + else + echo $DATA_VOLUME_FSTAB_CONF | sudo tee -a /etc/fstab + fi + + sudo mount -a + + chown bcuser:bcuser -R /data + else + echo "Data volume is mounted, nothing changed" + fi +fi \ No newline at end of file diff --git a/lib/base/lib/assets/user-data/node.sh b/lib/base/lib/assets/user-data/node.sh index 00bf8c73..9c6b46f5 100644 --- a/lib/base/lib/assets/user-data/node.sh +++ b/lib/base/lib/assets/user-data/node.sh @@ -6,10 +6,12 @@ LIFECYCLE_HOOK_NAME=${_LIFECYCLE_HOOK_NAME_} AUTOSCALING_GROUP_NAME=${_AUTOSCALING_GROUP_NAME_} RESOURCE_ID=${_NODE_CF_LOGICAL_ID_} ASSETS_S3_PATH=${_ASSETS_S3_PATH_} +DATA_VOLUME_TYPE=${_DATA_VOLUME_TYPE_} { echo "LIFECYCLE_HOOK_NAME=$LIFECYCLE_HOOK_NAME" echo "AUTOSCALING_GROUP_NAME=$AUTOSCALING_GROUP_NAME" echo "ASSETS_S3_PATH=$ASSETS_S3_PATH" + echo "DATA_VOLUME_TYPE=$DATA_VOLUME_TYPE" } >> /etc/environment arch=$(uname -m) @@ -34,6 +36,7 @@ yum -y install amazon-cloudwatch-agent collectd jq yq gcc ncurses-devel aws-cfn- wget $YQ_URI -O /usr/bin/yq && chmod +x /usr/bin/yq # install aria2 a p2p downloader +cd /tmp if [ "$arch" == "x86_64" ]; then wget https://github.com/q3aql/aria2-static-builds/releases/download/v1.36.0/aria2-1.36.0-linux-gnu-64bit-build1.tar.bz2 @@ -235,37 +238,48 @@ ExecStart=/home/bcuser/bin/node.sh WantedBy=multi-user.target EOF' -echo "Signaling completion to CloudFormation to continue with volume mount" -/opt/aws/bin/cfn-signal --stack $STACK_NAME --resource $RESOURCE_ID --region $REGION +if [[ "$LIFECYCLE_HOOK_NAME" == "none" ]]; then + echo "We run single node setup. Signaling completion to CloudFormation to continue with volume mount" + /opt/aws/bin/cfn-signal --stack $STACK_NAME --resource $RESOURCE_ID --region $REGION +fi echo "Preparing data volume" -echo "Wait for one minute for the volume to be available" -sleep 60 - -if $(lsblk | grep -q nvme1n1); then - echo "nvme1n1 is found. Configuring attached storage" +echo "Wait for one minute for the volume to become available" +sleep 60s - if [ "$FORMAT_DISK" == "false" ]; then - echo "Not creating a new filesystem in the disk. Existing data might be present!!" - else - mkfs -t ext4 /dev/nvme1n1 - fi +if [[ "$DATA_VOLUME_TYPE" == "instance-store" ]]; then + echo "Data volume type is instance store" - sleep 10 - # Define the line to add to fstab - uuid=$(lsblk -n -o UUID /dev/nvme1n1) - line="UUID=$uuid /data ext4 defaults 0 2" + cd /opt + chmod +x /opt/setup-instance-store-volumes.sh - # Write the line to fstab - echo $line | sudo tee -a /etc/fstab + (crontab -l; echo "@reboot /opt/setup-instance-store-volumes.sh >/tmp/setup-instance-store-volumes.log 2>&1") | crontab - + crontab -l - mount -a + DATA_VOLUME_ID=/dev/$(lsblk -lnb | awk 'max < $4 {max = $4; vol = $1} END {print vol}') else - echo "nvme1n1 is not found. Not doing anything" + echo "Data volume type is EBS" + + DATA_VOLUME_ID=/dev/$(lsblk -lnb | awk -v VOLUME_SIZE_BYTES="$DATA_VOLUME_SIZE" '{if ($4== VOLUME_SIZE_BYTES) {print $1}}') fi +mkfs -t ext4 $DATA_VOLUME_ID +echo "waiting for volume to get UUID" + OUTPUT=0; + while [ "$OUTPUT" = 0 ]; do + DATA_VOLUME_UUID=$(lsblk -fn -o UUID $DATA_VOLUME_ID) + OUTPUT=$(echo $DATA_VOLUME_UUID | grep -c - $2) + echo $OUTPUT + done +DATA_VOLUME_FSTAB_CONF="UUID=$DATA_VOLUME_UUID /data ext4 defaults 0 2" +echo "DATA_VOLUME_ID="$DATA_VOLUME_ID +echo "DATA_VOLUME_UUID="$DATA_VOLUME_UUID +echo "DATA_VOLUME_FSTAB_CONF="$DATA_VOLUME_FSTAB_CONF +echo $DATA_VOLUME_FSTAB_CONF | tee -a /etc/fstab +mount -a + lsblk -d chown -R bcuser:bcuser /data diff --git a/lib/base/lib/config/baseConfig.interface.ts b/lib/base/lib/config/baseConfig.interface.ts index e7e327a2..dc2f2300 100644 --- a/lib/base/lib/config/baseConfig.interface.ts +++ b/lib/base/lib/config/baseConfig.interface.ts @@ -23,3 +23,9 @@ export interface BaseBaseNodeConfig extends configTypes.BaseNodeConfig { l1ConsensusEndpoint: string; snapshotUrl: string; } + +export interface BaseHAConfig { + albHealthCheckGracePeriodMin: number; + heartBeatDelayMin: number; + numberOfNodes: number; +} \ No newline at end of file diff --git a/lib/base/lib/config/baseConfig.ts b/lib/base/lib/config/baseConfig.ts index 4da2e404..26727b08 100644 --- a/lib/base/lib/config/baseConfig.ts +++ b/lib/base/lib/config/baseConfig.ts @@ -39,3 +39,9 @@ export const baseNodeConfig: configTypes.BaseBaseNodeConfig = { throughput: process.env.BASE_DATA_VOL_THROUGHPUT ? parseInt(process.env.BASE_DATA_VOL_THROUGHPUT): 700, }, }; + +export const haNodeConfig: configTypes.BaseHAConfig = { + albHealthCheckGracePeriodMin: process.env.BASE_HA_ALB_HEALTHCHECK_GRACE_PERIOD_MIN ? parseInt(process.env.BASE_HA_ALB_HEALTHCHECK_GRACE_PERIOD_MIN) : 10, + heartBeatDelayMin: process.env.BASE_HA_NODES_HEARTBEAT_DELAY_MIN ? parseInt(process.env.BASE_HA_NODES_HEARTBEAT_DELAY_MIN) : 40, + numberOfNodes: process.env.BASE_HA_NUMBER_OF_NODES ? parseInt(process.env.BASE_HA_NUMBER_OF_NODES) : 2 +}; diff --git a/lib/base/lib/assets/node-cw-dashboard.ts b/lib/base/lib/constructs/node-cw-dashboard.ts similarity index 100% rename from lib/base/lib/assets/node-cw-dashboard.ts rename to lib/base/lib/constructs/node-cw-dashboard.ts diff --git a/lib/base/lib/ha-nodes-stack.ts b/lib/base/lib/ha-nodes-stack.ts new file mode 100644 index 00000000..d876281a --- /dev/null +++ b/lib/base/lib/ha-nodes-stack.ts @@ -0,0 +1,141 @@ +import * as cdk from "aws-cdk-lib"; +import * as cdkConstructs from "constructs"; +import * as ec2 from "aws-cdk-lib/aws-ec2"; +import * as iam from "aws-cdk-lib/aws-iam"; +import { AmazonLinuxGeneration, AmazonLinuxImage } from "aws-cdk-lib/aws-ec2"; +import * as s3Assets from "aws-cdk-lib/aws-s3-assets"; +import * as configTypes from "./config/baseConfig.interface"; +import { BaseNodeSecurityGroupConstruct } from "./constructs/base-node-security-group"; +import * as fs from "fs"; +import * as path from "path"; +import * as constants from "../../constructs/constants"; +import { HANodesConstruct } from "../../constructs/ha-rpc-nodes-with-alb"; +import * as nag from "cdk-nag"; + +export interface BaseHANodesStackProps extends cdk.StackProps { + instanceType: ec2.InstanceType; + instanceCpuType: ec2.AmazonLinuxCpuType; + baseNetworkId: configTypes.BaseNetworkId; + baseNodeConfiguration: configTypes.BaseNodeConfiguration; + restoreFromSnapshot: boolean; + l1ExecutionEndpoint: string, + l1ConsensusEndpoint: string, + snapshotUrl: string, + dataVolume: configTypes.BaseDataVolumeConfig; + albHealthCheckGracePeriodMin: number; + heartBeatDelayMin: number; + numberOfNodes: number; +} + +export class BaseHANodesStack extends cdk.Stack { + constructor(scope: cdkConstructs.Construct, id: string, props: BaseHANodesStackProps) { + super(scope, id, props); + + const REGION = cdk.Stack.of(this).region; + const STACK_NAME = cdk.Stack.of(this).stackName; + const lifecycleHookName = STACK_NAME; + const autoScalingGroupName = STACK_NAME; + + const { + instanceType, + instanceCpuType, + baseNetworkId, + baseNodeConfiguration, + restoreFromSnapshot, + l1ExecutionEndpoint, + l1ConsensusEndpoint, + dataVolume, + albHealthCheckGracePeriodMin, + heartBeatDelayMin, + numberOfNodes + } = props; + + // using default vpc + const vpc = ec2.Vpc.fromLookup(this, "vpc", { isDefault: true }); + + // setting up the security group for the node from BSC-specific construct + const instanceSG = new BaseNodeSecurityGroupConstruct(this, "security-group", { vpc: vpc }); + + // getting the IAM Role ARM from the common stack + const importedInstanceRoleArn = cdk.Fn.importValue("BaseNodeInstanceRoleArn"); + + const instanceRole = iam.Role.fromRoleArn(this, "iam-role", importedInstanceRoleArn); + + // making our scripts and configs from the local "assets" directory available for instance to download + const asset = new s3Assets.Asset(this, "assets", { + path: path.join(__dirname, "assets") + }); + + asset.bucket.grantRead(instanceRole); + + // parsing user data script and injecting necessary variables + const nodeScript = fs.readFileSync(path.join(__dirname, "assets", "user-data", "node.sh")).toString(); + const dataVolumeSizeBytes = dataVolume.sizeGiB * constants.GibibytesToBytesConversionCoefficient; + + const modifiedInitNodeScript = cdk.Fn.sub(nodeScript, { + _REGION_: REGION, + _ASSETS_S3_PATH_: `s3://${asset.s3BucketName}/${asset.s3ObjectKey}`, + _STACK_NAME_: STACK_NAME, + _NODE_CF_LOGICAL_ID_: constants.NoneValue, + _DATA_VOLUME_TYPE_: dataVolume.type, + _DATA_VOLUME_SIZE_: dataVolumeSizeBytes.toString(), + _NETWORK_ID_: baseNetworkId, + _NODE_CONFIG_: baseNodeConfiguration, + _LIFECYCLE_HOOK_NAME_: lifecycleHookName, + _AUTOSCALING_GROUP_NAME_: autoScalingGroupName, + _RESTORE_FROM_SNAPSHOT_: restoreFromSnapshot.toString(), + _FORMAT_DISK_: "true", + _L1_EXECUTION_ENDPOINT_: l1ExecutionEndpoint, + _L1_CONSENSUS_ENDPOINT_: l1ConsensusEndpoint, + _SNAPSHOT_URL_: props.snapshotUrl, + }); + + const rpcNodes = new HANodesConstruct(this, "rpc-nodes", { + instanceType, + dataVolumes: [dataVolume], + machineImage: new ec2.AmazonLinuxImage({ + generation: AmazonLinuxGeneration.AMAZON_LINUX_2, + kernel:ec2.AmazonLinuxKernel.KERNEL5_X, + cpuType: instanceCpuType + }), + role: instanceRole, + vpc, + securityGroup: instanceSG.securityGroup, + userData: modifiedInitNodeScript, + numberOfNodes, + rpcPortForALB: 8545, + albHealthCheckGracePeriodMin, + heartBeatDelayMin, + lifecycleHookName: lifecycleHookName, + autoScalingGroupName: autoScalingGroupName + }); + + + + new cdk.CfnOutput(this, "alb-url", { value: rpcNodes.loadBalancerDnsName }); + + // Adding suppressions to the stack + nag.NagSuppressions.addResourceSuppressions( + this, + [ + { + id: "AwsSolutions-AS3", + reason: "No notifications needed" + }, + { + id: "AwsSolutions-S1", + reason: "No access log needed for ALB logs bucket" + }, + { + id: "AwsSolutions-EC28", + reason: "Using basic monitoring to save costs" + }, + { + id: "AwsSolutions-IAM5", + reason: "Need read access to the S3 bucket with assets" + } + ], + true + ); + } +} diff --git a/lib/base/lib/single-node-stack.ts b/lib/base/lib/single-node-stack.ts index f47e7ac4..8c6eda38 100644 --- a/lib/base/lib/single-node-stack.ts +++ b/lib/base/lib/single-node-stack.ts @@ -5,7 +5,7 @@ import * as iam from "aws-cdk-lib/aws-iam"; import * as s3Assets from "aws-cdk-lib/aws-s3-assets"; import * as path from "path"; import * as fs from "fs"; -import * as nodeCwDashboard from "./assets/node-cw-dashboard" +import * as nodeCwDashboard from "./constructs/node-cw-dashboard" import * as cw from 'aws-cdk-lib/aws-cloudwatch'; import * as nag from "cdk-nag"; import { SingleNodeConstruct } from "../../constructs/single-node" diff --git a/lib/base/sample-configs/.env-sample-archive b/lib/base/sample-configs/.env-sample-archive index 12d99bea..b8f8efbd 100644 --- a/lib/base/sample-configs/.env-sample-archive +++ b/lib/base/sample-configs/.env-sample-archive @@ -8,8 +8,8 @@ AWS_REGION="us-east-1" ## Common configuration parameters ## BASE_NETWORK_ID="mainnet" # All options: "mainnet", "sepolia" -BASE_NODE_CONFIGURATION="archive" # All options: "full", "archive" -BASE_INSTANCE_TYPE="m7g.2xlarge" +BASE_NODE_CONFIGURATION="archive" # All options: "full", "archive" +BASE_INSTANCE_TYPE="m7g.2xlarge" # Reconneded for Insance Store: i3en.3xlarge, "x86_64" BASE_CPU_TYPE="ARM_64" # All options: "x86_64", "ARM_64". IMPORTANT: Make sure the CPU type matches the instance type used # Data volume configuration @@ -17,10 +17,10 @@ BASE_DATA_VOL_TYPE="gp3" # Other options: "io1" | "io2" | "gp3" | BASE_DATA_VOL_SIZE="7200" # Current required data size in GB to keep both snapshot archive and unarchived version of it. For Sepolia 1000 will be sufficient. BASE_DATA_VOL_IOPS="5000" # Max IOPS for EBS volumes (not applicable for "instance-store") BASE_DATA_VOL_THROUGHPUT="700" # Max throughput for EBS gp3 volumes (not applicable for "io1" | "io2" | "instance-store") -BASE_RESTORE_FROM_SNAPSHOT="true" # Download snapshot to speed up statup time BASE_L1_EXECUTION_ENDPOINT="https://ethereum-sepolia-rpc.publicnode.com" # Set your own URL to Ethereum L1 node: https://docs.base.org/tools/node-providers BASE_L1_CONSENSUS_ENDPOINT="https://ethereum-sepolia-beacon-api.publicnode.com" +BASE_RESTORE_FROM_SNAPSHOT="true" # Download snapshot to speed up statup time BASE_SNAPSHOT_URL="none" # Optionally provide the URL to download snpashot: https://docs.base.org/tutorials/run-a-base-node/#snapshots # Example for Sepolia: @@ -29,3 +29,8 @@ BASE_SNAPSHOT_URL="none" # Optionally provide the URL to download # Example for Mainnet and with Ethereum Blueprint with Geth-Lighthouse client combination and private IP: #BASE_L1_EXECUTION_ENDPOINT=http://172.31.15.220:8545 #BASE_L1_CONSENSUS_ENDPOINT=http://172.31.15.220:5052 + +## HA nodes configuration ## +BASE_HA_NUMBER_OF_NODES="2" # Total number of RPC nodes to be provisioned. Default: 2 +BASE_HA_ALB_HEALTHCHECK_GRACE_PERIOD_MIN="300" # Time enough to initialize the instance +BASE_HA_NODES_HEARTBEAT_DELAY_MIN="1440" # Time sufficient enough for a node do sync \ No newline at end of file diff --git a/lib/base/sample-configs/.env-sample-full b/lib/base/sample-configs/.env-sample-full index a99c26b3..0449d94a 100644 --- a/lib/base/sample-configs/.env-sample-full +++ b/lib/base/sample-configs/.env-sample-full @@ -9,7 +9,7 @@ AWS_REGION="us-east-1" ## Common configuration parameters ## BASE_NETWORK_ID="mainnet" # All options: "mainnet", "sepolia" BASE_NODE_CONFIGURATION="full" # All options: "full", "archive" -BASE_INSTANCE_TYPE="m7g.2xlarge" +BASE_INSTANCE_TYPE="m7g.2xlarge" # Reconneded for Insance Store: i3en.3xlarge, "x86_64" BASE_CPU_TYPE="ARM_64" # All options: "x86_64", "ARM_64". IMPORTANT: Make sure the CPU type matches the instance type used # Data volume configuration @@ -17,10 +17,10 @@ BASE_DATA_VOL_TYPE="gp3" # Other options: "io1" | "io2" | "gp3" | BASE_DATA_VOL_SIZE="4000" # Current required data size in GB to keep both snapshot archive and unarchived version of it. For Sepolia 1000 will be sufficient. BASE_DATA_VOL_IOPS="5000" # Max IOPS for EBS volumes (not applicable for "instance-store") BASE_DATA_VOL_THROUGHPUT="700" # Max throughput for EBS gp3 volumes (not applicable for "io1" | "io2" | "instance-store") -BASE_RESTORE_FROM_SNAPSHOT="true" # Download snapshot to speed up statup time BASE_L1_EXECUTION_ENDPOINT="https://ethereum-sepolia-rpc.publicnode.com" # Set your own URL to Ethereum L1 node: https://docs.base.org/tools/node-providers BASE_L1_CONSENSUS_ENDPOINT="https://ethereum-sepolia-beacon-api.publicnode.com" +BASE_RESTORE_FROM_SNAPSHOT="true" # Download snapshot to speed up statup time BASE_SNAPSHOT_URL="none" # Optionally provide the URL to download snpashot: https://docs.base.org/tutorials/run-a-base-node/#snapshots # Example for Sepolia: @@ -29,3 +29,8 @@ BASE_SNAPSHOT_URL="none" # Optionally provide the URL to download # Example for Mainnet and with Ethereum Blueprint with Geth-Lighthouse client combination and private IP: #BASE_L1_EXECUTION_ENDPOINT=http://172.31.15.220:8545 #BASE_L1_CONSENSUS_ENDPOINT=http://172.31.15.220:5052 + +## HA nodes configuration ## +BASE_HA_NUMBER_OF_NODES="2" # Total number of RPC nodes to be provisioned. Default: 2 +BASE_HA_ALB_HEALTHCHECK_GRACE_PERIOD_MIN="300" # Time enough to initialize the instance +BASE_HA_NODES_HEARTBEAT_DELAY_MIN="120" # Time sufficient enough for a node do sync \ No newline at end of file diff --git a/lib/base/single-archive-node-deploy.json b/lib/base/single-archive-node-deploy.json new file mode 100644 index 00000000..98a02002 --- /dev/null +++ b/lib/base/single-archive-node-deploy.json @@ -0,0 +1,5 @@ +{ + "base-single-node-archive-mainnet": { + "nodeinstanceid": "i-0206eadc184b36db5" + } +} diff --git a/lib/base/test/.env-test b/lib/base/test/.env-test index f9a14ccd..46c9f3f7 100644 --- a/lib/base/test/.env-test +++ b/lib/base/test/.env-test @@ -8,10 +8,13 @@ AWS_REGION="us-east-1" # Regions supported by Amazon Ma ## Common configuration parameters ## BASE_NETWORK_ID="mainnet" # All options: "mainnet" +BASE_NODE_CONFIGURATION="full" # All options: "full", "archive" +BASE_INSTANCE_TYPE="m7g.4xlarge" +BASE_CPU_TYPE="ARM_64" # All options: "x86_64", "ARM_64". IMPORTANT: Make sure the CPU type matches the instance type used # Data volume configuration BASE_DATA_VOL_TYPE="gp3" # Other options: "io1" | "io2" | "gp3" | "instance-store" . IMPORTANT: Use "instance-store" option only with instance types that support that feature, like popular for node im4gn, d3, i3en, and i4i instance families -BASE_DATA_VOL_SIZE="5100" # Current required data size in GB to keep both snapshot archive and unarchived version of it. For Sepolia 1000 will be sufficient. +BASE_DATA_VOL_SIZE="7200" # Current required data size in GB to keep both snapshot archive and unarchived version of it. For Sepolia 1000 will be sufficient. BASE_DATA_VOL_IOPS="5000" # Max IOPS for EBS volumes (not applicable for "instance-store") BASE_DATA_VOL_THROUGHPUT="700" # Max throughput for EBS gp3 volumes (not applicable for "io1" | "io2" | "instance-store") BASE_RESTORE_FROM_SNAPSHOT="true" # Download snapshot to speed up statup time @@ -24,3 +27,8 @@ BASE_L1_CONSENSUS_ENDPOINT=https://ethereum-sepolia-beacon-api.publicnode.com # Example for Mainnet and with Ethereum Blueprint with Geth-Lighthouse client combination and private IP: #BASE_L1_EXECUTION_ENDPOINT=http://172.31.15.220:8545 #BASE_L1_CONSENSUS_ENDPOINT=http://172.31.15.220:5052 + +## HA nodes configuration ## +BASE_HA_NUMBER_OF_NODES="2" # Total number of RPC nodes to be provisioned. Default: 2 +BASE_HA_ALB_HEALTHCHECK_GRACE_PERIOD_MIN="300" # Time enough to initialize the instance +BASE_HA_NODES_HEARTBEAT_DELAY_MIN="120" # Time sufficient enough for a node do sync \ No newline at end of file diff --git a/lib/base/test/base-single-node.test.ts b/lib/base/test/base-single-node.test.ts index e945f320..9cbc5a39 100644 --- a/lib/base/test/base-single-node.test.ts +++ b/lib/base/test/base-single-node.test.ts @@ -15,15 +15,17 @@ describe("BaseSingleNodeStack", () => { // Create the BaseSingleNodeStack. baseSingleNodeStack = new BaseSingleNodeStack(app, "base-single-node", { - stackName: `base-single-node-${config.baseNodeConfig.baseNetworkId}`, + stackName: `base-single-node-${config.baseNodeConfig.baseNodeConfiguration}-${config.baseNodeConfig.baseNetworkId}`, env: { account: config.baseConfig.accountId, region: config.baseConfig.region }, - + instanceType: config.baseNodeConfig.instanceType, instanceCpuType: config.baseNodeConfig.instanceCpuType, baseNetworkId: config.baseNodeConfig.baseNetworkId, + baseNodeConfiguration: config.baseNodeConfig.baseNodeConfiguration, restoreFromSnapshot: config.baseNodeConfig.restoreFromSnapshot, l1ExecutionEndpoint: config.baseNodeConfig.l1ExecutionEndpoint, l1ConsensusEndpoint: config.baseNodeConfig.l1ConsensusEndpoint, + snapshotUrl: config.baseNodeConfig.snapshotUrl, dataVolume: config.baseNodeConfig.dataVolume, }); @@ -111,7 +113,7 @@ describe("BaseSingleNodeStack", () => { ], IamInstanceProfile: Match.anyValue(), ImageId: Match.anyValue(), - InstanceType: "m7g.2xlarge", + InstanceType: "m7g.4xlarge", Monitoring: true, PropagateTagsToVolumeOnCreation: true, SecurityGroupIds: Match.anyValue(), @@ -124,7 +126,7 @@ describe("BaseSingleNodeStack", () => { Encrypted: true, Iops: 5000, MultiAttachEnabled: false, - Size: 5100, + Size: 7200, Throughput: 700, VolumeType: "gp3" }) @@ -145,7 +147,7 @@ describe("BaseSingleNodeStack", () => { "Fn::Join": [ "", [ - "base-single-node-mainnet-", + "base-single-node-full-mainnet-", { "Ref": Match.anyValue() } diff --git a/lib/base/test/ha-nodes-stack.test.ts b/lib/base/test/ha-nodes-stack.test.ts new file mode 100644 index 00000000..f7d4ee8c --- /dev/null +++ b/lib/base/test/ha-nodes-stack.test.ts @@ -0,0 +1,273 @@ +import { Match, Template } from "aws-cdk-lib/assertions"; +import * as cdk from "aws-cdk-lib"; +import * as dotenv from 'dotenv'; +dotenv.config({ path: './test/.env-test' }); +import * as config from "../lib/config/baseConfig"; +import * as configTypes from "../lib/config/baseConfig.interface"; +import { BaseHANodesStack } from "../lib/ha-nodes-stack"; + +describe("BaseHANodesStack", () => { + test("synthesizes the way we expect", () => { + const app = new cdk.App(); + + // Create the BaseHANodesStack. + const baseHANodesStack = new BaseHANodesStack(app, "base-sync-node", { + stackName: `base-ha-nodes-${config.baseNodeConfig.baseNodeConfiguration}-${config.baseNodeConfig.baseNetworkId}`, + env: { account: config.baseConfig.accountId, region: config.baseConfig.region }, + + instanceType: config.baseNodeConfig.instanceType, + instanceCpuType: config.baseNodeConfig.instanceCpuType, + baseNetworkId: config.baseNodeConfig.baseNetworkId, + baseNodeConfiguration: config.baseNodeConfig.baseNodeConfiguration, + restoreFromSnapshot: config.baseNodeConfig.restoreFromSnapshot, + l1ExecutionEndpoint: config.baseNodeConfig.l1ExecutionEndpoint, + l1ConsensusEndpoint: config.baseNodeConfig.l1ConsensusEndpoint, + snapshotUrl: config.baseNodeConfig.snapshotUrl, + dataVolume: config.baseNodeConfig.dataVolume, + + albHealthCheckGracePeriodMin: config.haNodeConfig.albHealthCheckGracePeriodMin, + heartBeatDelayMin: config.haNodeConfig.heartBeatDelayMin, + numberOfNodes: config.haNodeConfig.numberOfNodes + }); + + // Prepare the stack for assertions. + const template = Template.fromStack(baseHANodesStack); + + // Has EC2 instance security group. + template.hasResourceProperties("AWS::EC2::SecurityGroup", { + GroupDescription: Match.anyValue(), + VpcId: Match.anyValue(), + SecurityGroupEgress: [ + { + "CidrIp": "0.0.0.0/0", + "Description": "All outbound connections except 13000", + "FromPort": 0, + "IpProtocol": "tcp", + "ToPort": 12999 + }, + { + "CidrIp": "0.0.0.0/0", + "Description": "All outbound connections except 13000", + "FromPort": 13001, + "IpProtocol": "tcp", + "ToPort": 65535 + }, + { + "CidrIp": "0.0.0.0/0", + "Description": "All outbound connections except 13000", + "FromPort": 0, + "IpProtocol": "udp", + "ToPort": 12999 + }, + { + "CidrIp": "0.0.0.0/0", + "Description": "All outbound connections except 13000", + "FromPort": 13001, + "IpProtocol": "udp", + "ToPort": 65535 + } + ], + SecurityGroupIngress: [ + { + "CidrIp": "0.0.0.0/0", + "Description": "P2P", + "FromPort": 9222, + "IpProtocol": "tcp", + "ToPort": 9222 + }, + { + "CidrIp": "0.0.0.0/0", + "Description": "P2P", + "FromPort": 9222, + "IpProtocol": "udp", + "ToPort": 9222 + }, + { + "CidrIp": "1.2.3.4/5", + "Description": "Base Client RPC", + "FromPort": 8545, + "IpProtocol": "tcp", + "ToPort": 8545 + }, + { + "Description": "Allow access from ALB to Blockchain Node", + "FromPort": 0, + "IpProtocol": "tcp", + "SourceSecurityGroupId": { + "Fn::GetAtt": [ + Match.anyValue(), + "GroupId" + ] + }, + "ToPort": 65535 + }, + ] + }) + + // Has security group from ALB to EC2. + template.hasResourceProperties("AWS::EC2::SecurityGroupIngress", { + Description: "Load balancer to target", + FromPort: 8545, + GroupId: Match.anyValue(), + IpProtocol: "tcp", + SourceSecurityGroupId: Match.anyValue(), + ToPort: 8545, + }) + + // Has launch template profile for EC2 instances. + template.hasResourceProperties("AWS::IAM::InstanceProfile", { + Roles: [Match.anyValue()] + }); + + // Has EC2 launch template. + template.hasResourceProperties("AWS::EC2::LaunchTemplate", { + LaunchTemplateData: { + BlockDeviceMappings: [ + { + "DeviceName": "/dev/xvda", + "Ebs": { + "DeleteOnTermination": true, + "Encrypted": true, + "Iops": 3000, + "Throughput": 125, + "VolumeSize": 46, + "VolumeType": "gp3" + } + }, + { + "DeviceName": "/dev/sdf", + "Ebs": { + "DeleteOnTermination": true, + "Encrypted": true, + "Iops": 5000, + "Throughput": 700, + "VolumeSize": 7200, + "VolumeType": "gp3" + } + } + ], + EbsOptimized: true, + IamInstanceProfile: Match.anyValue(), + ImageId: Match.anyValue(), + InstanceType:"m7g.4xlarge", + SecurityGroupIds: [Match.anyValue()], + UserData: Match.anyValue(), + TagSpecifications: Match.anyValue(), + } + }) + + // Has Auto Scaling Group. + template.hasResourceProperties("AWS::AutoScaling::AutoScalingGroup", { + AutoScalingGroupName: `base-ha-nodes-${config.baseNodeConfig.baseNodeConfiguration}-${config.baseNodeConfig.baseNetworkId}`, + HealthCheckGracePeriod: config.haNodeConfig.albHealthCheckGracePeriodMin * 60, + HealthCheckType: "ELB", + DefaultInstanceWarmup: 60, + MinSize: "0", + MaxSize: "4", + DesiredCapacity: config.haNodeConfig.numberOfNodes.toString(), + VPCZoneIdentifier: Match.anyValue(), + TargetGroupARNs: Match.anyValue(), + }); + + // Has Auto Scaling Lifecycle Hook. + template.hasResourceProperties("AWS::AutoScaling::LifecycleHook", { + DefaultResult: "ABANDON", + HeartbeatTimeout: config.haNodeConfig.heartBeatDelayMin * 60, + LifecycleHookName: `base-ha-nodes-${config.baseNodeConfig.baseNodeConfiguration}-${config.baseNodeConfig.baseNetworkId}`, + LifecycleTransition: "autoscaling:EC2_INSTANCE_LAUNCHING", + }); + + // Has Auto Scaling Security Group. + template.hasResourceProperties("AWS::EC2::SecurityGroup", { + GroupDescription: Match.anyValue(), + SecurityGroupEgress: [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + SecurityGroupIngress: [ + { + "CidrIp": "1.2.3.4/5", + "Description": "Blockchain Node RPC", + "FromPort": 8545, + "IpProtocol": "tcp", + "ToPort": 8545 + } + ], + VpcId: Match.anyValue(), + }); + + // Has ALB. + template.hasResourceProperties("AWS::ElasticLoadBalancingV2::LoadBalancer", { + LoadBalancerAttributes: [ + { + Key: "deletion_protection.enabled", + Value: "false" + }, + { + Key: "access_logs.s3.enabled", + Value: "true" + }, + { + Key: "access_logs.s3.bucket", + Value: Match.anyValue(), + }, + { + Key: "access_logs.s3.prefix", + Value: `base-ha-nodes-${config.baseNodeConfig.baseNodeConfiguration}-${config.baseNodeConfig.baseNetworkId}` + } + ], + Scheme: "internal", + SecurityGroups: [ + Match.anyValue() + ], + "Subnets": [ + Match.anyValue(), + Match.anyValue() + ], + Type: "application", + }); + + // Has ALB listener. + template.hasResourceProperties("AWS::ElasticLoadBalancingV2::Listener", { + "DefaultActions": [ + { + "TargetGroupArn": Match.anyValue(), + Type: "forward" + } + ], + LoadBalancerArn: Match.anyValue(), + Port: 8545, + Protocol: "HTTP" + }) + + // Has ALB target group. + template.hasResourceProperties("AWS::ElasticLoadBalancingV2::TargetGroup", { + HealthCheckEnabled: true, + HealthCheckIntervalSeconds: 30, + HealthCheckPath: "/", + HealthCheckPort: "8545", + HealthyThresholdCount: 3, + Matcher: { + HttpCode: "200-299" + }, + Port: 8545, + Protocol: "HTTP", + TargetGroupAttributes: [ + { + Key: "deregistration_delay.timeout_seconds", + Value: "30" + }, + { + Key: "stickiness.enabled", + Value: "false" + } + ], + TargetType: "instance", + UnhealthyThresholdCount: 2, + VpcId: Match.anyValue(), + }) + }); +}); From 5f0b09fb4adb4143d2e477d99c5a239f8968037f Mon Sep 17 00:00:00 2001 From: Nikolay Vlasov Date: Mon, 27 May 2024 15:00:44 +1000 Subject: [PATCH 26/75] Base. Added alternative snapshot downloads from S3 URL. --- .../restore-from-snapshot-archive-s3.sh | 31 +++++++++++++++++++ ...pshot.sh => restore-from-snapshot-http.sh} | 1 - .../assets/setup-instance-store-volumes.sh | 9 ++++-- lib/base/lib/assets/user-data/node.sh | 23 +++++++++++--- lib/base/single-archive-node-deploy.json | 2 +- 5 files changed, 57 insertions(+), 9 deletions(-) create mode 100644 lib/base/lib/assets/restore-from-snapshot-archive-s3.sh rename lib/base/lib/assets/{restore-from-snapshot.sh => restore-from-snapshot-http.sh} (97%) diff --git a/lib/base/lib/assets/restore-from-snapshot-archive-s3.sh b/lib/base/lib/assets/restore-from-snapshot-archive-s3.sh new file mode 100644 index 00000000..16473ea6 --- /dev/null +++ b/lib/base/lib/assets/restore-from-snapshot-archive-s3.sh @@ -0,0 +1,31 @@ +#!/bin/bash +set +e + +source /etc/environment + +echo "Downloading Snapshot." + +cd /data + +SNAPSHOT_FILE_NAME=snapshot.tar.gz +SNAPSHOT_DIR=/data + +LATEST_SNAPSHOT_FILE_NAME=$(curl https://$NETWORK_ID-$NODE_CONFIG-snapshots.base.org/latest) && \ +s5cmd --log error cp s3://base-snapshots-$NETWORK_ID-archive/$LATEST_SNAPSHOT_FILE_NAME /data && \ +echo "Downloading Snapshot script finished" && \ +sleep 60 &&\ +echo "Starting snapshot decompression ..." && \ +tar -zxvf $SNAPSHOT_DIR/$SNAPSHOT_FILE_NAME -C /data 2>&1 | tee unzip.log && echo "decompresed successfully..." || echo "decompression failed..." >> snapshots-decompression.log + +echo "Decompresed snapshot, cleaning up..." + +mv /data/snapshots/$NETWORK_ID/download/* /data && \ +rm -rf /data/snapshots && \ +rm -rf $SNAPSHOT_DIR/$SNAPSHOT_FILE_NAME + +echo "Snapshot is ready, starting the service.." + +chown -R bcuser:bcuser /data + +sudo systemctl daemon-reload +sudo systemctl enable --now base \ No newline at end of file diff --git a/lib/base/lib/assets/restore-from-snapshot.sh b/lib/base/lib/assets/restore-from-snapshot-http.sh similarity index 97% rename from lib/base/lib/assets/restore-from-snapshot.sh rename to lib/base/lib/assets/restore-from-snapshot-http.sh index c73bb927..018974eb 100644 --- a/lib/base/lib/assets/restore-from-snapshot.sh +++ b/lib/base/lib/assets/restore-from-snapshot-http.sh @@ -16,7 +16,6 @@ if [ "$SNAPSHOT_URL" == "none" ] || [ -z "${SNAPSHOT_URL}" ]; then SNAPSHOT_URL=https://$NETWORK_ID-$NODE_CONFIG-snapshots.base.org/$LATEST_SNAPSHOT_FILE_NAME fi -# take about 1 hour to download the Snapshot while (( SNAPSHOT_DOWNLOAD_STATUS != 0 )) do PIDS=$(pgrep aria2c) diff --git a/lib/base/lib/assets/setup-instance-store-volumes.sh b/lib/base/lib/assets/setup-instance-store-volumes.sh index 934d19fb..0b919b61 100644 --- a/lib/base/lib/assets/setup-instance-store-volumes.sh +++ b/lib/base/lib/assets/setup-instance-store-volumes.sh @@ -13,8 +13,13 @@ if [ -n "$DATA_VOLUME_ID" ]; then mkfs.ext4 $DATA_VOLUME_ID echo "Data volume formatted. Mounting..." - # Waiting wihtouht using sleep as it sometimes just hangs.... - coproc read -t 10 && wait "$!" || true + echo "waiting for volume to get UUID" + OUTPUT=0; + while [ "$OUTPUT" = 0 ]; do + DATA_VOLUME_UUID=$(lsblk -fn -o UUID $DATA_VOLUME_ID) + OUTPUT=$(echo $DATA_VOLUME_UUID | grep -c - $2) + echo $OUTPUT + done DATA_VOLUME_UUID=$(lsblk -fn -o UUID $DATA_VOLUME_ID) DATA_VOLUME_FSTAB_CONF="UUID=$DATA_VOLUME_UUID /data ext4 defaults 0 2" echo "DATA_VOLUME_ID="$DATA_VOLUME_ID diff --git a/lib/base/lib/assets/user-data/node.sh b/lib/base/lib/assets/user-data/node.sh index 9c6b46f5..99575bae 100644 --- a/lib/base/lib/assets/user-data/node.sh +++ b/lib/base/lib/assets/user-data/node.sh @@ -35,7 +35,7 @@ yum update -y yum -y install amazon-cloudwatch-agent collectd jq yq gcc ncurses-devel aws-cfn-bootstrap zstd wget $YQ_URI -O /usr/bin/yq && chmod +x /usr/bin/yq -# install aria2 a p2p downloader +echo " Installing aria2 a p2p downloader" cd /tmp if [ "$arch" == "x86_64" ]; then @@ -50,6 +50,14 @@ else make install fi +echo " Installing s5cmd" +cd /opt +wget -q $S5CMD_URI -O s5cmd.tar.gz +tar -xf s5cmd.tar.gz +chmod +x s5cmd +mv s5cmd /usr/bin +s5cmd version + cd /opt echo "Downloading assets zip file" @@ -83,7 +91,6 @@ yum install -y $SSM_AGENT_BINARY_URI REGION=${_REGION_} STACK_NAME=${_STACK_NAME_} RESTORE_FROM_SNAPSHOT=${_RESTORE_FROM_SNAPSHOT_} -FORMAT_DISK=${_FORMAT_DISK_} NETWORK_ID=${_NETWORK_ID_} NODE_CONFIG=${_NODE_CONFIG_} L1_EXECUTION_ENDPOINT=${_L1_EXECUTION_ENDPOINT_} @@ -290,9 +297,15 @@ if [ "$RESTORE_FROM_SNAPSHOT" == "false" ]; then systemctl daemon-reload systemctl enable --now base else - echo "Restoring node from snapshot" - chmod +x /opt/restore-from-snapshot.sh - echo "/opt/restore-from-snapshot.sh" | at now + 1 min + if [ "$NODE_CONFIG" == "archive" ]; then + echo "Restoring archive node from snapshot over s3" + chmod +x /opt/restore-from-snapshot-archive-s3.sh + echo "/opt/restore-from-snapshot-archive-s3.sh" | at now + 1 min + else + echo "Restoring full node from snapshot over http" + chmod +x /opt/restore-from-snapshot-http.sh + echo "/opt/restore-from-snapshot-http.sh" | at now + 1 min + fi fi if [[ "$LIFECYCLE_HOOK_NAME" != "none" ]]; then diff --git a/lib/base/single-archive-node-deploy.json b/lib/base/single-archive-node-deploy.json index 98a02002..800bf5ad 100644 --- a/lib/base/single-archive-node-deploy.json +++ b/lib/base/single-archive-node-deploy.json @@ -1,5 +1,5 @@ { "base-single-node-archive-mainnet": { - "nodeinstanceid": "i-0206eadc184b36db5" + "nodeinstanceid": "i-03efb8b36f80c2fd1" } } From 974315e978505d4e6b01d08f20cff6246c3198b2 Mon Sep 17 00:00:00 2001 From: Nikolay Vlasov Date: Tue, 28 May 2024 13:45:48 +1000 Subject: [PATCH 27/75] Base. Refactoring service start stop configuration --- lib/base/lib/assets/base/{node.sh => node-start.sh} | 0 lib/base/lib/assets/base/node-stop.sh | 9 +++++++++ lib/base/lib/assets/user-data/node.sh | 8 +++++--- 3 files changed, 14 insertions(+), 3 deletions(-) rename lib/base/lib/assets/base/{node.sh => node-start.sh} (100%) create mode 100644 lib/base/lib/assets/base/node-stop.sh diff --git a/lib/base/lib/assets/base/node.sh b/lib/base/lib/assets/base/node-start.sh similarity index 100% rename from lib/base/lib/assets/base/node.sh rename to lib/base/lib/assets/base/node-start.sh diff --git a/lib/base/lib/assets/base/node-stop.sh b/lib/base/lib/assets/base/node-stop.sh new file mode 100644 index 00000000..9ec0285f --- /dev/null +++ b/lib/base/lib/assets/base/node-stop.sh @@ -0,0 +1,9 @@ +#!/bin/bash +set -e +echo "Script is stopping the node..." + +# Stop the node +cd /home/bcuser/node +/usr/local/bin/docker-compose -f /home/bcuser/node/docker-compose.yml down + +echo "Stopped" \ No newline at end of file diff --git a/lib/base/lib/assets/user-data/node.sh b/lib/base/lib/assets/user-data/node.sh index 99575bae..90af651a 100644 --- a/lib/base/lib/assets/user-data/node.sh +++ b/lib/base/lib/assets/user-data/node.sh @@ -226,8 +226,9 @@ crontab -l echo "Configuring node as a service" mkdir /home/bcuser/bin -mv /opt/base/node.sh /home/bcuser/bin/node.sh -chmod 766 /home/bcuser/bin/node.sh +mv /opt/base/node-start.sh /home/bcuser/bin/node-start.sh +mv /opt/base/node-stop.sh /home/bcuser/bin/node-stop.sh +chmod 766 /home/bcuser/bin/* chown -R bcuser:bcuser /home/bcuser sudo bash -c 'cat > /etc/systemd/system/base.service < Date: Wed, 29 May 2024 11:33:28 +1000 Subject: [PATCH 28/75] Base. Refactoring restoration of Archive nodes from snapshots --- lib/base/lib/assets/restore-from-snapshot-archive-s3.sh | 6 +++--- lib/base/lib/assets/user-data/node.sh | 3 ++- lib/base/sample-configs/.env-sample-archive | 2 +- lib/base/single-archive-node-deploy.json | 2 +- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/lib/base/lib/assets/restore-from-snapshot-archive-s3.sh b/lib/base/lib/assets/restore-from-snapshot-archive-s3.sh index 16473ea6..a85cd044 100644 --- a/lib/base/lib/assets/restore-from-snapshot-archive-s3.sh +++ b/lib/base/lib/assets/restore-from-snapshot-archive-s3.sh @@ -11,7 +11,7 @@ SNAPSHOT_FILE_NAME=snapshot.tar.gz SNAPSHOT_DIR=/data LATEST_SNAPSHOT_FILE_NAME=$(curl https://$NETWORK_ID-$NODE_CONFIG-snapshots.base.org/latest) && \ -s5cmd --log error cp s3://base-snapshots-$NETWORK_ID-archive/$LATEST_SNAPSHOT_FILE_NAME /data && \ +s5cmd --log error cp s3://base-snapshots-$NETWORK_ID-archive/$LATEST_SNAPSHOT_FILE_NAME $SNAPSHOT_DIR/$SNAPSHOT_FILE_NAME && \ echo "Downloading Snapshot script finished" && \ sleep 60 &&\ echo "Starting snapshot decompression ..." && \ @@ -19,13 +19,13 @@ tar -zxvf $SNAPSHOT_DIR/$SNAPSHOT_FILE_NAME -C /data 2>&1 | tee unzip.log && ec echo "Decompresed snapshot, cleaning up..." -mv /data/snapshots/$NETWORK_ID/download/* /data && \ +mv /data/snapshots/$NETWORK_ID/download/* $SNAPSHOT_DIR && \ rm -rf /data/snapshots && \ rm -rf $SNAPSHOT_DIR/$SNAPSHOT_FILE_NAME echo "Snapshot is ready, starting the service.." -chown -R bcuser:bcuser /data +chown -R bcuser:bcuser $SNAPSHOT_DIR sudo systemctl daemon-reload sudo systemctl enable --now base \ No newline at end of file diff --git a/lib/base/lib/assets/user-data/node.sh b/lib/base/lib/assets/user-data/node.sh index 90af651a..02e832a8 100644 --- a/lib/base/lib/assets/user-data/node.sh +++ b/lib/base/lib/assets/user-data/node.sh @@ -7,11 +7,13 @@ AUTOSCALING_GROUP_NAME=${_AUTOSCALING_GROUP_NAME_} RESOURCE_ID=${_NODE_CF_LOGICAL_ID_} ASSETS_S3_PATH=${_ASSETS_S3_PATH_} DATA_VOLUME_TYPE=${_DATA_VOLUME_TYPE_} +DATA_VOLUME_SIZE=${_DATA_VOLUME_SIZE_} { echo "LIFECYCLE_HOOK_NAME=$LIFECYCLE_HOOK_NAME" echo "AUTOSCALING_GROUP_NAME=$AUTOSCALING_GROUP_NAME" echo "ASSETS_S3_PATH=$ASSETS_S3_PATH" echo "DATA_VOLUME_TYPE=$DATA_VOLUME_TYPE" + echo "DATA_VOLUME_SIZE=$DATA_VOLUME_SIZE" } >> /etc/environment arch=$(uname -m) @@ -242,7 +244,6 @@ RestartSec=30 User=bcuser Environment="PATH=/bin:/usr/bin:/home/bcuser/bin" ExecStart=/home/bcuser/bin/node-start.sh -ExecStop=/home/bcuser/bin/node-stop.sh [Install] WantedBy=multi-user.target EOF' diff --git a/lib/base/sample-configs/.env-sample-archive b/lib/base/sample-configs/.env-sample-archive index b8f8efbd..1c9b078e 100644 --- a/lib/base/sample-configs/.env-sample-archive +++ b/lib/base/sample-configs/.env-sample-archive @@ -14,7 +14,7 @@ BASE_CPU_TYPE="ARM_64" # All options: "x86_64", "ARM_64". IMPORT # Data volume configuration BASE_DATA_VOL_TYPE="gp3" # Other options: "io1" | "io2" | "gp3" | "instance-store" . IMPORTANT: Use "instance-store" option only with instance types that support that feature, like popular for node im4gn, d3, i3en, and i4i instance families -BASE_DATA_VOL_SIZE="7200" # Current required data size in GB to keep both snapshot archive and unarchived version of it. For Sepolia 1000 will be sufficient. +BASE_DATA_VOL_SIZE="1000" # Current required data size in GB to keep both snapshot archive and unarchived version of it. For Sepolia 1000 will be sufficient. BASE_DATA_VOL_IOPS="5000" # Max IOPS for EBS volumes (not applicable for "instance-store") BASE_DATA_VOL_THROUGHPUT="700" # Max throughput for EBS gp3 volumes (not applicable for "io1" | "io2" | "instance-store") BASE_L1_EXECUTION_ENDPOINT="https://ethereum-sepolia-rpc.publicnode.com" # Set your own URL to Ethereum L1 node: https://docs.base.org/tools/node-providers diff --git a/lib/base/single-archive-node-deploy.json b/lib/base/single-archive-node-deploy.json index 800bf5ad..ff4abbd0 100644 --- a/lib/base/single-archive-node-deploy.json +++ b/lib/base/single-archive-node-deploy.json @@ -1,5 +1,5 @@ { "base-single-node-archive-mainnet": { - "nodeinstanceid": "i-03efb8b36f80c2fd1" + "nodeinstanceid": "i-09785d994a9adcadd" } } From 8d8e90d1868721a943f1518c374cd5fe3f5db2d6 Mon Sep 17 00:00:00 2001 From: Nikolay Vlasov Date: Wed, 12 Jun 2024 10:20:29 +1000 Subject: [PATCH 29/75] Base. Fixes in handling snapshot download from Cloudflare --- lib/base/README.md | 2 +- lib/base/lib/assets/user-data/node.sh | 12 +++--------- lib/base/single-archive-node-deploy.json | 5 ----- 3 files changed, 4 insertions(+), 15 deletions(-) delete mode 100644 lib/base/single-archive-node-deploy.json diff --git a/lib/base/README.md b/lib/base/README.md index a54ea52a..527f39c1 100644 --- a/lib/base/README.md +++ b/lib/base/README.md @@ -132,7 +132,7 @@ BASE_L1_EXECUTION_ENDPOINT="https://ethereum-sepolia-rpc.publicnode.com" BASE_L1_CONSENSUS_ENDPOINT="https://ethereum-sepolia-beacon-api.publicnode.com" ``` -2. Deploy Base RPC Node and wait for it to sync. For Mainnet it might a day when using snapshots or about a week if syncing from block 0. You can use snapshots provided by the Base team by setting `BASE_RESTORE_FROM_SNAPSHOT="true"` in `.env` file. +2. Deploy Base RPC Node and wait for it to sync. For Full node on Mainnet it might take a day when using snapshots or about a week if syncing from block 0. You can use snapshots provided by the Base team by setting `BASE_RESTORE_FROM_SNAPSHOT="true"` in `.env` file. ```bash pwd diff --git a/lib/base/lib/assets/user-data/node.sh b/lib/base/lib/assets/user-data/node.sh index 02e832a8..9d0a0059 100644 --- a/lib/base/lib/assets/user-data/node.sh +++ b/lib/base/lib/assets/user-data/node.sh @@ -300,15 +300,9 @@ if [ "$RESTORE_FROM_SNAPSHOT" == "false" ]; then systemctl daemon-reload systemctl enable --now base else - if [ "$NODE_CONFIG" == "archive" ]; then - echo "Restoring archive node from snapshot over s3" - chmod +x /opt/restore-from-snapshot-archive-s3.sh - echo "/opt/restore-from-snapshot-archive-s3.sh" | at now + 1 min - else - echo "Restoring full node from snapshot over http" - chmod +x /opt/restore-from-snapshot-http.sh - echo "/opt/restore-from-snapshot-http.sh" | at now + 1 min - fi + echo "Restoring full node from snapshot over http" + chmod +x /opt/restore-from-snapshot-http.sh + echo "/opt/restore-from-snapshot-http.sh" | at now + 1 min fi if [[ "$LIFECYCLE_HOOK_NAME" != "none" ]]; then diff --git a/lib/base/single-archive-node-deploy.json b/lib/base/single-archive-node-deploy.json deleted file mode 100644 index ff4abbd0..00000000 --- a/lib/base/single-archive-node-deploy.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "base-single-node-archive-mainnet": { - "nodeinstanceid": "i-09785d994a9adcadd" - } -} From 20fd2d07318ffb6a3c024fbbd2e69e96ae3d0444 Mon Sep 17 00:00:00 2001 From: Nikolay Vlasov Date: Mon, 17 Jun 2024 14:14:30 +1000 Subject: [PATCH 30/75] Base. Pre-review fixes --- lib/base/README.md | 6 ++-- lib/base/lib/assets/base/node-start.sh | 2 +- lib/base/lib/assets/base/node-stop.sh | 2 +- .../restore-from-snapshot-archive-s3.sh | 2 +- .../lib/assets/restore-from-snapshot-http.sh | 2 +- .../assets/setup-instance-store-volumes.sh | 4 +-- lib/base/lib/assets/user-data/node.sh | 8 ++--- lib/base/lib/config/baseConfig.interface.ts | 2 +- lib/base/sample-configs/.env-sample-archive | 2 +- lib/base/sample-configs/.env-sample-full | 4 +-- lib/base/sample-configs/.env-sample-full-ha | 36 +++++++++++++++++++ lib/base/test/.env-test | 2 +- lib/base/test/base-single-node.test.ts | 2 +- lib/base/test/ha-nodes-stack.test.ts | 4 +-- .../fantom-checker/syncchecker-fantom.sh | 2 +- website/docs/Blueprints/Base.md | 2 +- 16 files changed, 60 insertions(+), 22 deletions(-) create mode 100644 lib/base/sample-configs/.env-sample-full-ha diff --git a/lib/base/README.md b/lib/base/README.md index 527f39c1..796c452d 100644 --- a/lib/base/README.md +++ b/lib/base/README.md @@ -124,7 +124,7 @@ npx cdk deploy base-common ### Option 1: Deploy Single Node -1. For L1 node you you can set your own URLs in `BASE_L1_EXECUTION_ENDPOINT` and `BASE_L1_CONSENSUS_ENDPOINT` properties of `.env` file. It can be one of [the providers recommended by Base](https://docs.base.org/tools/node-providers) or you can run your own Ethereum node [with Node Runner Ethereum blueprint](https://aws-samples.github.io/aws-blockchain-node-runners/docs/Blueprints/Ethereum) (tested with geth-lighthouse combination). For example: +1. For L1 node you you can set your own URLs in `BASE_L1_EXECUTION_ENDPOINT` and `BASE_L1_CONSENSUS_ENDPOINT` properties of `.env` file. It can be one of [the providers recommended by Base](https://docs.base.org/tools/node-providers) or you can run your own Ethereum node [with Node Runner Ethereum blueprint](https://aws-samples.github.io/aws-blockchain-node-runners/docs/Blueprints/Ethereum) (tested with single-node geth-lighthouse combination). For example: ```bash #For Sepolia: @@ -181,7 +181,7 @@ BASE_L1_CONSENSUS_ENDPOINT="https://ethereum-sepolia-beacon-api.publicnode.com" npx cdk deploy base-ha-nodes --json --outputs-file ha-nodes-deploy.json ``` -2. Give the new RPC **full** nodes about 2-3 hours (24 hours for **archive** node) to initialize and then run the following query against the load balancer behind the RPC node created +2. Give the new RPC **full** nodes about 5 hours to initialize and then run the following query against the load balancer behind the RPC node created. ```bash export RPC_ALB_URL=$(cat ha-nodes-deploy.json | jq -r '..|.alburl? | select(. != null)') @@ -197,6 +197,8 @@ BASE_L1_CONSENSUS_ENDPOINT="https://ethereum-sepolia-beacon-api.publicnode.com" **NOTE:** By default and for security reasons the load balancer is available only from within the default VPC in the region where it is deployed. It is not available from the Internet and is not open for external connections. Before opening it up please make sure you protect your RPC APIs. +**NOTE:** We currently don't recommend running **archive** nodes in HA setup, because it takes way too long to get them synced. Use single-node setup instead. + ### Monitoring Every 5 minutes a script on the Base node publishes to CloudWatch service the metrics for current block for L1/L2 clients as well as blocks behind metric for L1 and minutes behind for L2. When the node is fully synced the blocks behind metric should get to 4 and minutes behind should get down to 0. To see the metrics for **single node only**: diff --git a/lib/base/lib/assets/base/node-start.sh b/lib/base/lib/assets/base/node-start.sh index 866c7fcd..728a7db3 100644 --- a/lib/base/lib/assets/base/node-start.sh +++ b/lib/base/lib/assets/base/node-start.sh @@ -6,4 +6,4 @@ echo "Script is starting..." cd /home/bcuser/node /usr/local/bin/docker-compose -f /home/bcuser/node/docker-compose.yml up -d -echo "Started" \ No newline at end of file +echo "Started" diff --git a/lib/base/lib/assets/base/node-stop.sh b/lib/base/lib/assets/base/node-stop.sh index 9ec0285f..5a4d392d 100644 --- a/lib/base/lib/assets/base/node-stop.sh +++ b/lib/base/lib/assets/base/node-stop.sh @@ -6,4 +6,4 @@ echo "Script is stopping the node..." cd /home/bcuser/node /usr/local/bin/docker-compose -f /home/bcuser/node/docker-compose.yml down -echo "Stopped" \ No newline at end of file +echo "Stopped" diff --git a/lib/base/lib/assets/restore-from-snapshot-archive-s3.sh b/lib/base/lib/assets/restore-from-snapshot-archive-s3.sh index a85cd044..6bafba14 100644 --- a/lib/base/lib/assets/restore-from-snapshot-archive-s3.sh +++ b/lib/base/lib/assets/restore-from-snapshot-archive-s3.sh @@ -28,4 +28,4 @@ echo "Snapshot is ready, starting the service.." chown -R bcuser:bcuser $SNAPSHOT_DIR sudo systemctl daemon-reload -sudo systemctl enable --now base \ No newline at end of file +sudo systemctl enable --now base diff --git a/lib/base/lib/assets/restore-from-snapshot-http.sh b/lib/base/lib/assets/restore-from-snapshot-http.sh index 018974eb..09714d7e 100644 --- a/lib/base/lib/assets/restore-from-snapshot-http.sh +++ b/lib/base/lib/assets/restore-from-snapshot-http.sh @@ -59,4 +59,4 @@ echo "Snapshot is ready, starting the service.." chown -R bcuser:bcuser /data sudo systemctl daemon-reload -sudo systemctl enable --now base \ No newline at end of file +sudo systemctl enable --now base diff --git a/lib/base/lib/assets/setup-instance-store-volumes.sh b/lib/base/lib/assets/setup-instance-store-volumes.sh index 0b919b61..ee2825d7 100644 --- a/lib/base/lib/assets/setup-instance-store-volumes.sh +++ b/lib/base/lib/assets/setup-instance-store-volumes.sh @@ -15,7 +15,7 @@ if [ -n "$DATA_VOLUME_ID" ]; then echo "Data volume formatted. Mounting..." echo "waiting for volume to get UUID" OUTPUT=0; - while [ "$OUTPUT" = 0 ]; do + while [ "$OUTPUT" = 0 ]; do DATA_VOLUME_UUID=$(lsblk -fn -o UUID $DATA_VOLUME_ID) OUTPUT=$(echo $DATA_VOLUME_UUID | grep -c - $2) echo $OUTPUT @@ -41,4 +41,4 @@ if [ -n "$DATA_VOLUME_ID" ]; then else echo "Data volume is mounted, nothing changed" fi -fi \ No newline at end of file +fi diff --git a/lib/base/lib/assets/user-data/node.sh b/lib/base/lib/assets/user-data/node.sh index 9d0a0059..ef488978 100644 --- a/lib/base/lib/assets/user-data/node.sh +++ b/lib/base/lib/assets/user-data/node.sh @@ -99,7 +99,7 @@ L1_EXECUTION_ENDPOINT=${_L1_EXECUTION_ENDPOINT_} L1_CONSENSUS_ENDPOINT=${_L1_CONSENSUS_ENDPOINT_} SNAPSHOT_URL=${_SNAPSHOT_URL_} -{ +{ echo "REGION=$REGION" echo "NETWORK_ID=$NETWORK_ID" echo "NODE_CONFIG=$NODE_CONFIG" @@ -278,7 +278,7 @@ fi mkfs -t ext4 $DATA_VOLUME_ID echo "waiting for volume to get UUID" OUTPUT=0; - while [ "$OUTPUT" = 0 ]; do + while [ "$OUTPUT" = 0 ]; do DATA_VOLUME_UUID=$(lsblk -fn -o UUID $DATA_VOLUME_ID) OUTPUT=$(echo $DATA_VOLUME_UUID | grep -c - $2) echo $OUTPUT @@ -309,8 +309,8 @@ if [[ "$LIFECYCLE_HOOK_NAME" != "none" ]]; then echo "Signaling ASG lifecycle hook to complete" TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600") INSTANCE_ID=$(curl -H "X-aws-ec2-metadata-token: $TOKEN" -s http://169.254.169.254/latest/meta-data/instance-id) - aws autoscaling complete-lifecycle-action --lifecycle-action-result CONTINUE --instance-id $INSTANCE_ID --lifecycle-hook-name "$LIFECYCLE_HOOK_NAME" --auto-scaling-group-name "$AUTOSCALING_GROUP_NAME" --region $AWS_REGION + aws autoscaling complete-lifecycle-action --lifecycle-action-result CONTINUE --instance-id $INSTANCE_ID --lifecycle-hook-name "$LIFECYCLE_HOOK_NAME" --auto-scaling-group-name "$AUTOSCALING_GROUP_NAME" --region $REGION fi echo "All Done!!" -set -e \ No newline at end of file +set -e diff --git a/lib/base/lib/config/baseConfig.interface.ts b/lib/base/lib/config/baseConfig.interface.ts index dc2f2300..b6d0fc3f 100644 --- a/lib/base/lib/config/baseConfig.interface.ts +++ b/lib/base/lib/config/baseConfig.interface.ts @@ -28,4 +28,4 @@ export interface BaseHAConfig { albHealthCheckGracePeriodMin: number; heartBeatDelayMin: number; numberOfNodes: number; -} \ No newline at end of file +} diff --git a/lib/base/sample-configs/.env-sample-archive b/lib/base/sample-configs/.env-sample-archive index 1c9b078e..eeb088a5 100644 --- a/lib/base/sample-configs/.env-sample-archive +++ b/lib/base/sample-configs/.env-sample-archive @@ -33,4 +33,4 @@ BASE_SNAPSHOT_URL="none" # Optionally provide the URL to download ## HA nodes configuration ## BASE_HA_NUMBER_OF_NODES="2" # Total number of RPC nodes to be provisioned. Default: 2 BASE_HA_ALB_HEALTHCHECK_GRACE_PERIOD_MIN="300" # Time enough to initialize the instance -BASE_HA_NODES_HEARTBEAT_DELAY_MIN="1440" # Time sufficient enough for a node do sync \ No newline at end of file +BASE_HA_NODES_HEARTBEAT_DELAY_MIN="1440" # Time sufficient enough for a node do sync diff --git a/lib/base/sample-configs/.env-sample-full b/lib/base/sample-configs/.env-sample-full index 0449d94a..ae78c77b 100644 --- a/lib/base/sample-configs/.env-sample-full +++ b/lib/base/sample-configs/.env-sample-full @@ -32,5 +32,5 @@ BASE_SNAPSHOT_URL="none" # Optionally provide the URL to download ## HA nodes configuration ## BASE_HA_NUMBER_OF_NODES="2" # Total number of RPC nodes to be provisioned. Default: 2 -BASE_HA_ALB_HEALTHCHECK_GRACE_PERIOD_MIN="300" # Time enough to initialize the instance -BASE_HA_NODES_HEARTBEAT_DELAY_MIN="120" # Time sufficient enough for a node do sync \ No newline at end of file +BASE_HA_ALB_HEALTHCHECK_GRACE_PERIOD_MIN="500" # Time enough to initialize the instance +BASE_HA_NODES_HEARTBEAT_DELAY_MIN="120" # Time sufficient enough for a node do sync diff --git a/lib/base/sample-configs/.env-sample-full-ha b/lib/base/sample-configs/.env-sample-full-ha new file mode 100644 index 00000000..ae78c77b --- /dev/null +++ b/lib/base/sample-configs/.env-sample-full-ha @@ -0,0 +1,36 @@ +############################################################# +# Example configuration for Base nodes runner app on AWS # +############################################################# + +## Set the AWS account is and region for your environment ## +AWS_ACCOUNT_ID="xxxxxxxx" +AWS_REGION="us-east-1" + +## Common configuration parameters ## +BASE_NETWORK_ID="mainnet" # All options: "mainnet", "sepolia" +BASE_NODE_CONFIGURATION="full" # All options: "full", "archive" +BASE_INSTANCE_TYPE="m7g.2xlarge" # Reconneded for Insance Store: i3en.3xlarge, "x86_64" +BASE_CPU_TYPE="ARM_64" # All options: "x86_64", "ARM_64". IMPORTANT: Make sure the CPU type matches the instance type used + +# Data volume configuration +BASE_DATA_VOL_TYPE="gp3" # Other options: "io1" | "io2" | "gp3" | "instance-store" . IMPORTANT: Use "instance-store" option only with instance types that support that feature, like popular for node im4gn, d3, i3en, and i4i instance families +BASE_DATA_VOL_SIZE="4000" # Current required data size in GB to keep both snapshot archive and unarchived version of it. For Sepolia 1000 will be sufficient. +BASE_DATA_VOL_IOPS="5000" # Max IOPS for EBS volumes (not applicable for "instance-store") +BASE_DATA_VOL_THROUGHPUT="700" # Max throughput for EBS gp3 volumes (not applicable for "io1" | "io2" | "instance-store") +BASE_L1_EXECUTION_ENDPOINT="https://ethereum-sepolia-rpc.publicnode.com" # Set your own URL to Ethereum L1 node: https://docs.base.org/tools/node-providers +BASE_L1_CONSENSUS_ENDPOINT="https://ethereum-sepolia-beacon-api.publicnode.com" + +BASE_RESTORE_FROM_SNAPSHOT="true" # Download snapshot to speed up statup time +BASE_SNAPSHOT_URL="none" # Optionally provide the URL to download snpashot: https://docs.base.org/tutorials/run-a-base-node/#snapshots + +# Example for Sepolia: +#BASE_L1_EXECUTION_ENDPOINT=https://ethereum-sepolia-rpc.publicnode.com +#BASE_L1_CONSENSUS_ENDPOINT=https://ethereum-sepolia-beacon-api.publicnode.com +# Example for Mainnet and with Ethereum Blueprint with Geth-Lighthouse client combination and private IP: +#BASE_L1_EXECUTION_ENDPOINT=http://172.31.15.220:8545 +#BASE_L1_CONSENSUS_ENDPOINT=http://172.31.15.220:5052 + +## HA nodes configuration ## +BASE_HA_NUMBER_OF_NODES="2" # Total number of RPC nodes to be provisioned. Default: 2 +BASE_HA_ALB_HEALTHCHECK_GRACE_PERIOD_MIN="500" # Time enough to initialize the instance +BASE_HA_NODES_HEARTBEAT_DELAY_MIN="120" # Time sufficient enough for a node do sync diff --git a/lib/base/test/.env-test b/lib/base/test/.env-test index 46c9f3f7..2796c5d3 100644 --- a/lib/base/test/.env-test +++ b/lib/base/test/.env-test @@ -31,4 +31,4 @@ BASE_L1_CONSENSUS_ENDPOINT=https://ethereum-sepolia-beacon-api.publicnode.com ## HA nodes configuration ## BASE_HA_NUMBER_OF_NODES="2" # Total number of RPC nodes to be provisioned. Default: 2 BASE_HA_ALB_HEALTHCHECK_GRACE_PERIOD_MIN="300" # Time enough to initialize the instance -BASE_HA_NODES_HEARTBEAT_DELAY_MIN="120" # Time sufficient enough for a node do sync \ No newline at end of file +BASE_HA_NODES_HEARTBEAT_DELAY_MIN="120" # Time sufficient enough for a node do sync diff --git a/lib/base/test/base-single-node.test.ts b/lib/base/test/base-single-node.test.ts index 9cbc5a39..809efc53 100644 --- a/lib/base/test/base-single-node.test.ts +++ b/lib/base/test/base-single-node.test.ts @@ -17,7 +17,7 @@ describe("BaseSingleNodeStack", () => { baseSingleNodeStack = new BaseSingleNodeStack(app, "base-single-node", { stackName: `base-single-node-${config.baseNodeConfig.baseNodeConfiguration}-${config.baseNodeConfig.baseNetworkId}`, env: { account: config.baseConfig.accountId, region: config.baseConfig.region }, - + instanceType: config.baseNodeConfig.instanceType, instanceCpuType: config.baseNodeConfig.instanceCpuType, baseNetworkId: config.baseNodeConfig.baseNetworkId, diff --git a/lib/base/test/ha-nodes-stack.test.ts b/lib/base/test/ha-nodes-stack.test.ts index f7d4ee8c..09a2473e 100644 --- a/lib/base/test/ha-nodes-stack.test.ts +++ b/lib/base/test/ha-nodes-stack.test.ts @@ -14,7 +14,7 @@ describe("BaseHANodesStack", () => { const baseHANodesStack = new BaseHANodesStack(app, "base-sync-node", { stackName: `base-ha-nodes-${config.baseNodeConfig.baseNodeConfiguration}-${config.baseNodeConfig.baseNetworkId}`, env: { account: config.baseConfig.accountId, region: config.baseConfig.region }, - + instanceType: config.baseNodeConfig.instanceType, instanceCpuType: config.baseNodeConfig.instanceCpuType, baseNetworkId: config.baseNodeConfig.baseNetworkId, @@ -24,7 +24,7 @@ describe("BaseHANodesStack", () => { l1ConsensusEndpoint: config.baseNodeConfig.l1ConsensusEndpoint, snapshotUrl: config.baseNodeConfig.snapshotUrl, dataVolume: config.baseNodeConfig.dataVolume, - + albHealthCheckGracePeriodMin: config.haNodeConfig.albHealthCheckGracePeriodMin, heartBeatDelayMin: config.haNodeConfig.heartBeatDelayMin, numberOfNodes: config.haNodeConfig.numberOfNodes diff --git a/lib/fantom/lib/assets/fantom-checker/syncchecker-fantom.sh b/lib/fantom/lib/assets/fantom-checker/syncchecker-fantom.sh index 04a37d27..d5c0df2d 100644 --- a/lib/fantom/lib/assets/fantom-checker/syncchecker-fantom.sh +++ b/lib/fantom/lib/assets/fantom-checker/syncchecker-fantom.sh @@ -25,7 +25,7 @@ FANTOM_SYNC_STATS=$(su bcuser -c '/home/bcuser/go-opera/build/opera attach --dat if [ -n "$FANTOM_SYNC_STATS" ] && [ "$FANTOM_SYNC_STATS" != "false" ]; then FANTOM_SYNC_BLOCK=$(su bcuser -c '/home/bcuser/go-opera/build/opera attach --datadir=/data --exec "ftm.syncing.currentBlock"') FANTOM_HIGHEST_BLOCK=$(su bcuser -c '/home/bcuser/go-opera/build/opera attach --datadir=/data --exec "ftm.syncing.highestBlock"') - + FANTOM_BLOCKS_BEHIND="$((FANTOM_HIGHEST_BLOCK-FANTOM_SYNC_BLOCK))" else diff --git a/website/docs/Blueprints/Base.md b/website/docs/Blueprints/Base.md index 833b66c4..042bf851 100644 --- a/website/docs/Blueprints/Base.md +++ b/website/docs/Blueprints/Base.md @@ -1,5 +1,5 @@ --- -sidebar_position: 6 +sidebar_position: 10 sidebar_label: Base --- # From 4a168a94cf4302c7a366675b7413b5b8c27be558 Mon Sep 17 00:00:00 2001 From: Nikolay Vlasov Date: Fri, 22 Dec 2023 17:00:39 +1100 Subject: [PATCH 31/75] Initial commit for Base --- lib/base/.gitignore | 11 + lib/base/.npmignore | 6 + lib/base/README.md | 239 +++++++ lib/base/app.ts | 34 + lib/base/cdk.json | 57 ++ .../doc/assets/Architecture-SingleNode-v3.jpg | Bin 0 -> 48320 bytes .../doc/assets/Architecture-SingleNode.drawio | 77 +++ lib/base/jest.config.js | 8 + .../lib/amb-ethereum-single-node-stack.ts | 43 ++ .../lib/assets/cfn-hup/cfn-auto-reloader.conf | 4 + lib/base/lib/assets/cfn-hup/cfn-hup.conf | 5 + lib/base/lib/assets/cfn-hup/cfn-hup.service | 8 + lib/base/lib/assets/cw-agent.json | 76 +++ lib/base/lib/assets/node-cw-dashboard.ts | 235 +++++++ lib/base/lib/assets/restore-from-snapshot.sh | 21 + .../assets/sync-checker/syncchecker-base.sh | 24 + lib/base/lib/assets/user-data/node.sh | 228 +++++++ lib/base/lib/common-stack.ts | 71 ++ lib/base/lib/config/baseConfig.interface.ts | 22 + lib/base/lib/config/baseConfig.ts | 38 ++ .../constructs/base-node-security-group.ts | 34 + lib/base/lib/single-node-stack.ts | 136 ++++ lib/base/package-lock.json | 641 ++++++++++++++++++ lib/base/package.json | 20 + lib/base/sample-configs/.env-sample-rpc | 20 + lib/base/test/.env-test | 22 + lib/base/test/base-ethereum-l1-node.test.ts | 57 ++ lib/base/test/base-single-node.test.ts | 123 ++++ lib/base/tsconfig.json | 31 + 29 files changed, 2291 insertions(+) create mode 100644 lib/base/.gitignore create mode 100644 lib/base/.npmignore create mode 100644 lib/base/README.md create mode 100644 lib/base/app.ts create mode 100644 lib/base/cdk.json create mode 100644 lib/base/doc/assets/Architecture-SingleNode-v3.jpg create mode 100644 lib/base/doc/assets/Architecture-SingleNode.drawio create mode 100644 lib/base/jest.config.js create mode 100644 lib/base/lib/amb-ethereum-single-node-stack.ts create mode 100644 lib/base/lib/assets/cfn-hup/cfn-auto-reloader.conf create mode 100644 lib/base/lib/assets/cfn-hup/cfn-hup.conf create mode 100644 lib/base/lib/assets/cfn-hup/cfn-hup.service create mode 100644 lib/base/lib/assets/cw-agent.json create mode 100644 lib/base/lib/assets/node-cw-dashboard.ts create mode 100644 lib/base/lib/assets/restore-from-snapshot.sh create mode 100644 lib/base/lib/assets/sync-checker/syncchecker-base.sh create mode 100644 lib/base/lib/assets/user-data/node.sh create mode 100644 lib/base/lib/common-stack.ts create mode 100644 lib/base/lib/config/baseConfig.interface.ts create mode 100644 lib/base/lib/config/baseConfig.ts create mode 100644 lib/base/lib/constructs/base-node-security-group.ts create mode 100644 lib/base/lib/single-node-stack.ts create mode 100644 lib/base/package-lock.json create mode 100644 lib/base/package.json create mode 100644 lib/base/sample-configs/.env-sample-rpc create mode 100644 lib/base/test/.env-test create mode 100644 lib/base/test/base-ethereum-l1-node.test.ts create mode 100644 lib/base/test/base-single-node.test.ts create mode 100644 lib/base/tsconfig.json diff --git a/lib/base/.gitignore b/lib/base/.gitignore new file mode 100644 index 00000000..0304fefb --- /dev/null +++ b/lib/base/.gitignore @@ -0,0 +1,11 @@ +*.js +!jest.config.js +*.d.ts +node_modules + +# CDK asset staging directory +.cdk.staging +cdk.out +.idea + +*-node.json diff --git a/lib/base/.npmignore b/lib/base/.npmignore new file mode 100644 index 00000000..c1d6d45d --- /dev/null +++ b/lib/base/.npmignore @@ -0,0 +1,6 @@ +*.ts +!*.d.ts + +# CDK asset staging directory +.cdk.staging +cdk.out diff --git a/lib/base/README.md b/lib/base/README.md new file mode 100644 index 00000000..dac10bb6 --- /dev/null +++ b/lib/base/README.md @@ -0,0 +1,239 @@ +# Sample AWS Blockchain Node Runner app for Base Nodes + +[Base](https://base.org/) is a "Layer 2" scaling solution for Ethereum. This blueprint helps to deploy Base RPC nodes on AWS and use [Amazon Managed Blockchain Access Ethereum](https://docs.aws.amazon.com/managed-blockchain/latest/ethereum-dev/ethereum-concepts.html) node for "Layer 1". It is meant to be used for development, testing or Proof of Concept purposes. + +## Overview of Deployment Architectures for Single Node setups + +### Single node setup + +![Single Node Deployment](./doc/assets/Architecture-SingleNode-v3.jpg) + +1. A Base node deployed in the [Default VPC](https://docs.aws.amazon.com/vpc/latest/userguide/default-vpc.html) continuously synchronizes with the rest of nodes on Base blockchain network through [Internet Gateway](https://docs.aws.amazon.com/vpc/latest/userguide/VPC_Internet_Gateway.html). +2. The Base node is used by dApps or development tools internally from within the Default VPC. JSON RPC API is not exposed to the Internet directly to protect nodes from unauthorized access. +3. You will need access to a fully-synced Ethereum Mainnet RPC endpoint before running. +4. The Base node sends various monitoring metrics for both EC2 and Base nodes to Amazon CloudWatch. + + +## Additional materials + +
+ +Review the for pros and cons of this solution. + +### Well-Architected Checklist + +This is the Well-Architected checklist for AWS Blockchain Node Runner app. This checklist takes into account questions from the [AWS Well-Architected Framework](https://aws.amazon.com/architecture/well-architected/) which are relevant to this workload. Please feel free to add more checks from the framework if required for your workload. + +| Pillar | Control | Question/Check | Remarks | +|:------------------------|:----------------------------------|:---------------------------------------------------------------------------------|:-----------------| +| Security | Network protection | Are there unnecessary open ports in security groups? | Please note that ports 9222 (TCP/UDP) for Base are open to public to support P2P protocols. We have to rely on the protection mechanisms built into the Base software to protect those ports. | +| | | Traffic inspection | AWS WAF could be implemented for traffic inspection. Additional charges will apply. | +| | Compute protection | Reduce attack surface | This solution uses Amazon Linux AMI. You may choose to run hardening scripts on it. | +| | | Enable people to perform actions at a distance | This solution uses AWS Systems Manager for terminal session, not ssh ports. | +| | Data protection at rest | Use encrypted Amazon Elastic Block Store (Amazon EBS) volumes | This solution uses encrypted Amazon EBS volumes. | +| | Data protection in transit | Use TLS | By design TLS is not used in Base RPC and P2P protocols because the data is considered public. To protect RPC traffic we expose the port only for internal use. | +| | Authorization and access control | Use instance profile with Amazon Elastic Compute Cloud (Amazon EC2) instances | This solution uses AWS Identity and Access Management (AWS IAM) role instead of IAM user. | +| | | Following principle of least privilege access | In the node, root user is not used (using special user "ubuntu" instead). | +| | Application security | Security focused development practices | cdk-nag is being used with documented suppressions. | +| Cost optimization | Service selection | Use cost effective resources | Base nodes currently doesn't provide binaries for ARM architecture, so AMD-powered EC2 instance type for better cost effectiveness. | +| | Cost awareness | Estimate costs | One Base node on m6a.2xlarge and 1T EBS gp3 volume will cost around US$367.21 per month in the US East (N. Virginia) region. Additionally the AMB Access Ethereum on bc.m5.xlarge will cost additional ~US$202 per month in the US East (N. Virginia) region. Approximately the total cost will be US$367.21 + US$202 = US$569.21 per month. | +| Reliability | Resiliency implementation | Withstand component failures | This solution currently does not have high availability and is deployed to a single availability zone. | +| | Data backup | How is data backed up? | The data is not specially backed up. The node will have to re-sync its state from other nodes in the Base network to recover. | +| | Resource monitoring | How are workload resources monitored? | Resources are being monitored using Amazon CloudWatch dashboards. Amazon CloudWatch custom metrics are being pushed via CloudWatch Agent. | +| Performance efficiency | Compute selection | How is compute solution selected? | Compute solution is selected based on the recommendations the from Base community to provide stable and cost-effective operations. | +| | Storage selection | How is storage solution selected? | Storage solution is selected based on the recommendations the from Base community to provide stable and cost-effective operations. | +| | Architecture selection | How is the best performance architecture selected? | In this solution we try to balance price and performance to achieve better cost efficiency, but not necessarily the best performance. | +| Operational excellence | Workload health | How is health of workload determined? | We rely on the standard EC2 instance monitoring tool to detect stalled instances. | +| Sustainability | Hardware & services | Select most efficient hardware for your workload | Base nodes currently doesn't provide binaries for ARM architecture, so AMD-powered EC2 instance type for better cost effectiveness. | +
+
+Recommended Infrastructure + +## Hardware Requirements + +**Minimum for Base node** + +- Instance type [m6a.xlarge](https://aws.amazon.com/ec2/instance-types/m6a/). +- 2500GB EBS gp3 storage with at least 6000 IOPS. + +**Recommended for Base node** + +- Instance type [m6a.2xlarge](https://aws.amazon.com/ec2/instance-types/m6a/). +- 2500GB EBS gp3 storage with at least 6000 IOPS.` + +**Amazon Managed Blockchain Ethereum L1** + +- Minimum instance type: [bc.m5.xlarge](https://aws.amazon.com/managed-blockchain/instance-types/) +- Recommended instance type: [bc.m5.2xlarge](https://aws.amazon.com/managed-blockchain/instance-types/) + +
+ +## Setup Instructions + +### Setup Cloud9 + +We will use AWS Cloud9 to execute the subsequent commands. Follow the instructions in [Cloud9 Setup](../../docs/setup-cloud9.md) + +### Clone this repository and install dependencies + +```bash + git clone https://github.com/alickwong/aws-blockchain-node-runners + cd aws-blockchain-node-runners + npm install +``` + +### Deploy Single Node + +1. Make sure you are in the root directory of the cloned repository + +2. If you have deleted or don't have the default VPC, create default VPC + + ```bash + aws ec2 create-default-vpc + ``` + + > NOTE: + > You may see the following error if the default VPC already exists: `An error occurred (DefaultVpcAlreadyExists) when calling the CreateDefaultVpc operation: A Default VPC already exists for this account in this region.`. That means you can just continue with the following steps. + +3. Configure your setup + + Create your own copy of `.env` file and edit it to update with your AWS Account ID and Region: + ```bash + # Make sure you are in aws-blockchain-node-runners/lib/base + cd lib/base + npm install + pwd + cp ./sample-configs/.env-sample-rpc .env + nano .env + ``` + > NOTE: + > Example configuration parameters are set in the local `.env-sample` file. You can find more examples inside `sample-configs` directory. + +4. Deploy common components such as IAM role + + ```bash + pwd + # Make sure you are in aws-blockchain-node-runners/lib/base + npx cdk deploy base-common + ``` + + > IMPORTANT: + > All AWS CDK v2 deployments use dedicated AWS resources to hold data during deployment. Therefore, your AWS account and Region must be [bootstrapped](https://docs.aws.amazon.com/cdk/v2/guide/bootstrapping.html) to create these resources before you can deploy. If you haven't already bootstrapped, issue the following command: + > ```bash + > cdk bootstrap aws://ACCOUNT-NUMBER/REGION + > ``` + +5. Deploy Amazon Managed Blockchain (AMB) Access Ethereum node and wait about 35-70 minutes for the node to sync + + ```bash + pwd + # Make sure you are in aws-blockchain-node-runners/lib/base + npx cdk deploy base-ethereum-l1-node --json --outputs-file base-ethereum-l1-node.json + ``` + To watch the progress, open the [AMB Web UI](https://console.aws.amazon.com/managedblockchain/home), click the name of your target network from the list (Mainnet, Goerly, etc.) and watch the status of the node to change from `Creating` to `Available`. + +6. Deploy Base RPC Node and wait for another 10-20 minutes for it to sync + + ```bash + pwd + # Make sure you are in aws-blockchain-node-runners/lib/base + npx cdk deploy base-single-node --json --outputs-file single-node-deploy.json + ``` + After starting the node you will need to wait for the initial synchronization process to finish.To see the progress, you may use SSM to connect into EC2 first and watch the log like this: + + ```bash + export INSTANCE_ID=$(cat single-node-deploy.json | jq -r '..|.node-instance-id? | select(. != null)') + echo "INSTANCE_ID=" $INSTANCE_ID + export AWS_REGION=us-east-1 + aws ssm start-session --target $INSTANCE_ID --region $AWS_REGION + echo Latest synced block behind by: $((($(date +%s)-$( \ + curl -d '{"id":0,"jsonrpc":"2.0","method":"optimism_syncStatus"}' \ + -H "Content-Type: application/json" http://localhost:7545 | \ + jq -r .result.unsafe_l2.timestamp))/60)) minutes + ``` + +7. Test Base RPC API + Use curl to query from within the node instance: + ```bash + export INSTANCE_ID=$(cat single-node-deploy.json | jq -r '..|.node-instance-id? | select(. != null)') + echo "INSTANCE_ID=" $INSTANCE_ID + export AWS_REGION=us-east-1 + aws ssm start-session --target $INSTANCE_ID --region $AWS_REGION + + curl -s -X POST -H "Content-Type: application/json" --data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' http://localhost:8545 + ``` + +### Monitoring +A script on the Base node publishes current block and blocks behind metrics to CloudWatch metrics every 5 minutes. When the node is fully synced the blocks behind metric should get to 0, which might take about 1.5 days. To see the metrics: + +- Navigate to CloudWatch service (make sure you are in the region you have specified for AWS_REGION) +- Open Dashboards and select `base-single-node` from the list of dashboards. + +## Clear up and undeploy everything + +1. Undeploy all Nodes and Common stacks + + ```bash + # Setting the AWS account id and region in case local .env file is lost + export AWS_ACCOUNT_ID= + export AWS_REGION= + + pwd + # Make sure you are in aws-blockchain-node-runners/lib/base + + # Undeploy Single Node + npx cdk destroy base-single-node + + # Undeploy AMB Etheruem node + npx cdk destroy base-ethereum-l1-node + + # Delete all common components like IAM role and Security Group + npx cdk destroy base-common + ``` + +2. Follow steps to delete the Cloud9 instance in [Cloud9 Setup](../../doc/setup-cloud9.md) + +## FAQ + +1. How to check the logs of the clients running on my Base node? + + **Note:** In this tutorial we chose not to use SSH and use Session Manager instead. That allows you to log all sessions in AWS CloudTrail to see who logged into the server and when. If you receive an error similar to `SessionManagerPlugin is not found`, [install Session Manager plugin for AWS CLI](https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-working-with-install-plugin.html) + + ```bash + pwd + # Make sure you are in aws-blockchain-node-runners/lib/base + + export INSTANCE_ID=$(cat single-node-deploy.json | jq -r '..|.node-instance-id? | select(. != null)') + echo "INSTANCE_ID=" $INSTANCE_ID + export AWS_REGION=us-east-1 + aws ssm start-session --target $INSTANCE_ID --region $AWS_REGION + sudo su bcuser + docker logs --tail 50 node_node_1 -f + ``` +2. How to check the logs from the EC2 user-data script? + + ```bash + pwd + # Make sure you are in aws-blockchain-node-runners/lib/base + + export INSTANCE_ID=$(cat single-node-deploy.json | jq -r '..|.node-instance-id? | select(. != null)') + echo "INSTANCE_ID=" $INSTANCE_ID + export AWS_REGION=us-east-1 + aws ssm start-session --target $INSTANCE_ID --region $AWS_REGION + sudo cat /var/log/cloud-init-output.log + ``` + +3. How can I restart the Base node? + + ``` bash + export INSTANCE_ID=$(cat single-node-deploy.json | jq -r '..|.node-instance-id? | select(. != null)') + echo "INSTANCE_ID=" $INSTANCE_ID + export AWS_REGION=us-east-1 + aws ssm start-session --target $INSTANCE_ID --region $AWS_REGION + sudo su bcuser + /usr/local/bin/docker-compose -f /home/bcuser/node/docker-compose.yml down && \ + /usr/local/bin/docker-compose -f /home/bcuser/node/docker-compose.yml up -d + ``` +4. Where to find the key Base client directories? + + - The data directory is `/data` diff --git a/lib/base/app.ts b/lib/base/app.ts new file mode 100644 index 00000000..8eea3432 --- /dev/null +++ b/lib/base/app.ts @@ -0,0 +1,34 @@ +#!/usr/bin/env node +import 'dotenv/config' +import 'source-map-support/register'; +import * as cdk from 'aws-cdk-lib'; +import * as config from "./lib/config/baseConfig"; +import {BaseCommonStack} from "./lib/common-stack"; +import {BaseAMBEthereumSingleNodeStack} from "./lib/amb-ethereum-single-node-stack"; +import {BaseSingleNodeStack} from "./lib/single-node-stack"; + +const app = new cdk.App(); +cdk.Tags.of(app).add("Project", "AWSBase"); + +new BaseCommonStack(app, "base-common", { + stackName: `base-nodes-common`, + env: { account: config.baseConfig.accountId, region: config.baseConfig.region }, +}); + +new BaseAMBEthereumSingleNodeStack(app, "base-ethereum-l1-node", { + stackName: `base-amb-ethereum-single-node-${config.baseNodeConfig.baseNetworkId}`, + env: { account: config.baseConfig.accountId, region: config.baseConfig.region }, + + ambEthereumNodeNetworkId: config.baseNodeConfig.ambEntereumNodeNetworkId, + ambEthereumNodeInstanceType: config.baseNodeConfig.ambEntereumNodeInstanceType, +}); + +new BaseSingleNodeStack(app, "base-single-node", { + stackName: `base-single-node-${config.baseNodeConfig.baseNetworkId}`, + env: { account: config.baseConfig.accountId, region: config.baseConfig.region }, + + instanceType: config.baseNodeConfig.instanceType, + instanceCpuType: config.baseNodeConfig.instanceCpuType, + baseNetworkId: config.baseNodeConfig.baseNetworkId, + dataVolume: config.baseNodeConfig.dataVolume, +}); diff --git a/lib/base/cdk.json b/lib/base/cdk.json new file mode 100644 index 00000000..7714e8c2 --- /dev/null +++ b/lib/base/cdk.json @@ -0,0 +1,57 @@ +{ + "app": "npx ts-node --prefer-ts-exts app.ts", + "watch": { + "include": [ + "**" + ], + "exclude": [ + "README.md", + "cdk*.json", + "**/*.d.ts", + "**/*.js", + "tsconfig.json", + "package*.json", + "yarn.lock", + "node_modules", + "test" + ] + }, + "context": { + "@aws-cdk/aws-lambda:recognizeLayerVersion": true, + "@aws-cdk/core:checkSecretUsage": true, + "@aws-cdk/core:target-partitions": [ + "aws", + "aws-cn" + ], + "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, + "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, + "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, + "@aws-cdk/aws-iam:minimizePolicies": true, + "@aws-cdk/core:validateSnapshotRemovalPolicy": true, + "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, + "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, + "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, + "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, + "@aws-cdk/core:enablePartitionLiterals": true, + "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, + "@aws-cdk/aws-iam:standardizedServicePrincipals": true, + "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, + "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, + "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, + "@aws-cdk/aws-route53-patters:useCertificate": true, + "@aws-cdk/customresources:installLatestAwsSdkDefault": false, + "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, + "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, + "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, + "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, + "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, + "@aws-cdk/aws-redshift:columnId": true, + "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, + "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, + "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, + "@aws-cdk/aws-kms:aliasNameRef": true, + "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true, + "@aws-cdk/core:includePrefixInUniqueNameGeneration": true, + "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true + } +} diff --git a/lib/base/doc/assets/Architecture-SingleNode-v3.jpg b/lib/base/doc/assets/Architecture-SingleNode-v3.jpg new file mode 100644 index 0000000000000000000000000000000000000000..99e05c260cd9207eb7e32f058e667a57c1b5daa5 GIT binary patch literal 48320 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
9%fp57m^JTU zDnh*{_KnPa{d~6iwj*rS2L+3)%>{o~+!b@2eW_WRGx*(DY5(vDhv_R{N)#!N8&HSU zS4W(4I(I0d#=~K;+)UbRLNdpzS_GkUr*yNC_OKpAf*jyiDyfyPnN$`WOJVf?P#DxT+N4A=qOM;yx-zJq7zh?D zT*z-~W9H6Md*n

t<(c_=yk;Cj)sg-%aTeR>0Qe`#?dQ>sp5#p6XO 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 SRqozg5!gIov6im>dFbC-Q80-B literal 0 HcmV?d00001 diff --git a/lib/base/doc/assets/Architecture-SingleNode.drawio b/lib/base/doc/assets/Architecture-SingleNode.drawio new file mode 100644 index 00000000..f6fb4275 --- /dev/null +++ b/lib/base/doc/assets/Architecture-SingleNode.drawio @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/base/jest.config.js b/lib/base/jest.config.js new file mode 100644 index 00000000..08263b89 --- /dev/null +++ b/lib/base/jest.config.js @@ -0,0 +1,8 @@ +module.exports = { + testEnvironment: 'node', + roots: ['/test'], + testMatch: ['**/*.test.ts'], + transform: { + '^.+\\.tsx?$': 'ts-jest' + } +}; diff --git a/lib/base/lib/amb-ethereum-single-node-stack.ts b/lib/base/lib/amb-ethereum-single-node-stack.ts new file mode 100644 index 00000000..614a171f --- /dev/null +++ b/lib/base/lib/amb-ethereum-single-node-stack.ts @@ -0,0 +1,43 @@ +import * as cdk from "aws-cdk-lib"; +import * as cdkConstructs from "constructs"; +import * as configTypes from "./config/baseConfig.interface"; +import { SingleNodeAMBEthereumConstruct } from "../../constructs/amb-ethereum-single-node"; + +export interface BaseAMBEthereumSingleNodeStackProps extends cdk.StackProps { + ambEthereumNodeNetworkId: configTypes.AMBEthereumNodeNetworkId, + ambEthereumNodeInstanceType: string, +} + +export class BaseAMBEthereumSingleNodeStack extends cdk.Stack { + constructor(scope: cdkConstructs.Construct, id: string, props: BaseAMBEthereumSingleNodeStackProps) { + super(scope, id, props); + + // Setting up necessary environment variables + const availabilityZones = cdk.Stack.of(this).availabilityZones; + const chosenAvailabilityZone = availabilityZones.slice(0, 1)[0]; + + // Getting our config from initialization properties + const { + ambEthereumNodeNetworkId, + ambEthereumNodeInstanceType, + } = props; + + // Setting up L1 Ethereum node with AMB Ethereum node construct + + const ambEthereumNode = new SingleNodeAMBEthereumConstruct(this, "base-amb-ethereum-l1-single-node", { + instanceType: ambEthereumNodeInstanceType, + availabilityZone: chosenAvailabilityZone, + ethNetworkId: ambEthereumNodeNetworkId, + }) + + new cdk.CfnOutput(this, "amb-eth-node-id", { + value: ambEthereumNode.nodeId, + exportName: "BaseAmbEthereumNodeId" + }); + + new cdk.CfnOutput(this, "amb-eth-node-rpc-url-billing-token", { + value: ambEthereumNode.rpcUrlWithBillingToken, + exportName: "BaseAmbEthereumNodeRpcUrlWithBillingToken", + }); + } +} diff --git a/lib/base/lib/assets/cfn-hup/cfn-auto-reloader.conf b/lib/base/lib/assets/cfn-hup/cfn-auto-reloader.conf new file mode 100644 index 00000000..3cd32a0a --- /dev/null +++ b/lib/base/lib/assets/cfn-hup/cfn-auto-reloader.conf @@ -0,0 +1,4 @@ +[cfn-auto-reloader-hook] +triggers=post.update +path=Resources.WebServerHost.Metadata.AWS::CloudFormation::Init +action=/opt/aws/bin/cfn-init -v --stack __AWS_STACK_NAME__ --resource WebServerHost --region __AWS_REGION__ diff --git a/lib/base/lib/assets/cfn-hup/cfn-hup.conf b/lib/base/lib/assets/cfn-hup/cfn-hup.conf new file mode 100644 index 00000000..2163b37a --- /dev/null +++ b/lib/base/lib/assets/cfn-hup/cfn-hup.conf @@ -0,0 +1,5 @@ +[main] +stack=__AWS_STACK_ID__ +region=__AWS_REGION__ +# The interval used to check for changes to the resource metadata in minutes. Default is 15 +interval=2 diff --git a/lib/base/lib/assets/cfn-hup/cfn-hup.service b/lib/base/lib/assets/cfn-hup/cfn-hup.service new file mode 100644 index 00000000..2660ea46 --- /dev/null +++ b/lib/base/lib/assets/cfn-hup/cfn-hup.service @@ -0,0 +1,8 @@ +[Unit] +Description=cfn-hup daemon +[Service] +Type=simple +ExecStart=/usr/local/bin/cfn-hup +Restart=always +[Install] +WantedBy=multi-user.target diff --git a/lib/base/lib/assets/cw-agent.json b/lib/base/lib/assets/cw-agent.json new file mode 100644 index 00000000..28833017 --- /dev/null +++ b/lib/base/lib/assets/cw-agent.json @@ -0,0 +1,76 @@ +{ + "agent": { + "metrics_collection_interval": 60, + "run_as_user": "root" + }, + "metrics": { + "aggregation_dimensions": [ + [ + "InstanceId" + ] + ], + "append_dimensions": { + "InstanceId": "${aws:InstanceId}" + }, + "metrics_collected": { + "cpu": { + "measurement": [ + "cpu_usage_idle", + "cpu_usage_iowait", + "cpu_usage_user", + "cpu_usage_system" + ], + "metrics_collection_interval": 60, + "resources": [ + "*" + ], + "totalcpu": false + }, + "disk": { + "measurement": [ + "used_percent" + ], + "metrics_collection_interval": 60, + "resources": [ + "*" + ] + }, + "diskio": { + "measurement": [ + "io_time", + "write_bytes", + "read_bytes", + "writes", + "reads", + "write_time", + "read_time", + "iops_in_progress" + ], + "metrics_collection_interval": 60, + "resources": [ + "*" + ] + }, + "mem": { + "measurement": [ + "mem_used_percent", + "mem_cached" + ], + "metrics_collection_interval": 60 + }, + "netstat": { + "measurement": [ + "tcp_established", + "tcp_time_wait" + ], + "metrics_collection_interval": 60 + }, + "swap": { + "measurement": [ + "swap_used_percent" + ], + "metrics_collection_interval": 60 + } + } + } +} diff --git a/lib/base/lib/assets/node-cw-dashboard.ts b/lib/base/lib/assets/node-cw-dashboard.ts new file mode 100644 index 00000000..fd8b5462 --- /dev/null +++ b/lib/base/lib/assets/node-cw-dashboard.ts @@ -0,0 +1,235 @@ +export const SyncNodeCWDashboardJSON = { + "widgets": [ + { + "height": 5, + "width": 6, + "y": 0, + "x": 0, + "type": "metric", + "properties": { + "view": "timeSeries", + "stat": "Average", + "period": 300, + "stacked": false, + "yAxis": { + "left": { + "min": 0 + } + }, + "region": "${REGION}", + "metrics": [ + [ "AWS/EC2", "CPUUtilization", "InstanceId", "${INSTANCE_ID}", { "label": "${INSTANCE_ID}-${INSTANCE_NAME}" } ] + ], + "title": "CPU utilization (%)" + } + }, + { + "height": 5, + "width": 6, + "y": 5, + "x": 18, + "type": "metric", + "properties": { + "view": "timeSeries", + "stat": "Average", + "period": 300, + "stacked": false, + "yAxis": { + "left": { + "min": 0 + } + }, + "region": "${REGION}", + "metrics": [ + [ "AWS/EC2", "NetworkIn", "InstanceId", "${INSTANCE_ID}", { "label": "${INSTANCE_ID}-${INSTANCE_NAME}" } ] + ], + "title": "Network in (bytes)" + } + }, + { + "height": 5, + "width": 6, + "y": 0, + "x": 18, + "type": "metric", + "properties": { + "view": "timeSeries", + "stat": "Average", + "period": 300, + "stacked": false, + "yAxis": { + "left": { + "min": 0 + } + }, + "region": "${REGION}", + "metrics": [ + [ "AWS/EC2", "NetworkOut", "InstanceId", "${INSTANCE_ID}", { "label": "${INSTANCE_ID}-${INSTANCE_NAME}" } ] + ], + "title": "Network out (bytes)" + } + }, + { + "height": 5, + "width": 6, + "y": 10, + "x": 0, + "type": "metric", + "properties": { + "view": "timeSeries", + "stacked": false, + "region": "${REGION}", + "stat": "Average", + "period": 300, + "metrics": [ + [ "CWAgent", "mem_used_percent", "InstanceId", "${INSTANCE_ID}", { "label": "${INSTANCE_ID}-${INSTANCE_NAME}" } ] + ], + "title": "Mem Used (%)" + } + }, + { + "height": 5, + "width": 6, + "y": 5, + "x": 0, + "type": "metric", + "properties": { + "view": "timeSeries", + "stacked": false, + "region": "${REGION}", + "stat": "Average", + "period": 300, + "metrics": [ + [ "CWAgent", "cpu_usage_iowait", "InstanceId", "${INSTANCE_ID}", { "label": "${INSTANCE_ID}-${INSTANCE_NAME}" } ] + ], + "title": "CPU Usage IO wait (%)" + } + }, + { + "height": 5, + "width": 6, + "y": 0, + "x": 6, + "type": "metric", + "properties": { + "metrics": [ + [ { "expression": "m7/PERIOD(m7)", "label": "Read", "id": "e7" } ], + [ "CWAgent", "diskio_reads", "InstanceId", "${INSTANCE_ID}", "name", "nvme1n1", { "id": "m7", "visible": false, "stat": "Sum", "period": 60 } ], + [ { "expression": "m8/PERIOD(m8)", "label": "Write", "id": "e8" } ], + [ "CWAgent", "diskio_writes", "InstanceId", "${INSTANCE_ID}", "name", "nvme1n1", { "id": "m8", "visible": false, "stat": "Sum", "period": 60 } ] + ], + "view": "timeSeries", + "stacked": false, + "region": "${REGION}", + "stat": "Sum", + "period": 60, + "title": "nvme1n1 Volume Read/Write (IO/sec)" + } + }, + { + "height": 5, + "width": 6, + "y": 5, + "x": 12, + "type": "metric", + "properties": { + "metrics": [ + [ "CWAgent", "elc_sync_block", "InstanceId", "${INSTANCE_ID}", { "label": "${INSTANCE_ID}-${INSTANCE_NAME}" } ] + ], + "sparkline": true, + "view": "timeSeries", + "stacked": false, + "region": "${REGION}", + "stat": "Maximum", + "period": 60, + "title": "Base Client Block Height" + } + }, + { + "height": 5, + "width": 6, + "y": 10, + "x": 12, + "type": "metric", + "properties": { + "sparkline": true, + "view": "timeSeries", + "stacked": false, + "region": "${REGION}", + "stat": "Maximum", + "period": 60, + "metrics": [ + [ "CWAgent", "elc_blocks_behind", "InstanceId", "${INSTANCE_ID}", { "label": "${INSTANCE_ID}-${INSTANCE_NAME}" } ] + ], + "title": "Base Client Blocks Behind" + } + }, + { + "height": 5, + "width": 6, + "y": 5, + "x": 6, + "type": "metric", + "properties": { + "view": "timeSeries", + "stat": "Sum", + "period": 60, + "stacked": false, + "yAxis": { + "left": { + "min": 0 + } + }, + "region": "${REGION}", + "metrics": [ + [ { "expression": "IF(m7_2 !=0, (m7_1 / m7_2), 0)", "label": "Read", "id": "e7" } ], + [ "CWAgent", "diskio_read_time", "InstanceId", "${INSTANCE_ID}", "name", "nvme1n1", { "id": "m7_1", "visible": false, "stat": "Sum", "period": 60 } ], + [ "CWAgent", "diskio_reads", "InstanceId", "${INSTANCE_ID}", "name", "nvme1n1", { "id": "m7_2", "visible": false, "stat": "Sum", "period": 60 } ], + [ { "expression": "IF(m7_4 !=0, (m7_3 / m7_4), 0)", "label": "Write", "id": "e8" } ], + [ "CWAgent", "diskio_write_time", "InstanceId", "${INSTANCE_ID}", "name", "nvme1n1", { "id": "m7_3", "visible": false, "stat": "Sum", "period": 60 } ], + [ "CWAgent", "diskio_writes", "InstanceId", "${INSTANCE_ID}", "name", "nvme1n1", { "id": "m7_4", "visible": false, "stat": "Sum", "period": 60 } ] + ], + "title": "nvme1n1 Volume Read/Write latency (ms/op)" + } + }, + { + "height": 5, + "width": 6, + "y": 10, + "x": 6, + "type": "metric", + "properties": { + "metrics": [ + [ { "expression": "(m2/1048576)/PERIOD(m2)", "label": "Read", "id": "e2", "period": 60, "region": "${REGION}" } ], + [ "CWAgent", "diskio_read_bytes", "InstanceId", "${INSTANCE_ID}", "name", "nvme1n1", { "id": "m2", "stat": "Sum", "visible": false, "period": 60 } ], + [ { "expression": "(m3/1048576)/PERIOD(m3)", "label": "Write", "id": "e3", "period": 60, "region": "${REGION}" } ], + [ "CWAgent", "diskio_write_bytes", "InstanceId", "${INSTANCE_ID}", "name", "nvme1n1", { "id": "m3", "stat": "Sum", "visible": false, "period": 60 } ] + ], + "view": "timeSeries", + "stacked": false, + "region": "${REGION}", + "stat": "Average", + "period": 60, + "title": "nvme1n1 Volume Read/Write throughput (MiB/sec)" + } + }, + { + "height": 5, + "width": 6, + "y": 0, + "x": 12, + "type": "metric", + "properties": { + "metrics": [ + [ "CWAgent", "disk_used_percent", "path", "/data", "InstanceId", "${INSTANCE_ID}", "device", "nvme1n1", "fstype", "ext4", { "region": "${REGION}", "label": "/data" } ] + ], + "sparkline": true, + "view": "singleValue", + "region": "${REGION}", + "title": "nvme1n1 Disk Used (%)", + "period": 60, + "stat": "Average" + } + } + ] +} diff --git a/lib/base/lib/assets/restore-from-snapshot.sh b/lib/base/lib/assets/restore-from-snapshot.sh new file mode 100644 index 00000000..0460fda8 --- /dev/null +++ b/lib/base/lib/assets/restore-from-snapshot.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +source /etc/environment +TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600") +INSTANCE_ID=$(curl -H "X-aws-ec2-metadata-token: $TOKEN" -s http://169.254.169.254/latest/meta-data/instance-id) +LATEST_SNAPSHOT_FILE_NAME=$(curl https://base-snapshots-$NETWORK_ID-archive.s3.amazonaws.com/latest) + +echo "Sync started at " $(date) +SECONDS=0 + +s5cmd --log error cp s3://base-snapshots-$NETWORK_ID-archive/$LATEST_SNAPSHOT_FILE_NAME /data && \ +tar -I zstd -xf /data/$LATEST_SNAPSHOT_FILE_NAME -C /data && \ +mv /data/snapshots/$NETWORK_ID/download/* /data && \ +rm -rf /data/snapshots && \ +rm -rf /data/$LATEST_SNAPSHOT_FILE_NAME + +chown -R bdcuser:bdcuser /data && \ +echo "Sync finished at " $(date) && \ +echo "$(($SECONDS / 60)) minutes and $(($SECONDS % 60)) seconds elapsed." && \ +sudo su bdcuser && \ +/usr/local/bin/docker-compose -f /home/bcuser/node/docker-compose.yml up -d \ No newline at end of file diff --git a/lib/base/lib/assets/sync-checker/syncchecker-base.sh b/lib/base/lib/assets/sync-checker/syncchecker-base.sh new file mode 100644 index 00000000..a084df3b --- /dev/null +++ b/lib/base/lib/assets/sync-checker/syncchecker-base.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +BASE_SYNC_STATS=$(curl -s -X POST -H "Content-Type: application/json" --data '{"jsonrpc":"2.0","method":"eth_syncing","params":[],"id":1}' http://localhost:8545 | jq -r ".result") + +if [[ "$BASE_SYNC_STATS" == "false" ]]; then + BASE_SYNC_BLOCK_HEX=$(curl -s -X POST -H "Content-Type: application/json" --data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' http://localhost:8545 | jq -r ".result") + BASE_HIGHEST_BLOCK_HEX=$BASE_SYNC_BLOCK_HEX +else + BASE_SYNC_BLOCK_HEX=$(echo $BASE_SYNC_STATS | jq -r ".currentBlock") + BASE_HIGHEST_BLOCK_HEX=$(echo $BASE_SYNC_STATS | jq -r ".highestBlock") +fi + +BASE_HIGHEST_BLOCK=$(echo $((${BASE_HIGHEST_BLOCK_HEX}))) +BASE_SYNC_BLOCK=$(echo $((${BASE_SYNC_BLOCK_HEX}))) +BASE_BLOCKS_BEHIND="$((BASE_HIGHEST_BLOCK-BASE_SYNC_BLOCK))" + +# Sending data to CloudWatch +TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600") +INSTANCE_ID=$(curl -H "X-aws-ec2-metadata-token: $TOKEN" -s http://169.254.169.254/latest/meta-data/instance-id) +REGION=$(curl -H "X-aws-ec2-metadata-token: $TOKEN" -s http://169.254.169.254/latest/dynamic/instance-identity/document | jq .region -r) +TIMESTAMP=$(date +"%Y-%m-%dT%H:%M:%S%:z") + +aws cloudwatch put-metric-data --metric-name elc_sync_block --namespace CWAgent --value $BASE_SYNC_BLOCK --timestamp $TIMESTAMP --dimensions InstanceId=$INSTANCE_ID --region $REGION +aws cloudwatch put-metric-data --metric-name elc_blocks_behind --namespace CWAgent --value $BASE_BLOCKS_BEHIND --timestamp $TIMESTAMP --dimensions InstanceId=$INSTANCE_ID --region $REGION diff --git a/lib/base/lib/assets/user-data/node.sh b/lib/base/lib/assets/user-data/node.sh new file mode 100644 index 00000000..94d1ad1c --- /dev/null +++ b/lib/base/lib/assets/user-data/node.sh @@ -0,0 +1,228 @@ +#!/bin/bash +set +e + +# Set by generic single-node and ha-node CDK components +LIFECYCLE_HOOK_NAME=${_LIFECYCLE_HOOK_NAME_} +AUTOSCALING_GROUP_NAME=${_AUTOSCALING_GROUP_NAME_} +RESOURCE_ID=${_NODE_CF_LOGICAL_ID_} +ASSETS_S3_PATH=${_ASSETS_S3_PATH_} +echo "LIFECYCLE_HOOK_NAME=$LIFECYCLE_HOOK_NAME" >> /etc/environment +echo "AUTOSCALING_GROUP_NAME=$AUTOSCALING_GROUP_NAME" >> /etc/environment +echo "ASSETS_S3_PATH=$ASSETS_S3_PATH" >> /etc/environment + +arch=$(uname -m) + +echo "Architecture detected: $arch" + +if [ "$arch" == "x86_64" ]; then + SSM_AGENT_BINARY_URI=https://s3.amazonaws.com/ec2-downloads-windows/SSMAgent/latest/linux_amd64/amazon-ssm-agent.rpm + AWS_CLI_BINARY_URI=https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip + S5CMD_URI=https://github.com/peak/s5cmd/releases/download/v2.1.0/s5cmd_2.1.0_Linux-64bit.tar.gz + YQ_URI=https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 +else + SSM_AGENT_BINARY_URI=https://s3.amazonaws.com/ec2-downloads-windows/SSMAgent/latest/linux_arm64/amazon-ssm-agent.rpm + AWS_CLI_BINARY_URI=https://awscli.amazonaws.com/awscli-exe-linux-aarch64.zip + S5CMD_URI=https://github.com/peak/s5cmd/releases/download/v2.1.0/s5cmd_2.1.0_Linux-arm64.tar.gz + YQ_URI=https://github.com/mikefarah/yq/releases/latest/download/yq_linux_arm64 +fi + +echo "Updating and installing required system packages" +yum update -y +yum -y install amazon-cloudwatch-agent collectd jq yq gcc ncurses-devel aws-cfn-bootstrap zstd +wget $YQ_URI -O /usr/bin/yq && chmod +x /usr/bin/yq + +cd /opt + +echo "Downloading assets zip file" +aws s3 cp $ASSETS_S3_PATH ./assets.zip +unzip -q assets.zip + +echo 'Configuring CloudWatch Agent' +cp /opt/cw-agent.json /opt/aws/amazon-cloudwatch-agent/etc/custom-amazon-cloudwatch-agent.json + +echo "Starting CloudWatch Agent" +/opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl \ +-a fetch-config -c file:/opt/aws/amazon-cloudwatch-agent/etc/custom-amazon-cloudwatch-agent.json -m ec2 -s +systemctl status amazon-cloudwatch-agent + +echo 'Uninstalling AWS CLI v1' +yum remove awscli + +echo 'Installing AWS CLI v2' +curl $AWS_CLI_BINARY_URI -o "awscliv2.zip" +unzip -q awscliv2.zip +./aws/install +rm /usr/bin/aws +ln /usr/local/bin/aws /usr/bin/aws + +aws configure set default.s3.max_concurrent_requests 50 +aws configure set default.s3.multipart_chunksize 256MB + +echo 'Installing SSM Agent' +yum install -y $SSM_AGENT_BINARY_URI + +echo "Installing s5cmd" +cd /opt +wget -q $S5CMD_URI -O s5cmd.tar.gz +tar -xf s5cmd.tar.gz +chmod +x s5cmd +mv s5cmd /usr/bin +s5cmd version + +# Base specific setup starts here + +# Set by Base-specic CDK components and stacks +REGION=${_REGION_} +STACK_NAME=${_STACK_NAME_} +AUTOSTART_CONTAINER=${_AUTOSTART_CONTAINER_} +FORMAT_DISK=${_FORMAT_DISK_} +NETWORK_ID=${_NETWORK_ID_} +L1_ENDPOINT=${_L1_ENDPOINT_} + +echo "REGION=$REGION" >> /etc/environment +echo "NETWORK_ID=$NETWORK_ID" >> /etc/environment +echo "L1_ENDPOINT=$L1_ENDPOINT" >> /etc/environment + +GIT_URL=https://github.com/base-org/node.git +SYNC_CHECKER_FILE_NAME=syncchecker-base.sh +SNAPSHOT_S3_PATH=s3://base-snapshots-$NETWORK_ID-archive.s3.amazonaws.com/$(curl https://base-snapshots-$NETWORK_ID-archive.s3.amazonaws.com/latest) + +yum -y install docker python3-pip cronie cronie-anacron gcc python3-devel git +yum -y remove python-requests +pip3 install docker-compose +pip3 install hapless +pip3 uninstall -y urllib3 +pip3 install 'urllib3<2.0' + +echo "Assigning Swap Space" +# Check if a swap file already exists +if [ -f /swapfile ]; then + # Remove the existing swap file + swapoff /swapfile + rm -rf /swapfile +fi + +# Create a new swap file +total_mem=$(grep MemTotal /proc/meminfo | awk '{print $2}') +# Calculate the swap size +swap_size=$((total_mem / 3)) +# Convert the swap size to MB +swap_size_mb=$((swap_size / 1024)) +unit=M +fallocate -l $swap_size_mb$unit /swapfile +chmod 600 /swapfile +mkswap /swapfile +swapon /swapfile + +# Enable the swap space to persist after reboot. +echo "/swapfile none swap sw 0 0" | sudo tee -a /etc/fstab + +sysctl vm.swappiness=6 +sysctl vm.vfs_cache_pressure=10 +echo "vm.swappiness=10" | sudo tee -a /etc/sysctl.conf +echo "vm.vfs_cache_pressure=10" | sudo tee -a /etc/sysctl.conf + +free -h + +mkdir -p /data + +# Creating run user and making sure it has all necessary permissions +groupadd -g 1002 bcuser +useradd -u 1002 -g 1002 -m -s /bin/bash bcuser +usermod -a -G docker bcuser +usermod -a -G docker ec2-user +chown -R bcuser:bcuser /secrets +chmod -R 755 /home/bcuser +chmod -R 755 /secrets + +echo "Starting docker" +service docker start +systemctl enable docker + +echo "Clonning node repo" +cd /home/bcuser +git clone $GIT_URL +cd ./node + +echo "Configuring node" + +case $NETWORK_ID in + "mainnet") + sed -i "s#OP_NODE_L1_ETH_RPC=https://1rpc.io/eth#OP_NODE_L1_ETH_RPC=$L1_ENDPOINT#g" /home/bcuser/node/.env.mainnet + sed -i '/.env.mainnet/s/^#//g' /home/bcuser/node/docker-compose.yml + ;; + "goerli") + sed -i "s#OP_NODE_L1_ETH_RPC=https://Base-goerli-rpc.allthatnode.com#OP_NODE_L1_ETH_RPC=$L1_ENDPOINT#g" /home/bcuser/node/.env.goerli + sed -i "s/.env.goerli/s/^#//g" /home/bcuser/node/docker-compose.yml + ;; + "sepolia") + sed -i "s#OP_NODE_L1_ETH_RPC=https://rpc.sepolia.org#OP_NODE_L1_ETH_RPC=$L1_ENDPOINT#g" /home/bcuser/node/.env.sepolia + sed -i "s/.env.sepolia/s/^#//g" /home/bcuser/node/docker-compose.yml + ;; + *) + echo "Network id is not valid." + exit 1 + ;; +esac + +yq -i '.services.geth.volumes[0] = "/data:/data"' /home/bcuser/node/docker-compose.yml + +chown -R bcuser:bcuser /home/bcuser/node + +echo "Configuring syncchecker script" +cp /opt/sync-checker/$SYNC_CHECKER_FILE_NAME /opt/syncchecker.sh +chmod 766 /opt/syncchecker.sh + +echo "*/5 * * * * /opt/syncchecker.sh" | crontab +crontab -l + +echo "Signaling completion to CloudFormation to continue with volume mount" +/opt/aws/bin/cfn-signal --stack $STACK_NAME --resource $RESOURCE_ID --region $REGION + +echo "Preparing data volume" + +echo "Wait for one minute for the volume to be available" +sleep 60 + +if $(lsblk | grep -q nvme1n1); then + echo "nvme1n1 is found. Configuring attached storage" + + if [ "$FORMAT_DISK" == "false" ]; then + echo "Not creating a new filesystem in the disk. Existing data might be present!!" + else + mkfs -t ext4 /dev/nvme1n1 + fi + + sleep 10 + # Define the line to add to fstab + uuid=$(lsblk -n -o UUID /dev/nvme1n1) + line="UUID=$uuid /data ext4 defaults 0 2" + + # Write the line to fstab + echo $line | sudo tee -a /etc/fstab + + mount -a + +else + echo "nvme1n1 is not found. Not doing anything" +fi + +lsblk -d + +chown -R bcuser:bcuser /data +chmod -R 755 /data + +if [ "$AUTOSTART_CONTAINER" == "false" ]; then + echo "Sync node. Autostart disabled. Start docker-compose manually!" +else + echo "Sync node. Autostart enabled. Starting docker-compose in 3 min." + cd /home/bcuser/node + echo "sudo su bcuser && /usr/local/bin/docker-compose -f /home/bcuser/node/docker-compose.yml up -d" | at now +3 minutes +fi + +# TODO: Add copy data from S3 script +# echo "Restoring data from snapshot" +# chmod 766 /opt/restore-from-snapshot.sh +# echo "/opt/restore-from-snapshot.sh" | at now +3 minutes + +echo "All Done!!" diff --git a/lib/base/lib/common-stack.ts b/lib/base/lib/common-stack.ts new file mode 100644 index 00000000..cb734545 --- /dev/null +++ b/lib/base/lib/common-stack.ts @@ -0,0 +1,71 @@ +import * as cdk from "aws-cdk-lib"; +import * as cdkConstructs from "constructs"; +import * as iam from 'aws-cdk-lib/aws-iam'; +import * as nag from "cdk-nag"; + +export interface BaseCommonStackProps extends cdk.StackProps { + +} + +export class BaseCommonStack extends cdk.Stack { + AWS_STACKNAME = cdk.Stack.of(this).stackName; + AWS_ACCOUNT_ID = cdk.Stack.of(this).account; + + constructor(scope: cdkConstructs.Construct, id: string, props: BaseCommonStackProps) { + super(scope, id, props); + + const region = cdk.Stack.of(this).region; + + const instanceRole = new iam.Role(this, `node-role`, { + assumedBy: new iam.ServicePrincipal("ec2.amazonaws.com"), + managedPolicies: [ + iam.ManagedPolicy.fromAwsManagedPolicyName("AmazonSSMManagedInstanceCore"), + iam.ManagedPolicy.fromAwsManagedPolicyName("CloudWatchAgentServerPolicy"), + ], + }); + + instanceRole.addToPolicy(new iam.PolicyStatement({ + // Can't target specific stack: https://github.com/aws/aws-cdk/issues/22657 + resources: ["*"], + actions: ["cloudformation:SignalResource"], + })); + + instanceRole.addToPolicy(new iam.PolicyStatement({ + resources: [`arn:aws:autoscaling:${region}:${this.AWS_ACCOUNT_ID}:autoScalingGroup:*:autoScalingGroupName/base-*`], + actions: ["autoscaling:CompleteLifecycleAction"], + })); + + instanceRole.addToPolicy( + new iam.PolicyStatement({ + resources: [ + "arn:aws:s3:::base-snapshots-*-archive", + "arn:aws:s3:::base-snapshots-*-archive/*" + ], + actions: ["s3:*Object"], + })); + + new cdk.CfnOutput(this, "Instance Role ARN", { + value: instanceRole.roleArn, + exportName: "BaseNodeInstanceRoleArn", + }); + + /** + * cdk-nag suppressions + */ + + nag.NagSuppressions.addResourceSuppressions( + this, + [ + { + id: "AwsSolutions-IAM4", + reason: "AmazonSSMManagedInstanceCore and CloudWatchAgentServerPolicy are restrictive enough", + }, + { + id: "AwsSolutions-IAM5", + reason: "Can't target specific stack: https://github.com/aws/aws-cdk/issues/22657", + }, + ], + true + ); + } +} diff --git a/lib/base/lib/config/baseConfig.interface.ts b/lib/base/lib/config/baseConfig.interface.ts new file mode 100644 index 00000000..b6a695ba --- /dev/null +++ b/lib/base/lib/config/baseConfig.interface.ts @@ -0,0 +1,22 @@ +import * as configTypes from "../../../constructs/config.interface"; + +export type BaseNetworkId = "mainnet" ; +export type BaseNodeConfiguration = "full" ; + +export {AMBEthereumNodeNetworkId} from "../../../constructs/config.interface"; + +export interface BaseDataVolumeConfig extends configTypes.DataVolumeConfig { +} + +export interface BaseAccountsVolumeConfig extends configTypes.DataVolumeConfig { +} + +export interface BaseBaseConfig extends configTypes.BaseConfig { +} + +export interface BaseBaseNodeConfig extends configTypes.BaseNodeConfig { + ambEntereumNodeNetworkId: configTypes.AMBEthereumNodeNetworkId; + ambEntereumNodeInstanceType: string; + baseNetworkId: BaseNetworkId; + dataVolume: BaseDataVolumeConfig; +} diff --git a/lib/base/lib/config/baseConfig.ts b/lib/base/lib/config/baseConfig.ts new file mode 100644 index 00000000..a8a314ea --- /dev/null +++ b/lib/base/lib/config/baseConfig.ts @@ -0,0 +1,38 @@ +import * as ec2 from "aws-cdk-lib/aws-ec2"; +import * as configTypes from "./baseConfig.interface"; +import * as constants from "../../../constructs/constants"; + + +const parseDataVolumeType = (dataVolumeType: string) => { + switch (dataVolumeType) { + case "gp3": + return ec2.EbsDeviceVolumeType.GP3; + case "io2": + return ec2.EbsDeviceVolumeType.IO2; + case "io1": + return ec2.EbsDeviceVolumeType.IO1; + case "instance-store": + return constants.InstanceStoreageDeviceVolumeType; + default: + return ec2.EbsDeviceVolumeType.GP3; + } +} + +export const baseConfig: configTypes.BaseBaseConfig = { + accountId: process.env.AWS_ACCOUNT_ID || "xxxxxxxxxxx", + region: process.env.AWS_REGION || "us-east-2", +} + +export const baseNodeConfig: configTypes.BaseBaseNodeConfig = { + ambEntereumNodeNetworkId: process.env.AMB_ENTEREUM_NODE_NETWORK_ID || "mainnet", + ambEntereumNodeInstanceType: process.env.AMB_ETHEREUM_NODE_INSTANCE_TYPE || "bc.m5.xlarge", + instanceType: new ec2.InstanceType(process.env.BASE_INSTANCE_TYPE ? process.env.BASE_INSTANCE_TYPE : "m6a.2xlarge"), + instanceCpuType: process.env.BASE_CPU_TYPE?.toLowerCase() == "x86_64" ? ec2.AmazonLinuxCpuType.X86_64 : ec2.AmazonLinuxCpuType.ARM_64, + baseNetworkId: process.env.BASE_NETWORK_ID || "mainnet", + dataVolume: { + sizeGiB: process.env.BASE_DATA_VOL_SIZE ? parseInt(process.env.BASE_DATA_VOL_SIZE): 1000, + type: parseDataVolumeType(process.env.BASE_DATA_VOL_TYPE?.toLowerCase() ? process.env.BASE_DATA_VOL_TYPE?.toLowerCase() : "gp3"), + iops: process.env.BASE_DATA_VOL_IOPS ? parseInt(process.env.BASE_DATA_VOL_IOPS): 5000, + throughput: process.env.BASE_DATA_VOL_THROUGHPUT ? parseInt(process.env.BASE_DATA_VOL_THROUGHPUT): 700, + }, +}; diff --git a/lib/base/lib/constructs/base-node-security-group.ts b/lib/base/lib/constructs/base-node-security-group.ts new file mode 100644 index 00000000..41e43bb1 --- /dev/null +++ b/lib/base/lib/constructs/base-node-security-group.ts @@ -0,0 +1,34 @@ +import * as cdk from "aws-cdk-lib"; +import * as cdkConstructs from 'constructs'; +import * as ec2 from "aws-cdk-lib/aws-ec2"; + +export interface BaseNodeSecurityGroupConstructProps { + vpc: cdk.aws_ec2.IVpc; + } + + export class BaseNodeSecurityGroupConstruct extends cdkConstructs.Construct { + public securityGroup: cdk.aws_ec2.ISecurityGroup; + + constructor(scope: cdkConstructs.Construct, id: string, props: BaseNodeSecurityGroupConstructProps) { + super(scope, id); + + const { + vpc, + } = props; + + const sg = new ec2.SecurityGroup(this, `rpc-node-security-group`, { + vpc, + description: "Security Group for Blockchain nodes", + allowAllOutbound: true, + }); + + // Public ports + sg.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(9222), "P2P"); + sg.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.udp(9222), "P2P"); + + // Private port + sg.addIngressRule(ec2.Peer.ipv4(vpc.vpcCidrBlock), ec2.Port.tcp(8545), "Base Client RPC"); + + this.securityGroup = sg + } + } diff --git a/lib/base/lib/single-node-stack.ts b/lib/base/lib/single-node-stack.ts new file mode 100644 index 00000000..e7e64dbc --- /dev/null +++ b/lib/base/lib/single-node-stack.ts @@ -0,0 +1,136 @@ +import * as cdk from "aws-cdk-lib"; +import * as cdkConstructs from "constructs"; +import * as ec2 from "aws-cdk-lib/aws-ec2"; +import * as iam from "aws-cdk-lib/aws-iam"; +import * as s3Assets from "aws-cdk-lib/aws-s3-assets"; +import * as path from "path"; +import * as fs from "fs"; +import * as nodeCwDashboard from "./assets/node-cw-dashboard" +import * as cw from 'aws-cdk-lib/aws-cloudwatch'; +import * as nag from "cdk-nag"; +import { SingleNodeConstruct } from "../../constructs/single-node" +import * as configTypes from "./config/baseConfig.interface"; +import * as constants from "../../constructs/constants"; +import { BaseNodeSecurityGroupConstruct } from "./constructs/base-node-security-group"; + +export interface BaseSingleNodeStackProps extends cdk.StackProps { + instanceType: ec2.InstanceType; + instanceCpuType: ec2.AmazonLinuxCpuType; + baseNetworkId: configTypes.BaseNetworkId; + dataVolume: configTypes.BaseDataVolumeConfig; +} + +export class BaseSingleNodeStack extends cdk.Stack { + constructor(scope: cdkConstructs.Construct, id: string, props: BaseSingleNodeStackProps) { + super(scope, id, props); + + // Setting up necessary environment variables + const REGION = cdk.Stack.of(this).region; + const STACK_NAME = cdk.Stack.of(this).stackName; + const STACK_ID = cdk.Stack.of(this).stackId; + const availabilityZones = cdk.Stack.of(this).availabilityZones; + const chosenAvailabilityZone = availabilityZones.slice(0, 1)[0]; + + // Getting our config from initialization properties + const { + instanceType, + instanceCpuType, + baseNetworkId, + dataVolume, + } = props; + + // Using default VPC + const vpc = ec2.Vpc.fromLookup(this, "vpc", { isDefault: true }); + + // Setting up the security group for the node from Base-specific construct + const instanceSG = new BaseNodeSecurityGroupConstruct (this, "security-group", { + vpc: vpc, + }) + + // Making our scripts and configis from the local "assets" directory available for instance to download + const asset = new s3Assets.Asset(this, "assets", { + path: path.join(__dirname, "assets"), + }); + + // Getting the IAM role ARN from the common stack + const importedInstanceRoleArn = cdk.Fn.importValue("BaseNodeInstanceRoleArn"); + const ambEthereumNodeRpcUrlWithBillingToken = cdk.Fn.importValue("BaseAmbEthereumNodeRpcUrlWithBillingToken"); + + const instanceRole = iam.Role.fromRoleArn(this, "iam-role", importedInstanceRoleArn); + + // Making sure our instance will be able to read the assets + asset.bucket.grantRead(instanceRole); + + // Setting up the node using generic Single Node constract + if (instanceCpuType === ec2.AmazonLinuxCpuType.ARM_64) { + throw new Error("ARM_64 is not yet supported"); + } + + const node = new SingleNodeConstruct(this, "rpc-node", { + instanceName: STACK_NAME, + instanceType, + dataVolumes: [dataVolume], + rootDataVolumeDeviceName: "/dev/xvda", + machineImage: new ec2.AmazonLinuxImage({ + generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2, + cpuType: instanceCpuType, + }), + vpc, + availabilityZone: chosenAvailabilityZone, + role: instanceRole, + securityGroup: instanceSG.securityGroup, + vpcSubnets: { + subnetType: ec2.SubnetType.PUBLIC, + }, + }); + + // Parsing user data script and injecting necessary variables + const nodeStartScript = fs.readFileSync(path.join(__dirname, "assets", "user-data", "node.sh")).toString(); + const dataVolumeSizeBytes = dataVolume.sizeGiB * constants.GibibytesToBytesConversionCoefficient; + + const modifiedInitNodeScript = cdk.Fn.sub(nodeStartScript, { + _REGION_: REGION, + _ASSETS_S3_PATH_: `s3://${asset.s3BucketName}/${asset.s3ObjectKey}`, + _STACK_NAME_: STACK_NAME, + _NODE_CF_LOGICAL_ID_: node.nodeCFLogicalId, + _DATA_VOLUME_TYPE_: dataVolume.type, + _DATA_VOLUME_SIZE_: dataVolumeSizeBytes.toString(), + _NETWORK_ID_: baseNetworkId, + _LIFECYCLE_HOOK_NAME_: constants.NoneValue, + _AUTOSCALING_GROUP_NAME_: constants.NoneValue, + _AUTOSTART_CONTAINER_: "true", + _FORMAT_DISK_: "true", + _L1_ENDPOINT_: ambEthereumNodeRpcUrlWithBillingToken, + }); + + node.instance.addUserData(modifiedInitNodeScript); + + // Adding CloudWatch dashboard to the node + const dashboardString = cdk.Fn.sub(JSON.stringify(nodeCwDashboard.SyncNodeCWDashboardJSON), { + INSTANCE_ID:node.instanceId, + INSTANCE_NAME: STACK_NAME, + REGION: REGION, + }) + + new cw.CfnDashboard(this, 'base-cw-dashboard', { + dashboardName: STACK_NAME, + dashboardBody: dashboardString, + }); + + new cdk.CfnOutput(this, "node-instance-id", { + value: node.instanceId, + }); + + // Adding suppressions to the stack + nag.NagSuppressions.addResourceSuppressions( + this, + [ + { + id: "AwsSolutions-IAM5", + reason: "Need read access to the S3 bucket with assets", + }, + ], + true + ); + } +} diff --git a/lib/base/package-lock.json b/lib/base/package-lock.json new file mode 100644 index 00000000..5d2c6444 --- /dev/null +++ b/lib/base/package-lock.json @@ -0,0 +1,641 @@ +{ + "name": "aws-blockchain-node-runners-base", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "aws-blockchain-node-runners-base", + "version": "0.1.0", + "dependencies": { + "@types/node": "^20.10.0" + }, + "devDependencies": { + "@types/jest": "^29.5.11" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", + "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.23.4", + "chalk": "^2.4.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/code-frame/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/code-frame/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/@babel/code-frame/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/code-frame/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", + "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/@babel/highlight/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "29.5.11", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.11.tgz", + "integrity": "sha512-S2mHmYIVe13vrm6q4kN6fLYYAka15ALQki/vgDC3mIukEOx8WJlv0kQPM+d4w8Gp6u0uSdKND04IlTXBv0rwnQ==", + "dev": true, + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "node_modules/@types/node": { + "version": "20.10.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.5.tgz", + "integrity": "sha512-nNPsNE65wjMxEKI93yOP+NPGGBJz/PoN3kZsVLee0XMiJolxSekEVD8wRwBUBqkwc7UWop0edW50yrCQW4CyRw==", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true + }, + "node_modules/@types/yargs": { + "version": "17.0.32", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", + "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + } + } +} diff --git a/lib/base/package.json b/lib/base/package.json new file mode 100644 index 00000000..3fb94aea --- /dev/null +++ b/lib/base/package.json @@ -0,0 +1,20 @@ +{ + "name": "aws-blockchain-node-runners-base", + "version": "0.1.0", + "scripts": { + "build": "tsc", + "watch": "tsc -w", + "test": "jest", + "cdk": "cdk", + "cdk_deploy_common": "cdk deploy base-common", + "cdk_synth_single_node": "cdk synth base-single-node", + "cdk_deploy_single_node": "cdk deploy base-single-node", + "cdk_destroy_single_node": "cdk destroy base-single-node" + }, + "dependencies": { + "@types/node": "^20.10.0" + }, + "devDependencies": { + "@types/jest": "^29.5.11" + } +} diff --git a/lib/base/sample-configs/.env-sample-rpc b/lib/base/sample-configs/.env-sample-rpc new file mode 100644 index 00000000..579e2e69 --- /dev/null +++ b/lib/base/sample-configs/.env-sample-rpc @@ -0,0 +1,20 @@ +############################################################# +# Example configuration for Base nodes runner app on AWS # +############################################################# + +## Set the AWS account is and region for your environment ## +AWS_ACCOUNT_ID="xxxxxxxx" +AWS_REGION="us-east-1" # Regions supported by Amazon Managed Blockchain Access Ethereum: https://docs.aws.amazon.com/general/latest/gr/managedblockchain.html#managedblockchain-access + +## Common configuration parameters ## +AMB_ETHEREUM_NODE_NETWORK_ID="mainnet" # All options: "mainnet", "goerli" +AMB_ETHEREUM_NODE_INSTANCE_TYPE="bc.m5.xlarge" # For available options see: https://aws.amazon.com/managed-blockchain/instance-types/ +BASE_NETWORK_ID="mainnet" # All options: "mainnet", "goerli" + +BASE_INSTANCE_TYPE="m6a.2xlarge" +BASE_CPU_TYPE="x86_64" # All options: "x86_64", "ARM_64". IMPORTANT: Make sure the CPU type matches the instance type used +# Data volume configuration +BASE_DATA_VOL_TYPE="gp3" # Other options: "io1" | "io2" | "gp3" | "instance-store" . IMPORTANT: Use "instance-store" option only with instance types that support that feature, like popular for node im4gn, d3, i3en, and i4i instance families +BASE_DATA_VOL_SIZE="1000" # Current required data size to keep both snapshot archive and unarchived version of it +BASE_DATA_VOL_IOPS="3000" # Max IOPS for EBS volumes (not applicable for "instance-store") +BASE_DATA_VOL_THROUGHPUT="700" # Max throughput for EBS gp3 volumes (not applicable for "io1" | "io2" | "instance-store") diff --git a/lib/base/test/.env-test b/lib/base/test/.env-test new file mode 100644 index 00000000..fb62be98 --- /dev/null +++ b/lib/base/test/.env-test @@ -0,0 +1,22 @@ +############################################################# +# Example configuration for Base nodes runner app on AWS # +############################################################# + +## Set the AWS account is and region for your environment ## +AWS_ACCOUNT_ID="347616198663" +AWS_REGION="us-east-1" # Regions supported by Amazon Managed Blockchain Access Ethereum: https://docs.aws.amazon.com/general/latest/gr/managedblockchain.html#managedblockchain-access + +## Common configuration parameters ## +AMB_ETHEREUM_NODE_NETWORK_ID="mainnet" # All options: "mainnet", "goerli" +AMB_ETHEREUM_NODE_INSTANCE_TYPE="bc.m5.xlarge" # For available options see: https://aws.amazon.com/managed-blockchain/instance-types/ +BASE_NETWORK_ID="mainnet" # All options: "mainnet" +BASE_NODE_CONFIGURATION="full" # All options: "full" +BASE_VERSION="TODO: TBA" # Current required version of Docker Compose file for Base node + +BASE_INSTANCE_TYPE="m6a.2xlarge" +BASE_CPU_TYPE="x86_64" # All options: "x86_64", "ARM_64". IMPORTANT: Make sure the CPU type matches the instance type used +# Data volume configuration +BASE_DATA_VOL_TYPE="gp3" # Other options: "io1" | "io2" | "gp3" | "instance-store" . IMPORTANT: Use "instance-store" option only with instance types that support that feature, like popular for node im4gn, d3, i3en, and i4i instance families +BASE_DATA_VOL_SIZE="1000" # Current required data size to keep both snapshot archive and unarchived version of it +BASE_DATA_VOL_IOPS="3000" # Max IOPS for EBS volumes (not applicable for "instance-store") +BASE_DATA_VOL_THROUGHPUT="700" # Max throughput for EBS gp3 volumes (not applicable for "io1" | "io2" | "instance-store") diff --git a/lib/base/test/base-ethereum-l1-node.test.ts b/lib/base/test/base-ethereum-l1-node.test.ts new file mode 100644 index 00000000..25ba1bf6 --- /dev/null +++ b/lib/base/test/base-ethereum-l1-node.test.ts @@ -0,0 +1,57 @@ +import {Match, Template} from "aws-cdk-lib/assertions"; +import * as cdk from "aws-cdk-lib"; +import * as dotenv from 'dotenv'; + +dotenv.config({path: './test/.env-test'}); +import * as config from "../lib/config/baseConfig"; +import {BaseAMBEthereumSingleNodeStack} from "../lib/amb-ethereum-single-node-stack"; + +describe("BaseAMBEthereumSingleNodeStack", () => { + let app: cdk.App; + let baseAMBEthereumSingleNode: BaseAMBEthereumSingleNodeStack; + let template: Template; + beforeAll(() => { + app = new cdk.App(); + + // Create the BaseAMBEthereumSingleNodeStack. + + baseAMBEthereumSingleNode = new BaseAMBEthereumSingleNodeStack(app, "base-ethereum-l1-node", { + stackName: `base-amb-ethereum-single-node-${config.baseNodeConfig.nodeConfiguration}`, + env: { account: config.baseConfig.accountId, region: config.baseConfig.region }, + + ambEthereumNodeNetworkId: config.baseNodeConfig.ambEntereumNodeNetworkId, + ambEthereumNodeInstanceType: config.baseNodeConfig.ambEntereumNodeInstanceType, + }); + + template = Template.fromStack(baseAMBEthereumSingleNode); + }); + + test("Check Node URL is correct", () => { + template.hasOutput("ambethnoderpcurlbillingtoken", { + Value: { + "Fn::Join": [ + "", + [ + "https://", + { + "Fn::GetAtt": [ + Match.anyValue(), + "NodeId" + ] + }, + ".t.ethereum.managedblockchain.us-east-1.amazonaws.com?billingtoken=", + { + "Fn::GetAtt": [ + Match.anyValue(), + "BillingToken" + ] + } + ] + ] + }, + "Export": { + "Name": "AmbEthereumNodeRpcUrlWithBillingToken" + } + }) + }); +}); diff --git a/lib/base/test/base-single-node.test.ts b/lib/base/test/base-single-node.test.ts new file mode 100644 index 00000000..a8e1562c --- /dev/null +++ b/lib/base/test/base-single-node.test.ts @@ -0,0 +1,123 @@ +import {Match, Template} from "aws-cdk-lib/assertions"; +import * as cdk from "aws-cdk-lib"; +import * as dotenv from 'dotenv'; + +dotenv.config({path: './test/.env-test'}); +import * as config from "../lib/config/baseConfig"; +import {BaseSingleNodeStack} from "../lib/single-node-stack"; + +describe("BaseSingleNodeStack", () => { + let app: cdk.App; + let baseSingleNodeStack: BaseSingleNodeStack; + let template: Template; + beforeAll(() => { + app = new cdk.App(); + + // Create the BaseSingleNodeStack. + baseSingleNodeStack = new BaseSingleNodeStack(app, "base-single-node", { + stackName: `base-single-node-${config.baseConfig.accountId}`, + env: {account: config.baseConfig.accountId, region: config.baseConfig.region}, + + instanceType: config.baseNodeConfig.instanceType, + instanceCpuType: config.baseNodeConfig.instanceCpuType, + baseVersion: config.baseNodeConfig.baseVersion, + nodeConfiguration: config.baseNodeConfig.nodeConfiguration, + dataVolume: config.baseNodeConfig.dataVolume, + baseNetworkId: config.baseNodeConfig.baseNetworkId + }); + + template = Template.fromStack(baseSingleNodeStack); + }); + + test("Check Security Group", () => { + // Has EC2 instance security group. + template.hasResourceProperties("AWS::EC2::SecurityGroup", { + GroupDescription: Match.anyValue(), + VpcId: Match.anyValue(), + SecurityGroupEgress: [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + SecurityGroupIngress: [ + { + "CidrIp": "0.0.0.0/0", + "Description": "P2P", + "FromPort": 9222, + "IpProtocol": "tcp", + "ToPort": 9222 + }, + { + "CidrIp": "0.0.0.0/0", + "Description": "P2P", + "FromPort": 9222, + "IpProtocol": "udp", + "ToPort": 9222 + }, + { + "CidrIp": "1.2.3.4/5", + "Description": "Base Client RPC", + "FromPort": 8545, + "IpProtocol": "tcp", + "ToPort": 8545 + } + ] + }) + }); + + + test("Check EC2 Settings", () => { + // Has EC2 instance with node configuration + template.hasResourceProperties("AWS::EC2::Instance", { + AvailabilityZone: Match.anyValue(), + UserData: Match.anyValue(), + BlockDeviceMappings: [ + { + DeviceName: "/dev/sda1", + Ebs: { + DeleteOnTermination: true, + Encrypted: true, + Iops: 3000, + VolumeSize: 46, + VolumeType: "gp3" + } + } + ], + IamInstanceProfile: Match.anyValue(), + ImageId: Match.anyValue(), + InstanceType: "m6a.2xlarge", + Monitoring: true, + PropagateTagsToVolumeOnCreation: true, + SecurityGroupIds: Match.anyValue(), + SubnetId: Match.anyValue(), + }); + + // // Has EBS data volume. + template.hasResourceProperties("AWS::EC2::Volume", { + AvailabilityZone: Match.anyValue(), + Encrypted: true, + Iops: 3000, + MultiAttachEnabled: false, + Size: 1000, + Throughput: 700, + VolumeType: "gp3" + }) + + // Has EBS data volume attachment. + template.hasResourceProperties("AWS::EC2::VolumeAttachment", { + Device: "/dev/sdf", + InstanceId: Match.anyValue(), + VolumeId: Match.anyValue(), + }) + }); + + test("Check CloudWatch Dashboard", () => { + // Has CloudWatch dashboard. + template.hasResourceProperties("AWS::CloudWatch::Dashboard", { + DashboardBody: Match.anyValue(), + DashboardName: `base-single-node-${config.baseConfig.accountId}` + }) + }); +}); diff --git a/lib/base/tsconfig.json b/lib/base/tsconfig.json new file mode 100644 index 00000000..aaa7dc51 --- /dev/null +++ b/lib/base/tsconfig.json @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": [ + "es2020", + "dom" + ], + "declaration": true, + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "noImplicitThis": true, + "alwaysStrict": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": false, + "inlineSourceMap": true, + "inlineSources": true, + "experimentalDecorators": true, + "strictPropertyInitialization": false, + "typeRoots": [ + "./node_modules/@types" + ] + }, + "exclude": [ + "node_modules", + "cdk.out" + ] +} From 4f19f17a5bcd4812fb2b370f1039a67d09832801 Mon Sep 17 00:00:00 2001 From: Nikolay Vlasov Date: Thu, 18 Jan 2024 16:53:28 +1100 Subject: [PATCH 32/75] [WIP]Improvements to Base blueprint passing smoke test --- lib/base/README.md | 3 +++ lib/base/app.ts | 1 + lib/base/lib/assets/restore-from-snapshot.sh | 6 +++--- lib/base/lib/assets/user-data/node.sh | 19 ++++++++----------- lib/base/lib/config/baseConfig.interface.ts | 1 + lib/base/lib/config/baseConfig.ts | 1 + lib/base/lib/single-node-stack.ts | 4 +++- lib/base/sample-configs/.env-sample-rpc | 5 +++-- lib/base/test/.env-test | 2 -- lib/base/test/base-ethereum-l1-node.test.ts | 4 ++-- lib/base/test/base-single-node.test.ts | 10 ++++------ 11 files changed, 29 insertions(+), 27 deletions(-) diff --git a/lib/base/README.md b/lib/base/README.md index dac10bb6..a78b2c05 100644 --- a/lib/base/README.md +++ b/lib/base/README.md @@ -208,6 +208,9 @@ A script on the Base node publishes current block and blocks behind metrics to C export AWS_REGION=us-east-1 aws ssm start-session --target $INSTANCE_ID --region $AWS_REGION sudo su bcuser + # Geth logs: + docker logs --tail 50 node_geth_1 -f + # Base logs: docker logs --tail 50 node_node_1 -f ``` 2. How to check the logs from the EC2 user-data script? diff --git a/lib/base/app.ts b/lib/base/app.ts index 8eea3432..a5c048e3 100644 --- a/lib/base/app.ts +++ b/lib/base/app.ts @@ -30,5 +30,6 @@ new BaseSingleNodeStack(app, "base-single-node", { instanceType: config.baseNodeConfig.instanceType, instanceCpuType: config.baseNodeConfig.instanceCpuType, baseNetworkId: config.baseNodeConfig.baseNetworkId, + restoreFromSnapshot: config.baseNodeConfig.restoreFromSnapshot, dataVolume: config.baseNodeConfig.dataVolume, }); diff --git a/lib/base/lib/assets/restore-from-snapshot.sh b/lib/base/lib/assets/restore-from-snapshot.sh index 0460fda8..0d9b14be 100644 --- a/lib/base/lib/assets/restore-from-snapshot.sh +++ b/lib/base/lib/assets/restore-from-snapshot.sh @@ -9,13 +9,13 @@ echo "Sync started at " $(date) SECONDS=0 s5cmd --log error cp s3://base-snapshots-$NETWORK_ID-archive/$LATEST_SNAPSHOT_FILE_NAME /data && \ -tar -I zstd -xf /data/$LATEST_SNAPSHOT_FILE_NAME -C /data && \ +tar -I zstdmt -xf /data/$LATEST_SNAPSHOT_FILE_NAME -C /data && \ mv /data/snapshots/$NETWORK_ID/download/* /data && \ rm -rf /data/snapshots && \ rm -rf /data/$LATEST_SNAPSHOT_FILE_NAME -chown -R bdcuser:bdcuser /data && \ +chown -R bcuser:bcuser /data && \ echo "Sync finished at " $(date) && \ echo "$(($SECONDS / 60)) minutes and $(($SECONDS % 60)) seconds elapsed." && \ -sudo su bdcuser && \ +sudo su bcuser && \ /usr/local/bin/docker-compose -f /home/bcuser/node/docker-compose.yml up -d \ No newline at end of file diff --git a/lib/base/lib/assets/user-data/node.sh b/lib/base/lib/assets/user-data/node.sh index 94d1ad1c..daf9a26e 100644 --- a/lib/base/lib/assets/user-data/node.sh +++ b/lib/base/lib/assets/user-data/node.sh @@ -74,7 +74,7 @@ s5cmd version # Set by Base-specic CDK components and stacks REGION=${_REGION_} STACK_NAME=${_STACK_NAME_} -AUTOSTART_CONTAINER=${_AUTOSTART_CONTAINER_} +RESTORE_FROM_SNAPSHOT=${_RESTORE_FROM_SNAPSHOT_} FORMAT_DISK=${_FORMAT_DISK_} NETWORK_ID=${_NETWORK_ID_} L1_ENDPOINT=${_L1_ENDPOINT_} @@ -165,7 +165,7 @@ case $NETWORK_ID in ;; esac -yq -i '.services.geth.volumes[0] = "/data:/data"' /home/bcuser/node/docker-compose.yml +sed -i "s#GETH_HOST_DATA_DIR=./geth-data#GETH_HOST_DATA_DIR=/data/geth#g" /home/bcuser/node/.env chown -R bcuser:bcuser /home/bcuser/node @@ -212,17 +212,14 @@ lsblk -d chown -R bcuser:bcuser /data chmod -R 755 /data -if [ "$AUTOSTART_CONTAINER" == "false" ]; then - echo "Sync node. Autostart disabled. Start docker-compose manually!" -else - echo "Sync node. Autostart enabled. Starting docker-compose in 3 min." +if [ "$RESTORE_FROM_SNAPSHOT" == "false" ]; then + echo "Skipping restoration from snapshot. Starting docker-compose in 3 min." cd /home/bcuser/node echo "sudo su bcuser && /usr/local/bin/docker-compose -f /home/bcuser/node/docker-compose.yml up -d" | at now +3 minutes +else + echo "Restoring data from snapshot" + chmod 766 /opt/restore-from-snapshot.sh + echo "/opt/restore-from-snapshot.sh" | at now +3 minutes fi -# TODO: Add copy data from S3 script -# echo "Restoring data from snapshot" -# chmod 766 /opt/restore-from-snapshot.sh -# echo "/opt/restore-from-snapshot.sh" | at now +3 minutes - echo "All Done!!" diff --git a/lib/base/lib/config/baseConfig.interface.ts b/lib/base/lib/config/baseConfig.interface.ts index b6a695ba..6dbf1e33 100644 --- a/lib/base/lib/config/baseConfig.interface.ts +++ b/lib/base/lib/config/baseConfig.interface.ts @@ -19,4 +19,5 @@ export interface BaseBaseNodeConfig extends configTypes.BaseNodeConfig { ambEntereumNodeInstanceType: string; baseNetworkId: BaseNetworkId; dataVolume: BaseDataVolumeConfig; + restoreFromSnapshot: boolean; } diff --git a/lib/base/lib/config/baseConfig.ts b/lib/base/lib/config/baseConfig.ts index a8a314ea..44ed175c 100644 --- a/lib/base/lib/config/baseConfig.ts +++ b/lib/base/lib/config/baseConfig.ts @@ -29,6 +29,7 @@ export const baseNodeConfig: configTypes.BaseBaseNodeConfig = { instanceType: new ec2.InstanceType(process.env.BASE_INSTANCE_TYPE ? process.env.BASE_INSTANCE_TYPE : "m6a.2xlarge"), instanceCpuType: process.env.BASE_CPU_TYPE?.toLowerCase() == "x86_64" ? ec2.AmazonLinuxCpuType.X86_64 : ec2.AmazonLinuxCpuType.ARM_64, baseNetworkId: process.env.BASE_NETWORK_ID || "mainnet", + restoreFromSnapshot: process.env.BASE_RESTORE_FROM_SNAPSHOT?.toLowerCase() == "true" ? true : false, dataVolume: { sizeGiB: process.env.BASE_DATA_VOL_SIZE ? parseInt(process.env.BASE_DATA_VOL_SIZE): 1000, type: parseDataVolumeType(process.env.BASE_DATA_VOL_TYPE?.toLowerCase() ? process.env.BASE_DATA_VOL_TYPE?.toLowerCase() : "gp3"), diff --git a/lib/base/lib/single-node-stack.ts b/lib/base/lib/single-node-stack.ts index e7e64dbc..814edac3 100644 --- a/lib/base/lib/single-node-stack.ts +++ b/lib/base/lib/single-node-stack.ts @@ -17,6 +17,7 @@ export interface BaseSingleNodeStackProps extends cdk.StackProps { instanceType: ec2.InstanceType; instanceCpuType: ec2.AmazonLinuxCpuType; baseNetworkId: configTypes.BaseNetworkId; + restoreFromSnapshot: boolean; dataVolume: configTypes.BaseDataVolumeConfig; } @@ -36,6 +37,7 @@ export class BaseSingleNodeStack extends cdk.Stack { instanceType, instanceCpuType, baseNetworkId, + restoreFromSnapshot, dataVolume, } = props; @@ -98,7 +100,7 @@ export class BaseSingleNodeStack extends cdk.Stack { _NETWORK_ID_: baseNetworkId, _LIFECYCLE_HOOK_NAME_: constants.NoneValue, _AUTOSCALING_GROUP_NAME_: constants.NoneValue, - _AUTOSTART_CONTAINER_: "true", + _RESTORE_FROM_SNAPSHOT_: restoreFromSnapshot.toString(), _FORMAT_DISK_: "true", _L1_ENDPOINT_: ambEthereumNodeRpcUrlWithBillingToken, }); diff --git a/lib/base/sample-configs/.env-sample-rpc b/lib/base/sample-configs/.env-sample-rpc index 579e2e69..daca3b70 100644 --- a/lib/base/sample-configs/.env-sample-rpc +++ b/lib/base/sample-configs/.env-sample-rpc @@ -15,6 +15,7 @@ BASE_INSTANCE_TYPE="m6a.2xlarge" BASE_CPU_TYPE="x86_64" # All options: "x86_64", "ARM_64". IMPORTANT: Make sure the CPU type matches the instance type used # Data volume configuration BASE_DATA_VOL_TYPE="gp3" # Other options: "io1" | "io2" | "gp3" | "instance-store" . IMPORTANT: Use "instance-store" option only with instance types that support that feature, like popular for node im4gn, d3, i3en, and i4i instance families -BASE_DATA_VOL_SIZE="1000" # Current required data size to keep both snapshot archive and unarchived version of it -BASE_DATA_VOL_IOPS="3000" # Max IOPS for EBS volumes (not applicable for "instance-store") +BASE_DATA_VOL_SIZE="3000" # Current required data size to keep both snapshot archive and unarchived version of it +BASE_DATA_VOL_IOPS="5000" # Max IOPS for EBS volumes (not applicable for "instance-store") BASE_DATA_VOL_THROUGHPUT="700" # Max throughput for EBS gp3 volumes (not applicable for "io1" | "io2" | "instance-store") +BASE_RESTORE_FROM_SNAPSHOT="false" # Download snapshot to speed up statup time \ No newline at end of file diff --git a/lib/base/test/.env-test b/lib/base/test/.env-test index fb62be98..70552ed3 100644 --- a/lib/base/test/.env-test +++ b/lib/base/test/.env-test @@ -10,8 +10,6 @@ AWS_REGION="us-east-1" # Regions supported by Amazon Ma AMB_ETHEREUM_NODE_NETWORK_ID="mainnet" # All options: "mainnet", "goerli" AMB_ETHEREUM_NODE_INSTANCE_TYPE="bc.m5.xlarge" # For available options see: https://aws.amazon.com/managed-blockchain/instance-types/ BASE_NETWORK_ID="mainnet" # All options: "mainnet" -BASE_NODE_CONFIGURATION="full" # All options: "full" -BASE_VERSION="TODO: TBA" # Current required version of Docker Compose file for Base node BASE_INSTANCE_TYPE="m6a.2xlarge" BASE_CPU_TYPE="x86_64" # All options: "x86_64", "ARM_64". IMPORTANT: Make sure the CPU type matches the instance type used diff --git a/lib/base/test/base-ethereum-l1-node.test.ts b/lib/base/test/base-ethereum-l1-node.test.ts index 25ba1bf6..bdab1a90 100644 --- a/lib/base/test/base-ethereum-l1-node.test.ts +++ b/lib/base/test/base-ethereum-l1-node.test.ts @@ -16,7 +16,7 @@ describe("BaseAMBEthereumSingleNodeStack", () => { // Create the BaseAMBEthereumSingleNodeStack. baseAMBEthereumSingleNode = new BaseAMBEthereumSingleNodeStack(app, "base-ethereum-l1-node", { - stackName: `base-amb-ethereum-single-node-${config.baseNodeConfig.nodeConfiguration}`, + stackName: `base-amb-ethereum-single-node-${config.baseNodeConfig.baseNetworkId}`, env: { account: config.baseConfig.accountId, region: config.baseConfig.region }, ambEthereumNodeNetworkId: config.baseNodeConfig.ambEntereumNodeNetworkId, @@ -50,7 +50,7 @@ describe("BaseAMBEthereumSingleNodeStack", () => { ] }, "Export": { - "Name": "AmbEthereumNodeRpcUrlWithBillingToken" + "Name": "BaseAmbEthereumNodeRpcUrlWithBillingToken" } }) }); diff --git a/lib/base/test/base-single-node.test.ts b/lib/base/test/base-single-node.test.ts index a8e1562c..e7f6dbac 100644 --- a/lib/base/test/base-single-node.test.ts +++ b/lib/base/test/base-single-node.test.ts @@ -15,15 +15,13 @@ describe("BaseSingleNodeStack", () => { // Create the BaseSingleNodeStack. baseSingleNodeStack = new BaseSingleNodeStack(app, "base-single-node", { - stackName: `base-single-node-${config.baseConfig.accountId}`, + stackName: `base-single-node-${config.baseNodeConfig.baseNetworkId}`, env: {account: config.baseConfig.accountId, region: config.baseConfig.region}, instanceType: config.baseNodeConfig.instanceType, instanceCpuType: config.baseNodeConfig.instanceCpuType, - baseVersion: config.baseNodeConfig.baseVersion, - nodeConfiguration: config.baseNodeConfig.nodeConfiguration, + baseNetworkId: config.baseNodeConfig.baseNetworkId, dataVolume: config.baseNodeConfig.dataVolume, - baseNetworkId: config.baseNodeConfig.baseNetworkId }); template = Template.fromStack(baseSingleNodeStack); @@ -75,7 +73,7 @@ describe("BaseSingleNodeStack", () => { UserData: Match.anyValue(), BlockDeviceMappings: [ { - DeviceName: "/dev/sda1", + DeviceName: "/dev/xvda", Ebs: { DeleteOnTermination: true, Encrypted: true, @@ -117,7 +115,7 @@ describe("BaseSingleNodeStack", () => { // Has CloudWatch dashboard. template.hasResourceProperties("AWS::CloudWatch::Dashboard", { DashboardBody: Match.anyValue(), - DashboardName: `base-single-node-${config.baseConfig.accountId}` + DashboardName: `base-single-node-${config.baseNodeConfig.baseNetworkId}` }) }); }); From 38212d4602bc077a5390815caec44142fd745d5d Mon Sep 17 00:00:00 2001 From: Nikolay Vlasov Date: Thu, 25 Jan 2024 18:00:14 +1100 Subject: [PATCH 33/75] Base code changes after e2e testing --- lib/base/README.md | 2 +- lib/base/app.ts | 1 + lib/base/lib/assets/node-cw-dashboard.ts | 60 +++++++++++++++---- .../assets/sync-checker/syncchecker-base.sh | 37 ++++++++---- lib/base/lib/config/baseConfig.interface.ts | 1 + lib/base/lib/config/baseConfig.ts | 1 + lib/base/lib/single-node-stack.ts | 10 +++- lib/base/sample-configs/.env-sample-rpc | 3 +- 8 files changed, 87 insertions(+), 28 deletions(-) diff --git a/lib/base/README.md b/lib/base/README.md index a78b2c05..c8bb4b74 100644 --- a/lib/base/README.md +++ b/lib/base/README.md @@ -123,7 +123,7 @@ We will use AWS Cloud9 to execute the subsequent commands. Follow the instructio > cdk bootstrap aws://ACCOUNT-NUMBER/REGION > ``` -5. Deploy Amazon Managed Blockchain (AMB) Access Ethereum node and wait about 35-70 minutes for the node to sync +5. (Optional) For L1 node you you can set your own URL in `BASE_L1_ENDPOINT` property of `.env` file or use the command below to deploy Amazon Managed Blockchain (AMB) Access Ethereum node. It takes about 35-70 minutes for the AMB Access node to sync. ```bash pwd diff --git a/lib/base/app.ts b/lib/base/app.ts index a5c048e3..da467559 100644 --- a/lib/base/app.ts +++ b/lib/base/app.ts @@ -31,5 +31,6 @@ new BaseSingleNodeStack(app, "base-single-node", { instanceCpuType: config.baseNodeConfig.instanceCpuType, baseNetworkId: config.baseNodeConfig.baseNetworkId, restoreFromSnapshot: config.baseNodeConfig.restoreFromSnapshot, + l1Endpoint: config.baseNodeConfig.l1Endpoint, dataVolume: config.baseNodeConfig.dataVolume, }); diff --git a/lib/base/lib/assets/node-cw-dashboard.ts b/lib/base/lib/assets/node-cw-dashboard.ts index fd8b5462..2d2c926e 100644 --- a/lib/base/lib/assets/node-cw-dashboard.ts +++ b/lib/base/lib/assets/node-cw-dashboard.ts @@ -127,14 +127,14 @@ export const SyncNodeCWDashboardJSON = { } }, { - "height": 5, + "height": 4, "width": 6, - "y": 5, + "y": 0, "x": 12, "type": "metric", "properties": { "metrics": [ - [ "CWAgent", "elc_sync_block", "InstanceId", "${INSTANCE_ID}", { "label": "${INSTANCE_ID}-${INSTANCE_NAME}" } ] + [ "CWAgent", "l2_current_block", "InstanceId", "${INSTANCE_ID}", { "label": "${INSTANCE_ID}-${INSTANCE_NAME}" } ] ], "sparkline": true, "view": "timeSeries", @@ -142,13 +142,13 @@ export const SyncNodeCWDashboardJSON = { "region": "${REGION}", "stat": "Maximum", "period": 60, - "title": "Base Client Block Height" + "title": "L2 Current Block" } }, { - "height": 5, + "height": 4, "width": 6, - "y": 10, + "y": 4, "x": 12, "type": "metric", "properties": { @@ -159,9 +159,47 @@ export const SyncNodeCWDashboardJSON = { "stat": "Maximum", "period": 60, "metrics": [ - [ "CWAgent", "elc_blocks_behind", "InstanceId", "${INSTANCE_ID}", { "label": "${INSTANCE_ID}-${INSTANCE_NAME}" } ] + [ "CWAgent", "l2_blocks_behind", "InstanceId", "${INSTANCE_ID}", { "label": "${INSTANCE_ID}-${INSTANCE_NAME}" } ] + ], + "title": "L2 Blocks Behind" + } + }, + { + "height": 3, + "width": 6, + "y": 8, + "x": 12, + "type": "metric", + "properties": { + "metrics": [ + [ "CWAgent", "l1_current_block", "InstanceId", "${INSTANCE_ID}", { "label": "${INSTANCE_ID}-${INSTANCE_NAME}" } ] ], - "title": "Base Client Blocks Behind" + "sparkline": true, + "view": "timeSeries", + "stacked": false, + "region": "${REGION}", + "stat": "Maximum", + "period": 60, + "title": "L1 Current Block" + } + }, + { + "height": 4, + "width": 6, + "y": 11, + "x": 12, + "type": "metric", + "properties": { + "sparkline": true, + "view": "timeSeries", + "stacked": false, + "region": "${REGION}", + "stat": "Maximum", + "period": 60, + "metrics": [ + [ "CWAgent", "l1_blocks_behind", "InstanceId", "${INSTANCE_ID}", { "label": "${INSTANCE_ID}-${INSTANCE_NAME}" } ] + ], + "title": "L1 Blocks Behind" } }, { @@ -214,10 +252,10 @@ export const SyncNodeCWDashboardJSON = { } }, { - "height": 5, + "height": 3, "width": 6, - "y": 0, - "x": 12, + "y": 15, + "x": 6, "type": "metric", "properties": { "metrics": [ diff --git a/lib/base/lib/assets/sync-checker/syncchecker-base.sh b/lib/base/lib/assets/sync-checker/syncchecker-base.sh index a084df3b..fabc6d09 100644 --- a/lib/base/lib/assets/sync-checker/syncchecker-base.sh +++ b/lib/base/lib/assets/sync-checker/syncchecker-base.sh @@ -1,18 +1,25 @@ #!/bin/bash +source /etc/environment -BASE_SYNC_STATS=$(curl -s -X POST -H "Content-Type: application/json" --data '{"jsonrpc":"2.0","method":"eth_syncing","params":[],"id":1}' http://localhost:8545 | jq -r ".result") +OPTIMISM_SYNC_STATUS=$(curl -s -X POST -H "Content-Type: application/json" --data '{"id":0,"jsonrpc":"2.0","method":"optimism_syncStatus"}' http://localhost:7545 | jq -r ".result") -if [[ "$BASE_SYNC_STATS" == "false" ]]; then - BASE_SYNC_BLOCK_HEX=$(curl -s -X POST -H "Content-Type: application/json" --data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' http://localhost:8545 | jq -r ".result") - BASE_HIGHEST_BLOCK_HEX=$BASE_SYNC_BLOCK_HEX -else - BASE_SYNC_BLOCK_HEX=$(echo $BASE_SYNC_STATS | jq -r ".currentBlock") - BASE_HIGHEST_BLOCK_HEX=$(echo $BASE_SYNC_STATS | jq -r ".highestBlock") -fi +# L1 client stats +L1_CLIENT_HEAD=$(echo $OPTIMISM_SYNC_STATUS | jq -r ".head_l1.number") +L1_CLIENT_CURRENT=$(echo $OPTIMISM_SYNC_STATUS | jq -r ".current_l1.number") +L1_CLIENT_BLOCKS_BEHIND="$((L1_CLIENT_HEAD-L1_CLIENT_CURRENT))" -BASE_HIGHEST_BLOCK=$(echo $((${BASE_HIGHEST_BLOCK_HEX}))) -BASE_SYNC_BLOCK=$(echo $((${BASE_SYNC_BLOCK_HEX}))) -BASE_BLOCKS_BEHIND="$((BASE_HIGHEST_BLOCK-BASE_SYNC_BLOCK))" +# L2 client stats +L2_CLIENT_HEAD=$(echo $OPTIMISM_SYNC_STATUS | jq -r ".queued_unsafe_l2.number") +L2_CLIENT_CURRENT=$(echo $OPTIMISM_SYNC_STATUS | jq -r ".unsafe_l2.number") +L2_CLIENT_BLOCKS_BEHIND="$((L2_CLIENT_HEAD-L2_CLIENT_CURRENT))" + +echo "L1_CLIENT_HEAD="$L1_CLIENT_HEAD +echo "L1_CLIENT_CURRENT="$L1_CLIENT_CURRENT +echo "L1_CLIENT_BLOCKS_BEHIND="$L1_CLIENT_BLOCKS_BEHIND + +echo "L2_CLIENT_HEAD="$L2_CLIENT_HEAD +echo "L2_CLIENT_CURRENT="$L2_CLIENT_CURRENT +echo "L2_CLIENT_BLOCKS_BEHIND="$L2_CLIENT_BLOCKS_BEHIND # Sending data to CloudWatch TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600") @@ -20,5 +27,9 @@ INSTANCE_ID=$(curl -H "X-aws-ec2-metadata-token: $TOKEN" -s http://169.254.169.2 REGION=$(curl -H "X-aws-ec2-metadata-token: $TOKEN" -s http://169.254.169.254/latest/dynamic/instance-identity/document | jq .region -r) TIMESTAMP=$(date +"%Y-%m-%dT%H:%M:%S%:z") -aws cloudwatch put-metric-data --metric-name elc_sync_block --namespace CWAgent --value $BASE_SYNC_BLOCK --timestamp $TIMESTAMP --dimensions InstanceId=$INSTANCE_ID --region $REGION -aws cloudwatch put-metric-data --metric-name elc_blocks_behind --namespace CWAgent --value $BASE_BLOCKS_BEHIND --timestamp $TIMESTAMP --dimensions InstanceId=$INSTANCE_ID --region $REGION +aws cloudwatch put-metric-data --metric-name l1_current_block --namespace CWAgent --value $L1_CLIENT_CURRENT --timestamp $TIMESTAMP --dimensions InstanceId=$INSTANCE_ID --region $REGION +aws cloudwatch put-metric-data --metric-name l1_blocks_behind --namespace CWAgent --value $L1_CLIENT_BLOCKS_BEHIND --timestamp $TIMESTAMP --dimensions InstanceId=$INSTANCE_ID --region $REGION + +aws cloudwatch put-metric-data --metric-name l2_current_block --namespace CWAgent --value $L2_CLIENT_CURRENT --timestamp $TIMESTAMP --dimensions InstanceId=$INSTANCE_ID --region $REGION +aws cloudwatch put-metric-data --metric-name l2_blocks_behind --namespace CWAgent --value $L2_CLIENT_BLOCKS_BEHIND --timestamp $TIMESTAMP --dimensions InstanceId=$INSTANCE_ID --region $REGION + diff --git a/lib/base/lib/config/baseConfig.interface.ts b/lib/base/lib/config/baseConfig.interface.ts index 6dbf1e33..89290d0f 100644 --- a/lib/base/lib/config/baseConfig.interface.ts +++ b/lib/base/lib/config/baseConfig.interface.ts @@ -20,4 +20,5 @@ export interface BaseBaseNodeConfig extends configTypes.BaseNodeConfig { baseNetworkId: BaseNetworkId; dataVolume: BaseDataVolumeConfig; restoreFromSnapshot: boolean; + l1Endpoint: string; } diff --git a/lib/base/lib/config/baseConfig.ts b/lib/base/lib/config/baseConfig.ts index 44ed175c..73ba97c7 100644 --- a/lib/base/lib/config/baseConfig.ts +++ b/lib/base/lib/config/baseConfig.ts @@ -30,6 +30,7 @@ export const baseNodeConfig: configTypes.BaseBaseNodeConfig = { instanceCpuType: process.env.BASE_CPU_TYPE?.toLowerCase() == "x86_64" ? ec2.AmazonLinuxCpuType.X86_64 : ec2.AmazonLinuxCpuType.ARM_64, baseNetworkId: process.env.BASE_NETWORK_ID || "mainnet", restoreFromSnapshot: process.env.BASE_RESTORE_FROM_SNAPSHOT?.toLowerCase() == "true" ? true : false, + l1Endpoint: process.env.BASE_L1_ENDPOINT || constants.NoneValue, dataVolume: { sizeGiB: process.env.BASE_DATA_VOL_SIZE ? parseInt(process.env.BASE_DATA_VOL_SIZE): 1000, type: parseDataVolumeType(process.env.BASE_DATA_VOL_TYPE?.toLowerCase() ? process.env.BASE_DATA_VOL_TYPE?.toLowerCase() : "gp3"), diff --git a/lib/base/lib/single-node-stack.ts b/lib/base/lib/single-node-stack.ts index 814edac3..5c6c6827 100644 --- a/lib/base/lib/single-node-stack.ts +++ b/lib/base/lib/single-node-stack.ts @@ -18,6 +18,7 @@ export interface BaseSingleNodeStackProps extends cdk.StackProps { instanceCpuType: ec2.AmazonLinuxCpuType; baseNetworkId: configTypes.BaseNetworkId; restoreFromSnapshot: boolean; + l1Endpoint: string; dataVolume: configTypes.BaseDataVolumeConfig; } @@ -38,6 +39,7 @@ export class BaseSingleNodeStack extends cdk.Stack { instanceCpuType, baseNetworkId, restoreFromSnapshot, + l1Endpoint, dataVolume, } = props; @@ -56,8 +58,12 @@ export class BaseSingleNodeStack extends cdk.Stack { // Getting the IAM role ARN from the common stack const importedInstanceRoleArn = cdk.Fn.importValue("BaseNodeInstanceRoleArn"); - const ambEthereumNodeRpcUrlWithBillingToken = cdk.Fn.importValue("BaseAmbEthereumNodeRpcUrlWithBillingToken"); + // If user has not supplied the URL for L1, attempting to use AMB node URL + let l1EndpointURL = l1Endpoint; + if (l1EndpointURL === constants.NoneValue){ + l1EndpointURL = cdk.Fn.importValue("BaseAmbEthereumNodeRpcUrlWithBillingToken"); + } const instanceRole = iam.Role.fromRoleArn(this, "iam-role", importedInstanceRoleArn); // Making sure our instance will be able to read the assets @@ -102,7 +108,7 @@ export class BaseSingleNodeStack extends cdk.Stack { _AUTOSCALING_GROUP_NAME_: constants.NoneValue, _RESTORE_FROM_SNAPSHOT_: restoreFromSnapshot.toString(), _FORMAT_DISK_: "true", - _L1_ENDPOINT_: ambEthereumNodeRpcUrlWithBillingToken, + _L1_ENDPOINT_: l1EndpointURL, }); node.instance.addUserData(modifiedInitNodeScript); diff --git a/lib/base/sample-configs/.env-sample-rpc b/lib/base/sample-configs/.env-sample-rpc index daca3b70..538606da 100644 --- a/lib/base/sample-configs/.env-sample-rpc +++ b/lib/base/sample-configs/.env-sample-rpc @@ -18,4 +18,5 @@ BASE_DATA_VOL_TYPE="gp3" # Other options: "io1" | "io2" | "gp3" | BASE_DATA_VOL_SIZE="3000" # Current required data size to keep both snapshot archive and unarchived version of it BASE_DATA_VOL_IOPS="5000" # Max IOPS for EBS volumes (not applicable for "instance-store") BASE_DATA_VOL_THROUGHPUT="700" # Max throughput for EBS gp3 volumes (not applicable for "io1" | "io2" | "instance-store") -BASE_RESTORE_FROM_SNAPSHOT="false" # Download snapshot to speed up statup time \ No newline at end of file +BASE_RESTORE_FROM_SNAPSHOT="false" # Download snapshot to speed up statup time +BASE_L1_ENDPOINT="none" # Leave as "none" if you use Amazon Managed Blockchain (AMB) Access Ethereum node or set your own L1 URL \ No newline at end of file From 178c55b17ce1ce36815aba99062f062c8a127bf3 Mon Sep 17 00:00:00 2001 From: Nikolay Vlasov Date: Mon, 29 Jan 2024 17:04:26 +1100 Subject: [PATCH 34/75] WIP Base node bug fixes --- lib/base/README.md | 47 +------------------ .../assets/sync-checker/syncchecker-base.sh | 7 ++- lib/base/lib/single-node-stack.ts | 5 -- lib/base/sample-configs/.env-sample-rpc | 4 +- 4 files changed, 9 insertions(+), 54 deletions(-) diff --git a/lib/base/README.md b/lib/base/README.md index c8bb4b74..7fe3e959 100644 --- a/lib/base/README.md +++ b/lib/base/README.md @@ -16,36 +16,6 @@ ## Additional materials -

- -Review the for pros and cons of this solution. - -### Well-Architected Checklist - -This is the Well-Architected checklist for AWS Blockchain Node Runner app. This checklist takes into account questions from the [AWS Well-Architected Framework](https://aws.amazon.com/architecture/well-architected/) which are relevant to this workload. Please feel free to add more checks from the framework if required for your workload. - -| Pillar | Control | Question/Check | Remarks | -|:------------------------|:----------------------------------|:---------------------------------------------------------------------------------|:-----------------| -| Security | Network protection | Are there unnecessary open ports in security groups? | Please note that ports 9222 (TCP/UDP) for Base are open to public to support P2P protocols. We have to rely on the protection mechanisms built into the Base software to protect those ports. | -| | | Traffic inspection | AWS WAF could be implemented for traffic inspection. Additional charges will apply. | -| | Compute protection | Reduce attack surface | This solution uses Amazon Linux AMI. You may choose to run hardening scripts on it. | -| | | Enable people to perform actions at a distance | This solution uses AWS Systems Manager for terminal session, not ssh ports. | -| | Data protection at rest | Use encrypted Amazon Elastic Block Store (Amazon EBS) volumes | This solution uses encrypted Amazon EBS volumes. | -| | Data protection in transit | Use TLS | By design TLS is not used in Base RPC and P2P protocols because the data is considered public. To protect RPC traffic we expose the port only for internal use. | -| | Authorization and access control | Use instance profile with Amazon Elastic Compute Cloud (Amazon EC2) instances | This solution uses AWS Identity and Access Management (AWS IAM) role instead of IAM user. | -| | | Following principle of least privilege access | In the node, root user is not used (using special user "ubuntu" instead). | -| | Application security | Security focused development practices | cdk-nag is being used with documented suppressions. | -| Cost optimization | Service selection | Use cost effective resources | Base nodes currently doesn't provide binaries for ARM architecture, so AMD-powered EC2 instance type for better cost effectiveness. | -| | Cost awareness | Estimate costs | One Base node on m6a.2xlarge and 1T EBS gp3 volume will cost around US$367.21 per month in the US East (N. Virginia) region. Additionally the AMB Access Ethereum on bc.m5.xlarge will cost additional ~US$202 per month in the US East (N. Virginia) region. Approximately the total cost will be US$367.21 + US$202 = US$569.21 per month. | -| Reliability | Resiliency implementation | Withstand component failures | This solution currently does not have high availability and is deployed to a single availability zone. | -| | Data backup | How is data backed up? | The data is not specially backed up. The node will have to re-sync its state from other nodes in the Base network to recover. | -| | Resource monitoring | How are workload resources monitored? | Resources are being monitored using Amazon CloudWatch dashboards. Amazon CloudWatch custom metrics are being pushed via CloudWatch Agent. | -| Performance efficiency | Compute selection | How is compute solution selected? | Compute solution is selected based on the recommendations the from Base community to provide stable and cost-effective operations. | -| | Storage selection | How is storage solution selected? | Storage solution is selected based on the recommendations the from Base community to provide stable and cost-effective operations. | -| | Architecture selection | How is the best performance architecture selected? | In this solution we try to balance price and performance to achieve better cost efficiency, but not necessarily the best performance. | -| Operational excellence | Workload health | How is health of workload determined? | We rely on the standard EC2 instance monitoring tool to detect stalled instances. | -| Sustainability | Hardware & services | Select most efficient hardware for your workload | Base nodes currently doesn't provide binaries for ARM architecture, so AMD-powered EC2 instance type for better cost effectiveness. | -
Recommended Infrastructure @@ -61,11 +31,6 @@ This is the Well-Architected checklist for AWS Blockchain Node Runner app. This - Instance type [m6a.2xlarge](https://aws.amazon.com/ec2/instance-types/m6a/). - 2500GB EBS gp3 storage with at least 6000 IOPS.` -**Amazon Managed Blockchain Ethereum L1** - -- Minimum instance type: [bc.m5.xlarge](https://aws.amazon.com/managed-blockchain/instance-types/) -- Recommended instance type: [bc.m5.2xlarge](https://aws.amazon.com/managed-blockchain/instance-types/) -
## Setup Instructions @@ -123,14 +88,7 @@ We will use AWS Cloud9 to execute the subsequent commands. Follow the instructio > cdk bootstrap aws://ACCOUNT-NUMBER/REGION > ``` -5. (Optional) For L1 node you you can set your own URL in `BASE_L1_ENDPOINT` property of `.env` file or use the command below to deploy Amazon Managed Blockchain (AMB) Access Ethereum node. It takes about 35-70 minutes for the AMB Access node to sync. - - ```bash - pwd - # Make sure you are in aws-blockchain-node-runners/lib/base - npx cdk deploy base-ethereum-l1-node --json --outputs-file base-ethereum-l1-node.json - ``` - To watch the progress, open the [AMB Web UI](https://console.aws.amazon.com/managedblockchain/home), click the name of your target network from the list (Mainnet, Goerly, etc.) and watch the status of the node to change from `Creating` to `Available`. +5. For L1 node you you can set your own URL in `BASE_L1_ENDPOINT` property of `.env` file. It can be one of [the providers recommended by Base](https://docs.base.org/tools/node-providers) or run your own Ethereum node [with Node Runner blueprint](https://aws-samples.github.io/aws-blockchain-node-runners/docs/Blueprints/Ethereum). 6. Deploy Base RPC Node and wait for another 10-20 minutes for it to sync @@ -184,9 +142,6 @@ A script on the Base node publishes current block and blocks behind metrics to C # Undeploy Single Node npx cdk destroy base-single-node - # Undeploy AMB Etheruem node - npx cdk destroy base-ethereum-l1-node - # Delete all common components like IAM role and Security Group npx cdk destroy base-common ``` diff --git a/lib/base/lib/assets/sync-checker/syncchecker-base.sh b/lib/base/lib/assets/sync-checker/syncchecker-base.sh index fabc6d09..b912a637 100644 --- a/lib/base/lib/assets/sync-checker/syncchecker-base.sh +++ b/lib/base/lib/assets/sync-checker/syncchecker-base.sh @@ -6,7 +6,12 @@ OPTIMISM_SYNC_STATUS=$(curl -s -X POST -H "Content-Type: application/json" --dat # L1 client stats L1_CLIENT_HEAD=$(echo $OPTIMISM_SYNC_STATUS | jq -r ".head_l1.number") L1_CLIENT_CURRENT=$(echo $OPTIMISM_SYNC_STATUS | jq -r ".current_l1.number") -L1_CLIENT_BLOCKS_BEHIND="$((L1_CLIENT_HEAD-L1_CLIENT_CURRENT))" + +if [ $L1_CLIENT_HEAD -eq 0 ]; then + L1_CLIENT_BLOCKS_BEHIND=0 +else + L1_CLIENT_BLOCKS_BEHIND="$((L1_CLIENT_HEAD-L1_CLIENT_CURRENT))" +fi # L2 client stats L2_CLIENT_HEAD=$(echo $OPTIMISM_SYNC_STATUS | jq -r ".queued_unsafe_l2.number") diff --git a/lib/base/lib/single-node-stack.ts b/lib/base/lib/single-node-stack.ts index 5c6c6827..0292d9f0 100644 --- a/lib/base/lib/single-node-stack.ts +++ b/lib/base/lib/single-node-stack.ts @@ -69,11 +69,6 @@ export class BaseSingleNodeStack extends cdk.Stack { // Making sure our instance will be able to read the assets asset.bucket.grantRead(instanceRole); - // Setting up the node using generic Single Node constract - if (instanceCpuType === ec2.AmazonLinuxCpuType.ARM_64) { - throw new Error("ARM_64 is not yet supported"); - } - const node = new SingleNodeConstruct(this, "rpc-node", { instanceName: STACK_NAME, instanceType, diff --git a/lib/base/sample-configs/.env-sample-rpc b/lib/base/sample-configs/.env-sample-rpc index 538606da..2edc9750 100644 --- a/lib/base/sample-configs/.env-sample-rpc +++ b/lib/base/sample-configs/.env-sample-rpc @@ -11,8 +11,8 @@ AMB_ETHEREUM_NODE_NETWORK_ID="mainnet" # All options: "mainnet", "goerl AMB_ETHEREUM_NODE_INSTANCE_TYPE="bc.m5.xlarge" # For available options see: https://aws.amazon.com/managed-blockchain/instance-types/ BASE_NETWORK_ID="mainnet" # All options: "mainnet", "goerli" -BASE_INSTANCE_TYPE="m6a.2xlarge" -BASE_CPU_TYPE="x86_64" # All options: "x86_64", "ARM_64". IMPORTANT: Make sure the CPU type matches the instance type used +BASE_INSTANCE_TYPE="m7g.2xlarge" +BASE_CPU_TYPE="ARM_64" # All options: "x86_64", "ARM_64". IMPORTANT: Make sure the CPU type matches the instance type used # Data volume configuration BASE_DATA_VOL_TYPE="gp3" # Other options: "io1" | "io2" | "gp3" | "instance-store" . IMPORTANT: Use "instance-store" option only with instance types that support that feature, like popular for node im4gn, d3, i3en, and i4i instance families BASE_DATA_VOL_SIZE="3000" # Current required data size to keep both snapshot archive and unarchived version of it From eef7e238fb181849c9da363fefa71ca1537dd513 Mon Sep 17 00:00:00 2001 From: Nikolay Vlasov Date: Fri, 2 Feb 2024 17:35:34 +1100 Subject: [PATCH 35/75] Base updates to README --- lib/base/README.md | 209 ++++++++++-------- .../doc/assets/Architecture-SingleNode-v3.png | Bin 0 -> 67677 bytes .../doc/assets/Architecture-SingleNode.drawio | 6 +- lib/base/lib/single-node-stack.ts | 2 +- lib/base/sample-configs/.env-sample-rpc | 2 +- 5 files changed, 116 insertions(+), 103 deletions(-) create mode 100644 lib/base/doc/assets/Architecture-SingleNode-v3.png diff --git a/lib/base/README.md b/lib/base/README.md index 7fe3e959..48c4b453 100644 --- a/lib/base/README.md +++ b/lib/base/README.md @@ -10,10 +10,9 @@ 1. A Base node deployed in the [Default VPC](https://docs.aws.amazon.com/vpc/latest/userguide/default-vpc.html) continuously synchronizes with the rest of nodes on Base blockchain network through [Internet Gateway](https://docs.aws.amazon.com/vpc/latest/userguide/VPC_Internet_Gateway.html). 2. The Base node is used by dApps or development tools internally from within the Default VPC. JSON RPC API is not exposed to the Internet directly to protect nodes from unauthorized access. -3. You will need access to a fully-synced Ethereum Mainnet RPC endpoint before running. +3. Your Base node needs access to a fully-synced [Ethereum Mainnet or Sepolia RPC endpoint](https://docs.base.org/tools/node-providers) . 4. The Base node sends various monitoring metrics for both EC2 and Base nodes to Amazon CloudWatch. - ## Additional materials
@@ -23,12 +22,12 @@ **Minimum for Base node** -- Instance type [m6a.xlarge](https://aws.amazon.com/ec2/instance-types/m6a/). +- Instance type [m7g.2xlarge](https://aws.amazon.com/ec2/instance-types/m6a/). - 2500GB EBS gp3 storage with at least 6000 IOPS. **Recommended for Base node** -- Instance type [m6a.2xlarge](https://aws.amazon.com/ec2/instance-types/m6a/). +- Instance type [m7g.2xlarge](https://aws.amazon.com/ec2/instance-types/m6a/). - 2500GB EBS gp3 storage with at least 6000 IOPS.`
@@ -39,7 +38,11 @@ We will use AWS Cloud9 to execute the subsequent commands. Follow the instructions in [Cloud9 Setup](../../docs/setup-cloud9.md) -### Clone this repository and install dependencies +### Make sure you have access to Ethereum L1 node + +Base node needs a URL to a Full Ethereum Node to validate blocks it receives. You can run your own with [Ethereum node blueprint](https://aws-samples.github.io/aws-blockchain-node-runners/docs/Blueprints/Ethereum) or use [one of Base partners](https://docs.base.org/tools/node-providers). + +### On your Cloud9: Clone this repository and install dependencies ```bash git clone https://github.com/alickwong/aws-blockchain-node-runners @@ -47,15 +50,15 @@ We will use AWS Cloud9 to execute the subsequent commands. Follow the instructio npm install ``` -### Deploy Single Node +### From your Cloud9: Deploy required dependencies 1. Make sure you are in the root directory of the cloned repository 2. If you have deleted or don't have the default VPC, create default VPC - ```bash - aws ec2 create-default-vpc - ``` + ```bash + aws ec2 create-default-vpc + ``` > NOTE: > You may see the following error if the default VPC already exists: `An error occurred (DefaultVpcAlreadyExists) when calling the CreateDefaultVpc operation: A Default VPC already exists for this account in this region.`. That means you can just continue with the following steps. @@ -63,24 +66,24 @@ We will use AWS Cloud9 to execute the subsequent commands. Follow the instructio 3. Configure your setup Create your own copy of `.env` file and edit it to update with your AWS Account ID and Region: - ```bash - # Make sure you are in aws-blockchain-node-runners/lib/base - cd lib/base - npm install - pwd - cp ./sample-configs/.env-sample-rpc .env - nano .env - ``` +```bash +# Make sure you are in aws-blockchain-node-runners/lib/base +cd lib/base +npm install +pwd +cp ./sample-configs/.env-sample-rpc .env +nano .env +``` > NOTE: > Example configuration parameters are set in the local `.env-sample` file. You can find more examples inside `sample-configs` directory. 4. Deploy common components such as IAM role - ```bash - pwd - # Make sure you are in aws-blockchain-node-runners/lib/base - npx cdk deploy base-common - ``` +```bash +pwd +# Make sure you are in aws-blockchain-node-runners/lib/base +npx cdk deploy base-common +``` > IMPORTANT: > All AWS CDK v2 deployments use dedicated AWS resources to hold data during deployment. Therefore, your AWS account and Region must be [bootstrapped](https://docs.aws.amazon.com/cdk/v2/guide/bootstrapping.html) to create these resources before you can deploy. If you haven't already bootstrapped, issue the following command: @@ -88,63 +91,73 @@ We will use AWS Cloud9 to execute the subsequent commands. Follow the instructio > cdk bootstrap aws://ACCOUNT-NUMBER/REGION > ``` -5. For L1 node you you can set your own URL in `BASE_L1_ENDPOINT` property of `.env` file. It can be one of [the providers recommended by Base](https://docs.base.org/tools/node-providers) or run your own Ethereum node [with Node Runner blueprint](https://aws-samples.github.io/aws-blockchain-node-runners/docs/Blueprints/Ethereum). - -6. Deploy Base RPC Node and wait for another 10-20 minutes for it to sync - - ```bash - pwd - # Make sure you are in aws-blockchain-node-runners/lib/base - npx cdk deploy base-single-node --json --outputs-file single-node-deploy.json - ``` - After starting the node you will need to wait for the initial synchronization process to finish.To see the progress, you may use SSM to connect into EC2 first and watch the log like this: - - ```bash - export INSTANCE_ID=$(cat single-node-deploy.json | jq -r '..|.node-instance-id? | select(. != null)') - echo "INSTANCE_ID=" $INSTANCE_ID - export AWS_REGION=us-east-1 - aws ssm start-session --target $INSTANCE_ID --region $AWS_REGION - echo Latest synced block behind by: $((($(date +%s)-$( \ - curl -d '{"id":0,"jsonrpc":"2.0","method":"optimism_syncStatus"}' \ - -H "Content-Type: application/json" http://localhost:7545 | \ - jq -r .result.unsafe_l2.timestamp))/60)) minutes - ``` - -7. Test Base RPC API +### From your Cloud9: Deploy Single Node + +1. For L1 node you you can set your own URL in `BASE_L1_ENDPOINT` property of `.env` file. It can be one of [the providers recommended by Base](https://docs.base.org/tools/node-providers) or run your own Ethereum node [with Node Runner blueprint](https://aws-samples.github.io/aws-blockchain-node-runners/docs/Blueprints/Ethereum). For example: + +```bash +#For Mainnet: +BASE_L1_ENDPOINT=https://1rpc.io/eth + +#For Sepolia: +BASE_L1_ENDPOINT=https://rpc.sepolia.org +``` + +2. Deploy Base RPC Node and wait for it to sync. For Mainnet it might take less than an hour when using snapshots (default) or multiple days if syncing from block 0. + +```bash +pwd +# Make sure you are in aws-blockchain-node-runners/lib/base +npx cdk deploy base-single-node --json --outputs-file single-node-deploy.json +``` +After starting the node you will need to wait for the initial synchronization process to finish.To see the progress, you may use SSM to connect into EC2 first and watch the log like this: + +```bash +export INSTANCE_ID=$(cat single-node-deploy.json | jq -r '..|.node-instance-id? | select(. != null)') +echo "INSTANCE_ID=" $INSTANCE_ID +export AWS_REGION=us-east-1 +aws ssm start-session --target $INSTANCE_ID --region $AWS_REGION +echo Latest synced block behind by: $((($(date +%s)-$( \ +curl -d '{"id":0,"jsonrpc":"2.0","method":"optimism_syncStatus"}' \ +-H "Content-Type: application/json" http://localhost:7545 | \ +jq -r .result.unsafe_l2.timestamp))/60)) minutes +``` + +3. Test Base RPC API [TODO: Is there an address we can query balance from?] Use curl to query from within the node instance: - ```bash - export INSTANCE_ID=$(cat single-node-deploy.json | jq -r '..|.node-instance-id? | select(. != null)') - echo "INSTANCE_ID=" $INSTANCE_ID - export AWS_REGION=us-east-1 - aws ssm start-session --target $INSTANCE_ID --region $AWS_REGION +```bash +export INSTANCE_ID=$(cat single-node-deploy.json | jq -r '..|.node-instance-id? | select(. != null)') +echo "INSTANCE_ID=" $INSTANCE_ID +export AWS_REGION=us-east-1 +aws ssm start-session --target $INSTANCE_ID --region $AWS_REGION - curl -s -X POST -H "Content-Type: application/json" --data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' http://localhost:8545 - ``` +curl -s -X POST -H "Content-Type: application/json" --data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' http://localhost:8545 +``` ### Monitoring A script on the Base node publishes current block and blocks behind metrics to CloudWatch metrics every 5 minutes. When the node is fully synced the blocks behind metric should get to 0, which might take about 1.5 days. To see the metrics: - Navigate to CloudWatch service (make sure you are in the region you have specified for AWS_REGION) -- Open Dashboards and select `base-single-node` from the list of dashboards. +- Open Dashboards and select `base-single-node-` from the list of dashboards. -## Clear up and undeploy everything +## From your Cloud9: Clear up and undeploy everything 1. Undeploy all Nodes and Common stacks - ```bash - # Setting the AWS account id and region in case local .env file is lost - export AWS_ACCOUNT_ID= - export AWS_REGION= +```bash +# Setting the AWS account id and region in case local .env file is lost +export AWS_ACCOUNT_ID= +export AWS_REGION= - pwd - # Make sure you are in aws-blockchain-node-runners/lib/base +pwd +# Make sure you are in aws-blockchain-node-runners/lib/base - # Undeploy Single Node - npx cdk destroy base-single-node +# Undeploy Single Node +npx cdk destroy base-single-node - # Delete all common components like IAM role and Security Group - npx cdk destroy base-common - ``` +# Delete all common components like IAM role and Security Group +npx cdk destroy base-common +``` 2. Follow steps to delete the Cloud9 instance in [Cloud9 Setup](../../doc/setup-cloud9.md) @@ -154,44 +167,44 @@ A script on the Base node publishes current block and blocks behind metrics to C **Note:** In this tutorial we chose not to use SSH and use Session Manager instead. That allows you to log all sessions in AWS CloudTrail to see who logged into the server and when. If you receive an error similar to `SessionManagerPlugin is not found`, [install Session Manager plugin for AWS CLI](https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-working-with-install-plugin.html) - ```bash - pwd - # Make sure you are in aws-blockchain-node-runners/lib/base - - export INSTANCE_ID=$(cat single-node-deploy.json | jq -r '..|.node-instance-id? | select(. != null)') - echo "INSTANCE_ID=" $INSTANCE_ID - export AWS_REGION=us-east-1 - aws ssm start-session --target $INSTANCE_ID --region $AWS_REGION - sudo su bcuser - # Geth logs: - docker logs --tail 50 node_geth_1 -f - # Base logs: - docker logs --tail 50 node_node_1 -f - ``` +```bash +pwd +# Make sure you are in aws-blockchain-node-runners/lib/base + +export INSTANCE_ID=$(cat single-node-deploy.json | jq -r '..|.node-instance-id? | select(. != null)') +echo "INSTANCE_ID=" $INSTANCE_ID +export AWS_REGION=us-east-1 +aws ssm start-session --target $INSTANCE_ID --region $AWS_REGION +sudo su bcuser +# Geth logs: +docker logs --tail 50 node_geth_1 -f +# Base logs: +docker logs --tail 50 node_node_1 -f +``` 2. How to check the logs from the EC2 user-data script? - ```bash - pwd - # Make sure you are in aws-blockchain-node-runners/lib/base - - export INSTANCE_ID=$(cat single-node-deploy.json | jq -r '..|.node-instance-id? | select(. != null)') - echo "INSTANCE_ID=" $INSTANCE_ID - export AWS_REGION=us-east-1 - aws ssm start-session --target $INSTANCE_ID --region $AWS_REGION - sudo cat /var/log/cloud-init-output.log - ``` +```bash +pwd +# Make sure you are in aws-blockchain-node-runners/lib/base + +export INSTANCE_ID=$(cat single-node-deploy.json | jq -r '..|.node-instance-id? | select(. != null)') +echo "INSTANCE_ID=" $INSTANCE_ID +export AWS_REGION=us-east-1 +aws ssm start-session --target $INSTANCE_ID --region $AWS_REGION +sudo cat /var/log/cloud-init-output.log +``` 3. How can I restart the Base node? - ``` bash - export INSTANCE_ID=$(cat single-node-deploy.json | jq -r '..|.node-instance-id? | select(. != null)') - echo "INSTANCE_ID=" $INSTANCE_ID - export AWS_REGION=us-east-1 - aws ssm start-session --target $INSTANCE_ID --region $AWS_REGION - sudo su bcuser - /usr/local/bin/docker-compose -f /home/bcuser/node/docker-compose.yml down && \ - /usr/local/bin/docker-compose -f /home/bcuser/node/docker-compose.yml up -d - ``` +``` bash +export INSTANCE_ID=$(cat single-node-deploy.json | jq -r '..|.node-instance-id? | select(. != null)') +echo "INSTANCE_ID=" $INSTANCE_ID +export AWS_REGION=us-east-1 +aws ssm start-session --target $INSTANCE_ID --region $AWS_REGION +sudo su bcuser +/usr/local/bin/docker-compose -f /home/bcuser/node/docker-compose.yml down && \ +/usr/local/bin/docker-compose -f /home/bcuser/node/docker-compose.yml up -d +``` 4. Where to find the key Base client directories? - The data directory is `/data` diff --git a/lib/base/doc/assets/Architecture-SingleNode-v3.png b/lib/base/doc/assets/Architecture-SingleNode-v3.png new file mode 100644 index 0000000000000000000000000000000000000000..35191bf7a10dd44ab14b42b439c4bf9ebeeb1f1e GIT binary patch literal 67677 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

t<(c_=yk;Cj)sg-%aTeR>0Qe`#?dQ>sp5#p6XO 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 SRqozg5!gIov6im>dFbC-Q80-B From 699b4829f04bd4431951d1859b71b6225effdd90 Mon Sep 17 00:00:00 2001 From: Nikolay Vlasov Date: Wed, 7 Feb 2024 16:28:55 +1100 Subject: [PATCH 37/75] Base fixed sync checker L2 blocks behind metric bug --- lib/base/README.md | 35 +++++++++++++++++-- .../assets/sync-checker/syncchecker-base.sh | 7 +++- lib/base/sample-configs/.env-sample-rpc | 16 +++++---- 3 files changed, 48 insertions(+), 10 deletions(-) diff --git a/lib/base/README.md b/lib/base/README.md index 9041e23b..07a9fad2 100644 --- a/lib/base/README.md +++ b/lib/base/README.md @@ -15,6 +15,35 @@ ## Additional materials +

Review the for pros and cons of this solution. + +### Well-Architected Checklist + +This is the Well-Architected checklist for Ethereum nodes implementation of the AWS Blockchain Node Runner app. This checklist takes into account questions from the [AWS Well-Architected Framework](https://aws.amazon.com/architecture/well-architected/) which are relevant to this workload. Please feel free to add more checks from the framework if required for your workload. + +| Pillar | Control | Question/Check | Remarks | +|:------------------------|:----------------------------------|:---------------------------------------------------------------------------------|:-----------------| +| Security | Network protection | Are there unnecessary open ports in security groups? | Please note that port 9222 (TCP/UDP) for Base are open to public to support P2P protocols. We have to rely on the protection mechanisms built into the Base software to protect those ports. | +| | | Traffic inspection | AWS WAF could be implemented for traffic inspection. Additional charges will apply. | +| | Compute protection | Reduce attack surface | This solution uses Amazon Linux 2 AMI. You may choose to run hardening scripts on it. | +| | | Enable people to perform actions at a distance | This solution uses AWS Systems Manager for terminal session, not ssh ports. | +| | Data protection at rest | Use encrypted Amazon Elastic Block Store (Amazon EBS) volumes | This solution uses encrypted Amazon EBS volumes. | +| | Data protection in transit | Use TLS | By design TLS is not used in Base RPC and P2P protocols because the data is considered public. To protect RPC traffic we expose the port only for internal use. | +| | Authorization and access control | Use instance profile with Amazon Elastic Compute Cloud (Amazon EC2) instances | This solution uses AWS Identity and Access Management (AWS IAM) role instead of IAM user. | +| | | Following principle of least privilege access | In the node, root user is not used (using special user "bcuser" instead). | +| | Application security | Security focused development practices | cdk-nag is being used with documented suppressions. | +| Cost optimization | Service selection | Use cost effective resources | Base nodes works well on ARM architecture and we use Graviton3-powered EC2 instances for better cost effectiveness. | +| | Cost awareness | Estimate costs | One Base node on m7g.2xlarge and 3TiB EBS gp3 volume will cost around US$503.27 per month in the US East (N. Virginia) region. Additional charges will be applied for Ethereum L1 node and might vary between US$200 and US$500 per month. Approximately the total cost will be US$503.27 + US$500 = US$1003.27 per month. | +| Reliability | Resiliency implementation | Withstand component failures | This solution currently does not have high availability and is deployed to a single availability zone. | +| | Data backup | How is data backed up? | The data is not specially backed up. The node will have to re-sync its state from other nodes in the Base network to recover. | +| | Resource monitoring | How are workload resources monitored? | Resources are being monitored using Amazon CloudWatch dashboards. Amazon CloudWatch custom metrics are being pushed via CloudWatch Agent. | +| Performance efficiency | Compute selection | How is compute solution selected? | Compute solution is selected based on the recommendations the from Base community to provide stable and cost-effective operations. | +| | Storage selection | How is storage solution selected? | Storage solution is selected based on the recommendations the from Base community to provide stable and cost-effective operations. | +| | Architecture selection | How is the best performance architecture selected? | In this solution we try to balance price and performance to achieve better cost efficiency, but not necessarily the best performance. | +| Operational excellence | Workload health | How is health of workload determined? | We rely on the standard EC2 instance monitoring tool to detect stalled instances. | +| Sustainability | Hardware & services | Select most efficient hardware for your workload | We use ARM-powered EC2 instance type for better cost/performance balance. | + +
Recommended Infrastructure @@ -22,12 +51,12 @@ **Minimum for Base node** -- Instance type [m7g.2xlarge](https://aws.amazon.com/ec2/instance-types/m6a/). -- 2500GB EBS gp3 storage with at least 6000 IOPS. +- Instance type [m7g.2xlarge](https://aws.amazon.com/ec2/instance-types/m7g/). +- 2500GB EBS gp3 storage with at least 5000 IOPS. **Recommended for Base node** -- Instance type [m7g.2xlarge](https://aws.amazon.com/ec2/instance-types/m6a/). +- Instance type [m7g.4xlarge](https://aws.amazon.com/ec2/instance-types/m7g/). - 2500GB EBS gp3 storage with at least 6000 IOPS.`
diff --git a/lib/base/lib/assets/sync-checker/syncchecker-base.sh b/lib/base/lib/assets/sync-checker/syncchecker-base.sh index b912a637..fee8e2b9 100644 --- a/lib/base/lib/assets/sync-checker/syncchecker-base.sh +++ b/lib/base/lib/assets/sync-checker/syncchecker-base.sh @@ -16,7 +16,12 @@ fi # L2 client stats L2_CLIENT_HEAD=$(echo $OPTIMISM_SYNC_STATUS | jq -r ".queued_unsafe_l2.number") L2_CLIENT_CURRENT=$(echo $OPTIMISM_SYNC_STATUS | jq -r ".unsafe_l2.number") -L2_CLIENT_BLOCKS_BEHIND="$((L2_CLIENT_HEAD-L2_CLIENT_CURRENT))" + +if [ "$L2_CLIENT_HEAD" == "null" ]; then + L2_CLIENT_BLOCKS_BEHIND=0 +else + L2_CLIENT_BLOCKS_BEHIND="$((L2_CLIENT_HEAD-L2_CLIENT_CURRENT))" +fi echo "L1_CLIENT_HEAD="$L1_CLIENT_HEAD echo "L1_CLIENT_CURRENT="$L1_CLIENT_CURRENT diff --git a/lib/base/sample-configs/.env-sample-rpc b/lib/base/sample-configs/.env-sample-rpc index a4b86c35..fc09f4c9 100644 --- a/lib/base/sample-configs/.env-sample-rpc +++ b/lib/base/sample-configs/.env-sample-rpc @@ -7,16 +7,20 @@ AWS_ACCOUNT_ID="xxxxxxxx" AWS_REGION="us-east-1" # Regions supported by Amazon Managed Blockchain Access Ethereum: https://docs.aws.amazon.com/general/latest/gr/managedblockchain.html#managedblockchain-access ## Common configuration parameters ## -AMB_ETHEREUM_NODE_NETWORK_ID="mainnet" # All options: "mainnet", "goerli" -AMB_ETHEREUM_NODE_INSTANCE_TYPE="bc.m5.xlarge" # For available options see: https://aws.amazon.com/managed-blockchain/instance-types/ BASE_NETWORK_ID="mainnet" # All options: "mainnet", "goerli" - BASE_INSTANCE_TYPE="m7g.2xlarge" BASE_CPU_TYPE="ARM_64" # All options: "x86_64", "ARM_64". IMPORTANT: Make sure the CPU type matches the instance type used + # Data volume configuration BASE_DATA_VOL_TYPE="gp3" # Other options: "io1" | "io2" | "gp3" | "instance-store" . IMPORTANT: Use "instance-store" option only with instance types that support that feature, like popular for node im4gn, d3, i3en, and i4i instance families -BASE_DATA_VOL_SIZE="3000" # Current required data size to keep both snapshot archive and unarchived version of it +BASE_DATA_VOL_SIZE="4100" # Current required data size to keep both snapshot archive and unarchived version of it BASE_DATA_VOL_IOPS="5000" # Max IOPS for EBS volumes (not applicable for "instance-store") BASE_DATA_VOL_THROUGHPUT="700" # Max throughput for EBS gp3 volumes (not applicable for "io1" | "io2" | "instance-store") -BASE_RESTORE_FROM_SNAPSHOT="false" # Download snapshot to speed up statup time -BASE_L1_ENDPOINT="none" # Set your own URL to Ethereum L1 node: https://docs.base.org/tools/node-providers \ No newline at end of file +BASE_RESTORE_FROM_SNAPSHOT="true" # Download snapshot to speed up statup time +BASE_L1_ENDPOINT="none" # Set your own URL to Ethereum L1 node: https://docs.base.org/tools/node-providers + +# Example L1 URLs: +# For Mainnet: +#BASE_L1_ENDPOINT=https://1rpc.io/eth +# For Sepolia: +#BASE_L1_ENDPOINT=https://rpc.sepolia.org \ No newline at end of file From c6f541b164f5fa7bb31a5a08dea40d4a0197ecb7 Mon Sep 17 00:00:00 2001 From: Nikolay Vlasov Date: Thu, 8 Feb 2024 13:39:41 +1100 Subject: [PATCH 38/75] Base Removed AMB node stack for now --- lib/base/app.ts | 9 --- .../lib/amb-ethereum-single-node-stack.ts | 43 -------------- lib/base/lib/config/baseConfig.interface.ts | 2 - lib/base/lib/config/baseConfig.ts | 2 - lib/base/test/.env-test | 2 - lib/base/test/base-ethereum-l1-node.test.ts | 57 ------------------- lib/base/test/base-single-node.test.ts | 16 +++++- 7 files changed, 14 insertions(+), 117 deletions(-) delete mode 100644 lib/base/lib/amb-ethereum-single-node-stack.ts delete mode 100644 lib/base/test/base-ethereum-l1-node.test.ts diff --git a/lib/base/app.ts b/lib/base/app.ts index da467559..81529827 100644 --- a/lib/base/app.ts +++ b/lib/base/app.ts @@ -4,7 +4,6 @@ import 'source-map-support/register'; import * as cdk from 'aws-cdk-lib'; import * as config from "./lib/config/baseConfig"; import {BaseCommonStack} from "./lib/common-stack"; -import {BaseAMBEthereumSingleNodeStack} from "./lib/amb-ethereum-single-node-stack"; import {BaseSingleNodeStack} from "./lib/single-node-stack"; const app = new cdk.App(); @@ -15,14 +14,6 @@ new BaseCommonStack(app, "base-common", { env: { account: config.baseConfig.accountId, region: config.baseConfig.region }, }); -new BaseAMBEthereumSingleNodeStack(app, "base-ethereum-l1-node", { - stackName: `base-amb-ethereum-single-node-${config.baseNodeConfig.baseNetworkId}`, - env: { account: config.baseConfig.accountId, region: config.baseConfig.region }, - - ambEthereumNodeNetworkId: config.baseNodeConfig.ambEntereumNodeNetworkId, - ambEthereumNodeInstanceType: config.baseNodeConfig.ambEntereumNodeInstanceType, -}); - new BaseSingleNodeStack(app, "base-single-node", { stackName: `base-single-node-${config.baseNodeConfig.baseNetworkId}`, env: { account: config.baseConfig.accountId, region: config.baseConfig.region }, diff --git a/lib/base/lib/amb-ethereum-single-node-stack.ts b/lib/base/lib/amb-ethereum-single-node-stack.ts deleted file mode 100644 index 614a171f..00000000 --- a/lib/base/lib/amb-ethereum-single-node-stack.ts +++ /dev/null @@ -1,43 +0,0 @@ -import * as cdk from "aws-cdk-lib"; -import * as cdkConstructs from "constructs"; -import * as configTypes from "./config/baseConfig.interface"; -import { SingleNodeAMBEthereumConstruct } from "../../constructs/amb-ethereum-single-node"; - -export interface BaseAMBEthereumSingleNodeStackProps extends cdk.StackProps { - ambEthereumNodeNetworkId: configTypes.AMBEthereumNodeNetworkId, - ambEthereumNodeInstanceType: string, -} - -export class BaseAMBEthereumSingleNodeStack extends cdk.Stack { - constructor(scope: cdkConstructs.Construct, id: string, props: BaseAMBEthereumSingleNodeStackProps) { - super(scope, id, props); - - // Setting up necessary environment variables - const availabilityZones = cdk.Stack.of(this).availabilityZones; - const chosenAvailabilityZone = availabilityZones.slice(0, 1)[0]; - - // Getting our config from initialization properties - const { - ambEthereumNodeNetworkId, - ambEthereumNodeInstanceType, - } = props; - - // Setting up L1 Ethereum node with AMB Ethereum node construct - - const ambEthereumNode = new SingleNodeAMBEthereumConstruct(this, "base-amb-ethereum-l1-single-node", { - instanceType: ambEthereumNodeInstanceType, - availabilityZone: chosenAvailabilityZone, - ethNetworkId: ambEthereumNodeNetworkId, - }) - - new cdk.CfnOutput(this, "amb-eth-node-id", { - value: ambEthereumNode.nodeId, - exportName: "BaseAmbEthereumNodeId" - }); - - new cdk.CfnOutput(this, "amb-eth-node-rpc-url-billing-token", { - value: ambEthereumNode.rpcUrlWithBillingToken, - exportName: "BaseAmbEthereumNodeRpcUrlWithBillingToken", - }); - } -} diff --git a/lib/base/lib/config/baseConfig.interface.ts b/lib/base/lib/config/baseConfig.interface.ts index 89290d0f..dabce7c9 100644 --- a/lib/base/lib/config/baseConfig.interface.ts +++ b/lib/base/lib/config/baseConfig.interface.ts @@ -15,8 +15,6 @@ export interface BaseBaseConfig extends configTypes.BaseConfig { } export interface BaseBaseNodeConfig extends configTypes.BaseNodeConfig { - ambEntereumNodeNetworkId: configTypes.AMBEthereumNodeNetworkId; - ambEntereumNodeInstanceType: string; baseNetworkId: BaseNetworkId; dataVolume: BaseDataVolumeConfig; restoreFromSnapshot: boolean; diff --git a/lib/base/lib/config/baseConfig.ts b/lib/base/lib/config/baseConfig.ts index 73ba97c7..ae3cf87b 100644 --- a/lib/base/lib/config/baseConfig.ts +++ b/lib/base/lib/config/baseConfig.ts @@ -24,8 +24,6 @@ export const baseConfig: configTypes.BaseBaseConfig = { } export const baseNodeConfig: configTypes.BaseBaseNodeConfig = { - ambEntereumNodeNetworkId: process.env.AMB_ENTEREUM_NODE_NETWORK_ID || "mainnet", - ambEntereumNodeInstanceType: process.env.AMB_ETHEREUM_NODE_INSTANCE_TYPE || "bc.m5.xlarge", instanceType: new ec2.InstanceType(process.env.BASE_INSTANCE_TYPE ? process.env.BASE_INSTANCE_TYPE : "m6a.2xlarge"), instanceCpuType: process.env.BASE_CPU_TYPE?.toLowerCase() == "x86_64" ? ec2.AmazonLinuxCpuType.X86_64 : ec2.AmazonLinuxCpuType.ARM_64, baseNetworkId: process.env.BASE_NETWORK_ID || "mainnet", diff --git a/lib/base/test/.env-test b/lib/base/test/.env-test index 70552ed3..dae5ca49 100644 --- a/lib/base/test/.env-test +++ b/lib/base/test/.env-test @@ -7,8 +7,6 @@ AWS_ACCOUNT_ID="347616198663" AWS_REGION="us-east-1" # Regions supported by Amazon Managed Blockchain Access Ethereum: https://docs.aws.amazon.com/general/latest/gr/managedblockchain.html#managedblockchain-access ## Common configuration parameters ## -AMB_ETHEREUM_NODE_NETWORK_ID="mainnet" # All options: "mainnet", "goerli" -AMB_ETHEREUM_NODE_INSTANCE_TYPE="bc.m5.xlarge" # For available options see: https://aws.amazon.com/managed-blockchain/instance-types/ BASE_NETWORK_ID="mainnet" # All options: "mainnet" BASE_INSTANCE_TYPE="m6a.2xlarge" diff --git a/lib/base/test/base-ethereum-l1-node.test.ts b/lib/base/test/base-ethereum-l1-node.test.ts deleted file mode 100644 index bdab1a90..00000000 --- a/lib/base/test/base-ethereum-l1-node.test.ts +++ /dev/null @@ -1,57 +0,0 @@ -import {Match, Template} from "aws-cdk-lib/assertions"; -import * as cdk from "aws-cdk-lib"; -import * as dotenv from 'dotenv'; - -dotenv.config({path: './test/.env-test'}); -import * as config from "../lib/config/baseConfig"; -import {BaseAMBEthereumSingleNodeStack} from "../lib/amb-ethereum-single-node-stack"; - -describe("BaseAMBEthereumSingleNodeStack", () => { - let app: cdk.App; - let baseAMBEthereumSingleNode: BaseAMBEthereumSingleNodeStack; - let template: Template; - beforeAll(() => { - app = new cdk.App(); - - // Create the BaseAMBEthereumSingleNodeStack. - - baseAMBEthereumSingleNode = new BaseAMBEthereumSingleNodeStack(app, "base-ethereum-l1-node", { - stackName: `base-amb-ethereum-single-node-${config.baseNodeConfig.baseNetworkId}`, - env: { account: config.baseConfig.accountId, region: config.baseConfig.region }, - - ambEthereumNodeNetworkId: config.baseNodeConfig.ambEntereumNodeNetworkId, - ambEthereumNodeInstanceType: config.baseNodeConfig.ambEntereumNodeInstanceType, - }); - - template = Template.fromStack(baseAMBEthereumSingleNode); - }); - - test("Check Node URL is correct", () => { - template.hasOutput("ambethnoderpcurlbillingtoken", { - Value: { - "Fn::Join": [ - "", - [ - "https://", - { - "Fn::GetAtt": [ - Match.anyValue(), - "NodeId" - ] - }, - ".t.ethereum.managedblockchain.us-east-1.amazonaws.com?billingtoken=", - { - "Fn::GetAtt": [ - Match.anyValue(), - "BillingToken" - ] - } - ] - ] - }, - "Export": { - "Name": "BaseAmbEthereumNodeRpcUrlWithBillingToken" - } - }) - }); -}); diff --git a/lib/base/test/base-single-node.test.ts b/lib/base/test/base-single-node.test.ts index e7f6dbac..80c48d47 100644 --- a/lib/base/test/base-single-node.test.ts +++ b/lib/base/test/base-single-node.test.ts @@ -16,11 +16,13 @@ describe("BaseSingleNodeStack", () => { // Create the BaseSingleNodeStack. baseSingleNodeStack = new BaseSingleNodeStack(app, "base-single-node", { stackName: `base-single-node-${config.baseNodeConfig.baseNetworkId}`, - env: {account: config.baseConfig.accountId, region: config.baseConfig.region}, + env: { account: config.baseConfig.accountId, region: config.baseConfig.region }, instanceType: config.baseNodeConfig.instanceType, instanceCpuType: config.baseNodeConfig.instanceCpuType, baseNetworkId: config.baseNodeConfig.baseNetworkId, + restoreFromSnapshot: config.baseNodeConfig.restoreFromSnapshot, + l1Endpoint: config.baseNodeConfig.l1Endpoint, dataVolume: config.baseNodeConfig.dataVolume, }); @@ -115,7 +117,17 @@ describe("BaseSingleNodeStack", () => { // Has CloudWatch dashboard. template.hasResourceProperties("AWS::CloudWatch::Dashboard", { DashboardBody: Match.anyValue(), - DashboardName: `base-single-node-${config.baseNodeConfig.baseNetworkId}` + DashboardName: { + "Fn::Join": [ + "", + [ + "base-single-node-mainnet-", + { + "Ref": Match.anyValue() + } + ] + ] + } }) }); }); From 48deb800e88d9965465712976f8f2db0a0f4c405 Mon Sep 17 00:00:00 2001 From: Danyal Prout Date: Tue, 13 Feb 2024 21:52:46 -0600 Subject: [PATCH 39/75] Remove Goerli / update docs --- lib/base/README.md | 2 +- lib/base/lib/assets/user-data/node.sh | 4 ---- lib/base/sample-configs/.env-sample-rpc | 2 +- 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/lib/base/README.md b/lib/base/README.md index 07a9fad2..3397f6c6 100644 --- a/lib/base/README.md +++ b/lib/base/README.md @@ -74,7 +74,7 @@ Base node needs a URL to a Full Ethereum Node to validate blocks it receives. Yo ### On your Cloud9: Clone this repository and install dependencies ```bash - git clone https://github.com/alickwong/aws-blockchain-node-runners + git clone https://github.com/aws-samples/aws-blockchain-node-runners cd aws-blockchain-node-runners npm install ``` diff --git a/lib/base/lib/assets/user-data/node.sh b/lib/base/lib/assets/user-data/node.sh index daf9a26e..9a7f2749 100644 --- a/lib/base/lib/assets/user-data/node.sh +++ b/lib/base/lib/assets/user-data/node.sh @@ -151,10 +151,6 @@ case $NETWORK_ID in sed -i "s#OP_NODE_L1_ETH_RPC=https://1rpc.io/eth#OP_NODE_L1_ETH_RPC=$L1_ENDPOINT#g" /home/bcuser/node/.env.mainnet sed -i '/.env.mainnet/s/^#//g' /home/bcuser/node/docker-compose.yml ;; - "goerli") - sed -i "s#OP_NODE_L1_ETH_RPC=https://Base-goerli-rpc.allthatnode.com#OP_NODE_L1_ETH_RPC=$L1_ENDPOINT#g" /home/bcuser/node/.env.goerli - sed -i "s/.env.goerli/s/^#//g" /home/bcuser/node/docker-compose.yml - ;; "sepolia") sed -i "s#OP_NODE_L1_ETH_RPC=https://rpc.sepolia.org#OP_NODE_L1_ETH_RPC=$L1_ENDPOINT#g" /home/bcuser/node/.env.sepolia sed -i "s/.env.sepolia/s/^#//g" /home/bcuser/node/docker-compose.yml diff --git a/lib/base/sample-configs/.env-sample-rpc b/lib/base/sample-configs/.env-sample-rpc index fc09f4c9..11c5d445 100644 --- a/lib/base/sample-configs/.env-sample-rpc +++ b/lib/base/sample-configs/.env-sample-rpc @@ -7,7 +7,7 @@ AWS_ACCOUNT_ID="xxxxxxxx" AWS_REGION="us-east-1" # Regions supported by Amazon Managed Blockchain Access Ethereum: https://docs.aws.amazon.com/general/latest/gr/managedblockchain.html#managedblockchain-access ## Common configuration parameters ## -BASE_NETWORK_ID="mainnet" # All options: "mainnet", "goerli" +BASE_NETWORK_ID="mainnet" # All options: "mainnet", "sepolia" BASE_INSTANCE_TYPE="m7g.2xlarge" BASE_CPU_TYPE="ARM_64" # All options: "x86_64", "ARM_64". IMPORTANT: Make sure the CPU type matches the instance type used From 8a8981f8d948043d428e913a112f8d051441dd8d Mon Sep 17 00:00:00 2001 From: Nikolay Vlasov Date: Mon, 4 Mar 2024 17:36:39 +1100 Subject: [PATCH 40/75] Base. Added newly required param for L1 Consensus URL --- lib/base/README.md | 22 +++++++++++---------- lib/base/app.ts | 3 ++- lib/base/lib/assets/user-data/node.sh | 17 ++++++++++------ lib/base/lib/config/baseConfig.interface.ts | 3 ++- lib/base/lib/config/baseConfig.ts | 5 +++-- lib/base/lib/single-node-stack.ts | 21 ++++++++++++-------- lib/base/sample-configs/.env-sample-rpc | 22 +++++++++++---------- lib/base/tsconfig.json | 2 +- 8 files changed, 56 insertions(+), 39 deletions(-) diff --git a/lib/base/README.md b/lib/base/README.md index 3397f6c6..08a0d08c 100644 --- a/lib/base/README.md +++ b/lib/base/README.md @@ -1,6 +1,10 @@ # Sample AWS Blockchain Node Runner app for Base Nodes -[Base](https://base.org/) is a "Layer 2" scaling solution for Ethereum. This blueprint helps to deploy Base RPC nodes on AWS and use [Amazon Managed Blockchain Access Ethereum](https://docs.aws.amazon.com/managed-blockchain/latest/ethereum-dev/ethereum-concepts.html) node for "Layer 1". It is meant to be used for development, testing or Proof of Concept purposes. +[Base](https://base.org/) is a "Layer 2" scaling solution for Ethereum. This blueprint helps to deploy Base RPC nodes on AWS. It is meant to be used for development, testing or Proof of Concept purposes. + +| Contributed by | +|:---------------| +|[@frbrkoala](https://github.com/frbrkoala), [@danyalprout](https://github.com/danyalprout)| ## Overview of Deployment Architectures for Single Node setups @@ -15,6 +19,7 @@ ## Additional materials +
Review the for pros and cons of this solution. ### Well-Architected Checklist @@ -33,7 +38,7 @@ This is the Well-Architected checklist for Ethereum nodes implementation of the | | | Following principle of least privilege access | In the node, root user is not used (using special user "bcuser" instead). | | | Application security | Security focused development practices | cdk-nag is being used with documented suppressions. | | Cost optimization | Service selection | Use cost effective resources | Base nodes works well on ARM architecture and we use Graviton3-powered EC2 instances for better cost effectiveness. | -| | Cost awareness | Estimate costs | One Base node on m7g.2xlarge and 3TiB EBS gp3 volume will cost around US$503.27 per month in the US East (N. Virginia) region. Additional charges will be applied for Ethereum L1 node and might vary between US$200 and US$500 per month. Approximately the total cost will be US$503.27 + US$500 = US$1003.27 per month. | +| | Cost awareness | Estimate costs | One Base node with on-Demand priced m7g.2xlarge and 3TiB EBS gp3 volume will cost around US$599.27 per month in the US East (N. Virginia) region. Additional charges will apply for Ethereum L1 node and will depend on the service used. | | Reliability | Resiliency implementation | Withstand component failures | This solution currently does not have high availability and is deployed to a single availability zone. | | | Data backup | How is data backed up? | The data is not specially backed up. The node will have to re-sync its state from other nodes in the Base network to recover. | | | Resource monitoring | How are workload resources monitored? | Resources are being monitored using Amazon CloudWatch dashboards. Amazon CloudWatch custom metrics are being pushed via CloudWatch Agent. | @@ -54,10 +59,10 @@ This is the Well-Architected checklist for Ethereum nodes implementation of the - Instance type [m7g.2xlarge](https://aws.amazon.com/ec2/instance-types/m7g/). - 2500GB EBS gp3 storage with at least 5000 IOPS. -**Recommended for Base node** +**Recommended for Base node on maiinet** - Instance type [m7g.4xlarge](https://aws.amazon.com/ec2/instance-types/m7g/). -- 2500GB EBS gp3 storage with at least 6000 IOPS.` +- 4100GB EBS gp3 storage with at least 6000 IOPS.`
@@ -69,7 +74,7 @@ We will use AWS Cloud9 to execute the subsequent commands. Follow the instructio ### Make sure you have access to Ethereum L1 node -Base node needs a URL to a Full Ethereum Node to validate blocks it receives. You can run your own with [Ethereum node blueprint](https://aws-samples.github.io/aws-blockchain-node-runners/docs/Blueprints/Ethereum) or use [one of Base partners](https://docs.base.org/tools/node-providers). +Base node needs a URL to a Full Ethereum Node to validate blocks it receives. You can run your own with [Ethereum node blueprint](https://aws-samples.github.io/aws-blockchain-node-runners/docs/Blueprints/Ethereum) or use [one of partners of Base](https://docs.base.org/tools/node-providers). ### On your Cloud9: Clone this repository and install dependencies @@ -98,13 +103,10 @@ Base node needs a URL to a Full Ethereum Node to validate blocks it receives. Yo ```bash # Make sure you are in aws-blockchain-node-runners/lib/base cd lib/base -npm install pwd cp ./sample-configs/.env-sample-rpc .env nano .env ``` - > NOTE: - > Example configuration parameters are set in the local `.env-sample` file. You can find more examples inside `sample-configs` directory. 4. Deploy common components such as IAM role @@ -139,7 +141,7 @@ pwd # Make sure you are in aws-blockchain-node-runners/lib/base npx cdk deploy base-single-node --json --outputs-file single-node-deploy.json ``` -After starting the node you will need to wait for the initial synchronization process to finish.To see the progress, you may use SSM to connect into EC2 first and watch the log like this: +A node connected to Sepolia network should start within an hour, while syncing nodes with Mainnet might take a while. Although you can force the node to use snapshots provided by Base team by setting `BASE_RESTORE_FROM_SNAPSHOT="true"` in `.env` file, you might still need to watch your node ifinish synchronizing. You can watch the progress with CloudWatch dashboard (see [Monitoring](#monitoring)) or check the progress manually. For manual access, use SSM to connect into EC2 first and watch the log like this: ```bash export INSTANCE_ID=$(cat single-node-deploy.json | jq -r '..|.node-instance-id? | select(. != null)') @@ -167,7 +169,7 @@ curl -s -X POST -H "Content-Type: application/json" --data '{"jsonrpc":"2.0","me A script on the Base node publishes current block and blocks behind metrics to CloudWatch metrics every 5 minutes. When the node is fully synced the blocks behind metric should get to 0, which might take about 1.5 days. To see the metrics: - Navigate to CloudWatch service (make sure you are in the region you have specified for AWS_REGION) -- Open Dashboards and select `base-single-node-` from the list of dashboards. +- Open Dashboards and select `base-single-node--` from the list of dashboards. ## From your Cloud9: Clear up and undeploy everything diff --git a/lib/base/app.ts b/lib/base/app.ts index 81529827..eb61a5cd 100644 --- a/lib/base/app.ts +++ b/lib/base/app.ts @@ -22,6 +22,7 @@ new BaseSingleNodeStack(app, "base-single-node", { instanceCpuType: config.baseNodeConfig.instanceCpuType, baseNetworkId: config.baseNodeConfig.baseNetworkId, restoreFromSnapshot: config.baseNodeConfig.restoreFromSnapshot, - l1Endpoint: config.baseNodeConfig.l1Endpoint, + l1ExecutionEndpoint: config.baseNodeConfig.l1ExecutionEndpoint, + l1ConsensusEndpoint: config.baseNodeConfig.l1ConsensusEndpoint, dataVolume: config.baseNodeConfig.dataVolume, }); diff --git a/lib/base/lib/assets/user-data/node.sh b/lib/base/lib/assets/user-data/node.sh index 9a7f2749..4f7d1679 100644 --- a/lib/base/lib/assets/user-data/node.sh +++ b/lib/base/lib/assets/user-data/node.sh @@ -77,15 +77,16 @@ STACK_NAME=${_STACK_NAME_} RESTORE_FROM_SNAPSHOT=${_RESTORE_FROM_SNAPSHOT_} FORMAT_DISK=${_FORMAT_DISK_} NETWORK_ID=${_NETWORK_ID_} -L1_ENDPOINT=${_L1_ENDPOINT_} +L1_EXECUTION_ENDPOINT=${_L1_EXECUTION_ENDPOINT_} +L1_CONSENSUS_ENDPOINT=${_L1_CONSENSUS_ENDPOINT_} echo "REGION=$REGION" >> /etc/environment echo "NETWORK_ID=$NETWORK_ID" >> /etc/environment -echo "L1_ENDPOINT=$L1_ENDPOINT" >> /etc/environment +echo "L1_EXECUTION_ENDPOINT=$L1_EXECUTION_ENDPOINT" >> /etc/environment +echo "L1_CONSENSUS_ENDPOINT=$L1_CONSENSUS_ENDPOINT" >> /etc/environment GIT_URL=https://github.com/base-org/node.git SYNC_CHECKER_FILE_NAME=syncchecker-base.sh -SNAPSHOT_S3_PATH=s3://base-snapshots-$NETWORK_ID-archive.s3.amazonaws.com/$(curl https://base-snapshots-$NETWORK_ID-archive.s3.amazonaws.com/latest) yum -y install docker python3-pip cronie cronie-anacron gcc python3-devel git yum -y remove python-requests @@ -148,12 +149,16 @@ echo "Configuring node" case $NETWORK_ID in "mainnet") - sed -i "s#OP_NODE_L1_ETH_RPC=https://1rpc.io/eth#OP_NODE_L1_ETH_RPC=$L1_ENDPOINT#g" /home/bcuser/node/.env.mainnet + sed -i "s#OP_NODE_L1_ETH_RPC=https://1rpc.io/eth#OP_NODE_L1_ETH_RPC=$L1_EXECUTION_ENDPOINT#g" /home/bcuser/node/.env.mainnet sed -i '/.env.mainnet/s/^#//g' /home/bcuser/node/docker-compose.yml + sed -i '/OP_NODE_L1_BEACON/s/^#//g' /home/bcuser/node/.env.mainnet + sed -i "s#OP_NODE_L1_BEACON=https://your.sepolia.beacon.node/endpoint-here#OP_NODE_L1_BEACON=$L1_CONSENSUS_ENDPOINT#g" /home/bcuser/node/.env.mainnet ;; "sepolia") - sed -i "s#OP_NODE_L1_ETH_RPC=https://rpc.sepolia.org#OP_NODE_L1_ETH_RPC=$L1_ENDPOINT#g" /home/bcuser/node/.env.sepolia - sed -i "s/.env.sepolia/s/^#//g" /home/bcuser/node/docker-compose.yml + sed -i "s#OP_NODE_L1_ETH_RPC=https://rpc.sepolia.org#OP_NODE_L1_ETH_RPC=$L1_EXECUTION_ENDPOINT#g" /home/bcuser/node/.env.sepolia + sed -i "/.env.sepolia/s/^#//g" /home/bcuser/node/docker-compose.yml + sed -i '/OP_NODE_L1_BEACON/s/^#//g' /home/bcuser/node/.env.sepolia + sed -i "s#OP_NODE_L1_BEACON=https://your.sepolia.beacon.node/endpoint-here#OP_NODE_L1_BEACON=$L1_CONSENSUS_ENDPOINT#g" /home/bcuser/node/.env.sepolia ;; *) echo "Network id is not valid." diff --git a/lib/base/lib/config/baseConfig.interface.ts b/lib/base/lib/config/baseConfig.interface.ts index dabce7c9..24539158 100644 --- a/lib/base/lib/config/baseConfig.interface.ts +++ b/lib/base/lib/config/baseConfig.interface.ts @@ -18,5 +18,6 @@ export interface BaseBaseNodeConfig extends configTypes.BaseNodeConfig { baseNetworkId: BaseNetworkId; dataVolume: BaseDataVolumeConfig; restoreFromSnapshot: boolean; - l1Endpoint: string; + l1ExecutionEndpoint: string; + l1ConsensusEndpoint: string; } diff --git a/lib/base/lib/config/baseConfig.ts b/lib/base/lib/config/baseConfig.ts index ae3cf87b..9bfd345e 100644 --- a/lib/base/lib/config/baseConfig.ts +++ b/lib/base/lib/config/baseConfig.ts @@ -24,11 +24,12 @@ export const baseConfig: configTypes.BaseBaseConfig = { } export const baseNodeConfig: configTypes.BaseBaseNodeConfig = { - instanceType: new ec2.InstanceType(process.env.BASE_INSTANCE_TYPE ? process.env.BASE_INSTANCE_TYPE : "m6a.2xlarge"), + instanceType: new ec2.InstanceType(process.env.BASE_INSTANCE_TYPE ? process.env.BASE_INSTANCE_TYPE : "m7g.2xlarge"), instanceCpuType: process.env.BASE_CPU_TYPE?.toLowerCase() == "x86_64" ? ec2.AmazonLinuxCpuType.X86_64 : ec2.AmazonLinuxCpuType.ARM_64, baseNetworkId: process.env.BASE_NETWORK_ID || "mainnet", restoreFromSnapshot: process.env.BASE_RESTORE_FROM_SNAPSHOT?.toLowerCase() == "true" ? true : false, - l1Endpoint: process.env.BASE_L1_ENDPOINT || constants.NoneValue, + l1ExecutionEndpoint: process.env.BASE_L1_EXECUTION_ENDPOINT || constants.NoneValue, + l1ConsensusEndpoint: process.env.BASE_L1_CONSENSUS_ENDPOINT || constants.NoneValue, dataVolume: { sizeGiB: process.env.BASE_DATA_VOL_SIZE ? parseInt(process.env.BASE_DATA_VOL_SIZE): 1000, type: parseDataVolumeType(process.env.BASE_DATA_VOL_TYPE?.toLowerCase() ? process.env.BASE_DATA_VOL_TYPE?.toLowerCase() : "gp3"), diff --git a/lib/base/lib/single-node-stack.ts b/lib/base/lib/single-node-stack.ts index 07854438..b32fa3cc 100644 --- a/lib/base/lib/single-node-stack.ts +++ b/lib/base/lib/single-node-stack.ts @@ -18,7 +18,8 @@ export interface BaseSingleNodeStackProps extends cdk.StackProps { instanceCpuType: ec2.AmazonLinuxCpuType; baseNetworkId: configTypes.BaseNetworkId; restoreFromSnapshot: boolean; - l1Endpoint: string; + l1ExecutionEndpoint: string, + l1ConsensusEndpoint: string, dataVolume: configTypes.BaseDataVolumeConfig; } @@ -39,10 +40,18 @@ export class BaseSingleNodeStack extends cdk.Stack { instanceCpuType, baseNetworkId, restoreFromSnapshot, - l1Endpoint, + l1ExecutionEndpoint, + l1ConsensusEndpoint, dataVolume, } = props; + if (l1ExecutionEndpoint === constants.NoneValue){ + throw new Error("L1 Execution Endpoint cannot be set to None. Set BASE_L1_EXECUTION_ENDPOINT "); + } + if (l1ConsensusEndpoint === constants.NoneValue){ + throw new Error("L1 Consensus Endpoint cannot be set to None. Set BASE_L1_CONSENSUS_ENDPOINT "); + } + // Using default VPC const vpc = ec2.Vpc.fromLookup(this, "vpc", { isDefault: true }); @@ -59,11 +68,6 @@ export class BaseSingleNodeStack extends cdk.Stack { // Getting the IAM role ARN from the common stack const importedInstanceRoleArn = cdk.Fn.importValue("BaseNodeInstanceRoleArn"); - // If user has not supplied the URL for L1, attempting to use AMB node URL - let l1EndpointURL = l1Endpoint; - if (l1EndpointURL === constants.NoneValue){ - l1EndpointURL = cdk.Fn.importValue("BaseAmbEthereumNodeRpcUrlWithBillingToken"); - } const instanceRole = iam.Role.fromRoleArn(this, "iam-role", importedInstanceRoleArn); // Making sure our instance will be able to read the assets @@ -103,7 +107,8 @@ export class BaseSingleNodeStack extends cdk.Stack { _AUTOSCALING_GROUP_NAME_: constants.NoneValue, _RESTORE_FROM_SNAPSHOT_: restoreFromSnapshot.toString(), _FORMAT_DISK_: "true", - _L1_ENDPOINT_: l1EndpointURL, + _L1_EXECUTION_ENDPOINT_: l1ExecutionEndpoint, + _L1_CONSENSUS_ENDPOINT_: l1ConsensusEndpoint, }); node.instance.addUserData(modifiedInitNodeScript); diff --git a/lib/base/sample-configs/.env-sample-rpc b/lib/base/sample-configs/.env-sample-rpc index 11c5d445..de38914b 100644 --- a/lib/base/sample-configs/.env-sample-rpc +++ b/lib/base/sample-configs/.env-sample-rpc @@ -4,23 +4,25 @@ ## Set the AWS account is and region for your environment ## AWS_ACCOUNT_ID="xxxxxxxx" -AWS_REGION="us-east-1" # Regions supported by Amazon Managed Blockchain Access Ethereum: https://docs.aws.amazon.com/general/latest/gr/managedblockchain.html#managedblockchain-access +AWS_REGION="us-east-1" ## Common configuration parameters ## -BASE_NETWORK_ID="mainnet" # All options: "mainnet", "sepolia" +BASE_NETWORK_ID="sepolia" # All options: "mainnet", "sepolia" BASE_INSTANCE_TYPE="m7g.2xlarge" BASE_CPU_TYPE="ARM_64" # All options: "x86_64", "ARM_64". IMPORTANT: Make sure the CPU type matches the instance type used # Data volume configuration BASE_DATA_VOL_TYPE="gp3" # Other options: "io1" | "io2" | "gp3" | "instance-store" . IMPORTANT: Use "instance-store" option only with instance types that support that feature, like popular for node im4gn, d3, i3en, and i4i instance families -BASE_DATA_VOL_SIZE="4100" # Current required data size to keep both snapshot archive and unarchived version of it +BASE_DATA_VOL_SIZE="4100" # Current required data size in GB to keep both snapshot archive and unarchived version of it BASE_DATA_VOL_IOPS="5000" # Max IOPS for EBS volumes (not applicable for "instance-store") BASE_DATA_VOL_THROUGHPUT="700" # Max throughput for EBS gp3 volumes (not applicable for "io1" | "io2" | "instance-store") -BASE_RESTORE_FROM_SNAPSHOT="true" # Download snapshot to speed up statup time -BASE_L1_ENDPOINT="none" # Set your own URL to Ethereum L1 node: https://docs.base.org/tools/node-providers +BASE_RESTORE_FROM_SNAPSHOT="true" # Download snapshot to speed up statup time +BASE_L1_EXECUTION_ENDPOINT="https://ethereum-sepolia-rpc.publicnode.com" # Set your own URL to Ethereum L1 node: https://docs.base.org/tools/node-providers +BASE_L1_CONSENSUS_ENDPOINT=https://ethereum-sepolia-beacon-api.publicnode.com -# Example L1 URLs: -# For Mainnet: -#BASE_L1_ENDPOINT=https://1rpc.io/eth -# For Sepolia: -#BASE_L1_ENDPOINT=https://rpc.sepolia.org \ No newline at end of file +# Example for Sepolia: +#BASE_L1_EXECUTION_ENDPOINT=https://ethereum-sepolia-rpc.publicnode.com +#BASE_L1_CONSENSUS_ENDPOINT=https://ethereum-sepolia-beacon-api.publicnode.com +# Example for Mainnet and with Ethereum Blueprint with Geth-Lighthouse client combination and private IP: +#BASE_L1_EXECUTION_ENDPOINT=http://172.31.15.220:8545 +#BASE_L1_CONSENSUS_ENDPOINT=http://172.31.15.220:5052 \ No newline at end of file diff --git a/lib/base/tsconfig.json b/lib/base/tsconfig.json index aaa7dc51..8e1979f3 100644 --- a/lib/base/tsconfig.json +++ b/lib/base/tsconfig.json @@ -21,7 +21,7 @@ "experimentalDecorators": true, "strictPropertyInitialization": false, "typeRoots": [ - "./node_modules/@types" + "../../node_modules/@types" ] }, "exclude": [ From dabe6e4bd3c2887bebf6917d4489e9b99b06572b Mon Sep 17 00:00:00 2001 From: Nikolay Vlasov Date: Tue, 5 Mar 2024 11:41:46 +1100 Subject: [PATCH 41/75] Base. Corrected unite tests after adding new configuration prams --- lib/base/sample-configs/.env-sample-rpc | 2 +- lib/base/test/.env-test | 18 +++-- lib/base/test/base-common.test.ts | 98 +++++++++++++++++++++++++ lib/base/test/base-single-node.test.ts | 9 ++- 4 files changed, 117 insertions(+), 10 deletions(-) create mode 100644 lib/base/test/base-common.test.ts diff --git a/lib/base/sample-configs/.env-sample-rpc b/lib/base/sample-configs/.env-sample-rpc index de38914b..5e9eb2be 100644 --- a/lib/base/sample-configs/.env-sample-rpc +++ b/lib/base/sample-configs/.env-sample-rpc @@ -13,7 +13,7 @@ BASE_CPU_TYPE="ARM_64" # All options: "x86_64", "ARM_64". IMPORT # Data volume configuration BASE_DATA_VOL_TYPE="gp3" # Other options: "io1" | "io2" | "gp3" | "instance-store" . IMPORTANT: Use "instance-store" option only with instance types that support that feature, like popular for node im4gn, d3, i3en, and i4i instance families -BASE_DATA_VOL_SIZE="4100" # Current required data size in GB to keep both snapshot archive and unarchived version of it +BASE_DATA_VOL_SIZE="5100" # Current required data size in GB to keep both snapshot archive and unarchived version of it. For Sepolia 1000 will be sufficient. BASE_DATA_VOL_IOPS="5000" # Max IOPS for EBS volumes (not applicable for "instance-store") BASE_DATA_VOL_THROUGHPUT="700" # Max throughput for EBS gp3 volumes (not applicable for "io1" | "io2" | "instance-store") BASE_RESTORE_FROM_SNAPSHOT="true" # Download snapshot to speed up statup time diff --git a/lib/base/test/.env-test b/lib/base/test/.env-test index dae5ca49..f9a14ccd 100644 --- a/lib/base/test/.env-test +++ b/lib/base/test/.env-test @@ -3,16 +3,24 @@ ############################################################# ## Set the AWS account is and region for your environment ## -AWS_ACCOUNT_ID="347616198663" +AWS_ACCOUNT_ID="xxxxxxxxxxxx" AWS_REGION="us-east-1" # Regions supported by Amazon Managed Blockchain Access Ethereum: https://docs.aws.amazon.com/general/latest/gr/managedblockchain.html#managedblockchain-access ## Common configuration parameters ## BASE_NETWORK_ID="mainnet" # All options: "mainnet" -BASE_INSTANCE_TYPE="m6a.2xlarge" -BASE_CPU_TYPE="x86_64" # All options: "x86_64", "ARM_64". IMPORTANT: Make sure the CPU type matches the instance type used # Data volume configuration BASE_DATA_VOL_TYPE="gp3" # Other options: "io1" | "io2" | "gp3" | "instance-store" . IMPORTANT: Use "instance-store" option only with instance types that support that feature, like popular for node im4gn, d3, i3en, and i4i instance families -BASE_DATA_VOL_SIZE="1000" # Current required data size to keep both snapshot archive and unarchived version of it -BASE_DATA_VOL_IOPS="3000" # Max IOPS for EBS volumes (not applicable for "instance-store") +BASE_DATA_VOL_SIZE="5100" # Current required data size in GB to keep both snapshot archive and unarchived version of it. For Sepolia 1000 will be sufficient. +BASE_DATA_VOL_IOPS="5000" # Max IOPS for EBS volumes (not applicable for "instance-store") BASE_DATA_VOL_THROUGHPUT="700" # Max throughput for EBS gp3 volumes (not applicable for "io1" | "io2" | "instance-store") +BASE_RESTORE_FROM_SNAPSHOT="true" # Download snapshot to speed up statup time +BASE_L1_EXECUTION_ENDPOINT="https://ethereum-sepolia-rpc.publicnode.com" # Set your own URL to Ethereum L1 node: https://docs.base.org/tools/node-providers +BASE_L1_CONSENSUS_ENDPOINT=https://ethereum-sepolia-beacon-api.publicnode.com + +# Example for Sepolia: +#BASE_L1_EXECUTION_ENDPOINT=https://ethereum-sepolia-rpc.publicnode.com +#BASE_L1_CONSENSUS_ENDPOINT=https://ethereum-sepolia-beacon-api.publicnode.com +# Example for Mainnet and with Ethereum Blueprint with Geth-Lighthouse client combination and private IP: +#BASE_L1_EXECUTION_ENDPOINT=http://172.31.15.220:8545 +#BASE_L1_CONSENSUS_ENDPOINT=http://172.31.15.220:5052 diff --git a/lib/base/test/base-common.test.ts b/lib/base/test/base-common.test.ts new file mode 100644 index 00000000..4b9e657d --- /dev/null +++ b/lib/base/test/base-common.test.ts @@ -0,0 +1,98 @@ +import {Match, Template} from "aws-cdk-lib/assertions"; +import * as cdk from "aws-cdk-lib"; +import * as dotenv from 'dotenv'; + +dotenv.config({path: './test/.env-test'}); +import * as config from "../lib/config/baseConfig"; +import {BaseCommonStack} from "../lib/common-stack"; + +describe("BaseCommonStack", () => { + let app: cdk.App; + let baseCommonStack: BaseCommonStack; + let template: Template; + beforeAll(() => { + app = new cdk.App(); + + // Create the BaseCommonStack. + baseCommonStack = new BaseCommonStack(app, "base-single-node", { + stackName: `base-nodes-common`, + env: { account: config.baseConfig.accountId, region: config.baseConfig.region }, + }); + + template = Template.fromStack(baseCommonStack); + }); + + test("Check Node Instance Role", () => { + // Has EC2 instance security group. + template.hasResourceProperties("AWS::IAM::Role", { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: "sts:AssumeRole", + Effect: "Allow", + Principal: { + Service: "ec2.amazonaws.com" + } + } + ], + Version: "2012-10-17" + }, + ManagedPolicyArns: [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/AmazonSSMManagedInstanceCore" + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/CloudWatchAgentServerPolicy" + ] + ] + } + ] + }); + }); + + test("Check Node Instance Role Policy", () => { + // Has EC2 instance security group. + template.hasResourceProperties("AWS::IAM::Policy", { + PolicyDocument: { + Statement: [ + { + Action: "cloudformation:SignalResource", + Effect: "Allow", + Resource: "*" + }, + { + Action: "autoscaling:CompleteLifecycleAction", + Effect: "Allow", + Resource: "arn:aws:autoscaling:us-east-1:xxxxxxxxxxxx:autoScalingGroup:*:autoScalingGroupName/base-*" + }, + { + Action: "s3:*Object", + Effect: "Allow", + Resource: [ + "arn:aws:s3:::base-snapshots-*-archive", + "arn:aws:s3:::base-snapshots-*-archive/*" + ] + } + ], + "Version": "2012-10-17" + } + }); + }); + +}); diff --git a/lib/base/test/base-single-node.test.ts b/lib/base/test/base-single-node.test.ts index 80c48d47..ffdb7c94 100644 --- a/lib/base/test/base-single-node.test.ts +++ b/lib/base/test/base-single-node.test.ts @@ -22,7 +22,8 @@ describe("BaseSingleNodeStack", () => { instanceCpuType: config.baseNodeConfig.instanceCpuType, baseNetworkId: config.baseNodeConfig.baseNetworkId, restoreFromSnapshot: config.baseNodeConfig.restoreFromSnapshot, - l1Endpoint: config.baseNodeConfig.l1Endpoint, + l1ExecutionEndpoint: config.baseNodeConfig.l1ExecutionEndpoint, + l1ConsensusEndpoint: config.baseNodeConfig.l1ConsensusEndpoint, dataVolume: config.baseNodeConfig.dataVolume, }); @@ -87,7 +88,7 @@ describe("BaseSingleNodeStack", () => { ], IamInstanceProfile: Match.anyValue(), ImageId: Match.anyValue(), - InstanceType: "m6a.2xlarge", + InstanceType: "m7g.2xlarge", Monitoring: true, PropagateTagsToVolumeOnCreation: true, SecurityGroupIds: Match.anyValue(), @@ -98,9 +99,9 @@ describe("BaseSingleNodeStack", () => { template.hasResourceProperties("AWS::EC2::Volume", { AvailabilityZone: Match.anyValue(), Encrypted: true, - Iops: 3000, + Iops: 5000, MultiAttachEnabled: false, - Size: 1000, + Size: 5100, Throughput: 700, VolumeType: "gp3" }) From 6dc99cde79c6f35285b16eb53d49f9a06f1e6536 Mon Sep 17 00:00:00 2001 From: Nikolay Vlasov Date: Mon, 25 Mar 2024 12:43:18 +1100 Subject: [PATCH 42/75] Base. WIP. Modifications after testing --- lib/base/README.md | 12 +- lib/base/lib/assets/user-data/node.sh | 4 +- lib/base/package-lock.json | 641 ------------------------ lib/base/sample-configs/.env-sample-rpc | 2 +- 4 files changed, 7 insertions(+), 652 deletions(-) delete mode 100644 lib/base/package-lock.json diff --git a/lib/base/README.md b/lib/base/README.md index 08a0d08c..490d6f0d 100644 --- a/lib/base/README.md +++ b/lib/base/README.md @@ -124,14 +124,12 @@ npx cdk deploy base-common ### From your Cloud9: Deploy Single Node -1. For L1 node you you can set your own URL in `BASE_L1_ENDPOINT` property of `.env` file. It can be one of [the providers recommended by Base](https://docs.base.org/tools/node-providers) or run your own Ethereum node [with Node Runner blueprint](https://aws-samples.github.io/aws-blockchain-node-runners/docs/Blueprints/Ethereum). For example: +1. For L1 node you you can set your own URLs in `BASE_L1_EXECUTION_ENDPOINT` and `BASE_L1_CONSENSUS_ENDPOINT` properties of `.env` file. It can be one of [the providers recommended by Base](https://docs.base.org/tools/node-providers) or you can run your own Ethereum node [with Node Runner blueprint](https://aws-samples.github.io/aws-blockchain-node-runners/docs/Blueprints/Ethereum). For example: ```bash -#For Mainnet: -BASE_L1_ENDPOINT=https://1rpc.io/eth - #For Sepolia: -BASE_L1_ENDPOINT=https://rpc.sepolia.org +BASE_L1_EXECUTION_ENDPOINT="https://ethereum-sepolia-rpc.publicnode.com" +BASE_L1_CONSENSUS_ENDPOINT="https://ethereum-sepolia-beacon-api.publicnode.com" ``` 2. Deploy Base RPC Node and wait for it to sync. For Mainnet it might take less than an hour when using snapshots (default) or multiple days if syncing from block 0. @@ -141,7 +139,7 @@ pwd # Make sure you are in aws-blockchain-node-runners/lib/base npx cdk deploy base-single-node --json --outputs-file single-node-deploy.json ``` -A node connected to Sepolia network should start within an hour, while syncing nodes with Mainnet might take a while. Although you can force the node to use snapshots provided by Base team by setting `BASE_RESTORE_FROM_SNAPSHOT="true"` in `.env` file, you might still need to watch your node ifinish synchronizing. You can watch the progress with CloudWatch dashboard (see [Monitoring](#monitoring)) or check the progress manually. For manual access, use SSM to connect into EC2 first and watch the log like this: +A node connected to Sepolia network should start within an hour, while syncing nodes with Mainnet might take a while. Although you can use snapshots provided by the Base team by setting `BASE_RESTORE_FROM_SNAPSHOT="true"` in `.env` file, you might still have wait for your node to finish synchronizing. You can watch the progress with CloudWatch dashboard (see [Monitoring](#monitoring)) or check the progress manually. For manual access, use SSM to connect into EC2 first and watch the log like this: ```bash export INSTANCE_ID=$(cat single-node-deploy.json | jq -r '..|.node-instance-id? | select(. != null)') @@ -154,7 +152,7 @@ curl -d '{"id":0,"jsonrpc":"2.0","method":"optimism_syncStatus"}' \ jq -r .result.unsafe_l2.timestamp))/60)) minutes ``` -3. Test Base RPC API [TODO: Is there an address we can query balance from?] +3. Test Base RPC API Use curl to query from within the node instance: ```bash export INSTANCE_ID=$(cat single-node-deploy.json | jq -r '..|.node-instance-id? | select(. != null)') diff --git a/lib/base/lib/assets/user-data/node.sh b/lib/base/lib/assets/user-data/node.sh index 4f7d1679..c26bff4f 100644 --- a/lib/base/lib/assets/user-data/node.sh +++ b/lib/base/lib/assets/user-data/node.sh @@ -132,9 +132,7 @@ groupadd -g 1002 bcuser useradd -u 1002 -g 1002 -m -s /bin/bash bcuser usermod -a -G docker bcuser usermod -a -G docker ec2-user -chown -R bcuser:bcuser /secrets chmod -R 755 /home/bcuser -chmod -R 755 /secrets echo "Starting docker" service docker start @@ -152,7 +150,7 @@ case $NETWORK_ID in sed -i "s#OP_NODE_L1_ETH_RPC=https://1rpc.io/eth#OP_NODE_L1_ETH_RPC=$L1_EXECUTION_ENDPOINT#g" /home/bcuser/node/.env.mainnet sed -i '/.env.mainnet/s/^#//g' /home/bcuser/node/docker-compose.yml sed -i '/OP_NODE_L1_BEACON/s/^#//g' /home/bcuser/node/.env.mainnet - sed -i "s#OP_NODE_L1_BEACON=https://your.sepolia.beacon.node/endpoint-here#OP_NODE_L1_BEACON=$L1_CONSENSUS_ENDPOINT#g" /home/bcuser/node/.env.mainnet + sed -i "s#OP_NODE_L1_BEACON=https://your.mainet.beacon.node/endpoint-here#OP_NODE_L1_BEACON=$L1_CONSENSUS_ENDPOINT#g" /home/bcuser/node/.env.mainnet ;; "sepolia") sed -i "s#OP_NODE_L1_ETH_RPC=https://rpc.sepolia.org#OP_NODE_L1_ETH_RPC=$L1_EXECUTION_ENDPOINT#g" /home/bcuser/node/.env.sepolia diff --git a/lib/base/package-lock.json b/lib/base/package-lock.json deleted file mode 100644 index 5d2c6444..00000000 --- a/lib/base/package-lock.json +++ /dev/null @@ -1,641 +0,0 @@ -{ - "name": "aws-blockchain-node-runners-base", - "version": "0.1.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "aws-blockchain-node-runners-base", - "version": "0.1.0", - "dependencies": { - "@types/node": "^20.10.0" - }, - "devDependencies": { - "@types/jest": "^29.5.11" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", - "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", - "dev": true, - "dependencies": { - "@babel/highlight": "^7.23.4", - "chalk": "^2.4.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/code-frame/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/code-frame/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/code-frame/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@babel/code-frame/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "node_modules/@babel/code-frame/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@babel/code-frame/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/code-frame/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", - "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", - "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", - "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.22.20", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@babel/highlight/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "node_modules/@babel/highlight/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@babel/highlight/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@jest/expect-utils": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", - "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", - "dev": true, - "dependencies": { - "jest-get-type": "^29.6.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/schemas": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", - "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "dev": true, - "dependencies": { - "@sinclair/typebox": "^0.27.8" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/types": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", - "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", - "dev": true, - "dependencies": { - "@jest/schemas": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@sinclair/typebox": { - "version": "0.27.8", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", - "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", - "dev": true - }, - "node_modules/@types/istanbul-lib-coverage": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", - "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", - "dev": true - }, - "node_modules/@types/istanbul-lib-report": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", - "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", - "dev": true, - "dependencies": { - "@types/istanbul-lib-coverage": "*" - } - }, - "node_modules/@types/istanbul-reports": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", - "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", - "dev": true, - "dependencies": { - "@types/istanbul-lib-report": "*" - } - }, - "node_modules/@types/jest": { - "version": "29.5.11", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.11.tgz", - "integrity": "sha512-S2mHmYIVe13vrm6q4kN6fLYYAka15ALQki/vgDC3mIukEOx8WJlv0kQPM+d4w8Gp6u0uSdKND04IlTXBv0rwnQ==", - "dev": true, - "dependencies": { - "expect": "^29.0.0", - "pretty-format": "^29.0.0" - } - }, - "node_modules/@types/node": { - "version": "20.10.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.5.tgz", - "integrity": "sha512-nNPsNE65wjMxEKI93yOP+NPGGBJz/PoN3kZsVLee0XMiJolxSekEVD8wRwBUBqkwc7UWop0edW50yrCQW4CyRw==", - "dependencies": { - "undici-types": "~5.26.4" - } - }, - "node_modules/@types/stack-utils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", - "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", - "dev": true - }, - "node_modules/@types/yargs": { - "version": "17.0.32", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", - "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", - "dev": true, - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/@types/yargs-parser": { - "version": "21.0.3", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", - "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", - "dev": true - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "dependencies": { - "fill-range": "^7.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/ci-info": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", - "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "engines": { - "node": ">=8" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/diff-sequences": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", - "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", - "dev": true, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/escape-string-regexp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/expect": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", - "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", - "dev": true, - "dependencies": { - "@jest/expect-utils": "^29.7.0", - "jest-get-type": "^29.6.3", - "jest-matcher-utils": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/jest-diff": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", - "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", - "dev": true, - "dependencies": { - "chalk": "^4.0.0", - "diff-sequences": "^29.6.3", - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-get-type": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", - "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", - "dev": true, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-matcher-utils": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", - "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", - "dev": true, - "dependencies": { - "chalk": "^4.0.0", - "jest-diff": "^29.7.0", - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-message-util": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", - "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.12.13", - "@jest/types": "^29.6.3", - "@types/stack-utils": "^2.0.0", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "micromatch": "^4.0.4", - "pretty-format": "^29.7.0", - "slash": "^3.0.0", - "stack-utils": "^2.0.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-util": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", - "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", - "dev": true, - "dependencies": { - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "graceful-fs": "^4.2.9", - "picomatch": "^2.2.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true - }, - "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", - "dev": true, - "dependencies": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, - "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/pretty-format/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/react-is": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", - "dev": true - }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/stack-utils": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", - "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", - "dev": true, - "dependencies": { - "escape-string-regexp": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" - } - } -} diff --git a/lib/base/sample-configs/.env-sample-rpc b/lib/base/sample-configs/.env-sample-rpc index 5e9eb2be..e0960664 100644 --- a/lib/base/sample-configs/.env-sample-rpc +++ b/lib/base/sample-configs/.env-sample-rpc @@ -18,7 +18,7 @@ BASE_DATA_VOL_IOPS="5000" # Max IOPS for EBS volumes (not applicabl BASE_DATA_VOL_THROUGHPUT="700" # Max throughput for EBS gp3 volumes (not applicable for "io1" | "io2" | "instance-store") BASE_RESTORE_FROM_SNAPSHOT="true" # Download snapshot to speed up statup time BASE_L1_EXECUTION_ENDPOINT="https://ethereum-sepolia-rpc.publicnode.com" # Set your own URL to Ethereum L1 node: https://docs.base.org/tools/node-providers -BASE_L1_CONSENSUS_ENDPOINT=https://ethereum-sepolia-beacon-api.publicnode.com +BASE_L1_CONSENSUS_ENDPOINT="https://ethereum-sepolia-beacon-api.publicnode.com" # Example for Sepolia: #BASE_L1_EXECUTION_ENDPOINT=https://ethereum-sepolia-rpc.publicnode.com From 8fcbbb000be1a460e4463dc1c4693ca48fb63674 Mon Sep 17 00:00:00 2001 From: Nikolay Vlasov Date: Thu, 28 Mar 2024 17:00:02 +1100 Subject: [PATCH 43/75] Base Bug fixes in mainnet configuration after testing --- lib/base/README.md | 4 ++-- lib/base/lib/assets/node-cw-dashboard.ts | 4 ++-- .../assets/sync-checker/syncchecker-base.sh | 23 +++++++++---------- lib/base/lib/assets/user-data/node.sh | 2 +- 4 files changed, 16 insertions(+), 17 deletions(-) diff --git a/lib/base/README.md b/lib/base/README.md index 490d6f0d..3a083a3b 100644 --- a/lib/base/README.md +++ b/lib/base/README.md @@ -1,11 +1,11 @@ # Sample AWS Blockchain Node Runner app for Base Nodes -[Base](https://base.org/) is a "Layer 2" scaling solution for Ethereum. This blueprint helps to deploy Base RPC nodes on AWS. It is meant to be used for development, testing or Proof of Concept purposes. - | Contributed by | |:---------------| |[@frbrkoala](https://github.com/frbrkoala), [@danyalprout](https://github.com/danyalprout)| +[Base](https://base.org/) is a "Layer 2" scaling solution for Ethereum. This blueprint helps to deploy Base RPC nodes on AWS. It is meant to be used for development, testing or Proof of Concept purposes. + ## Overview of Deployment Architectures for Single Node setups ### Single node setup diff --git a/lib/base/lib/assets/node-cw-dashboard.ts b/lib/base/lib/assets/node-cw-dashboard.ts index 2d2c926e..7dbabdad 100644 --- a/lib/base/lib/assets/node-cw-dashboard.ts +++ b/lib/base/lib/assets/node-cw-dashboard.ts @@ -159,9 +159,9 @@ export const SyncNodeCWDashboardJSON = { "stat": "Maximum", "period": 60, "metrics": [ - [ "CWAgent", "l2_blocks_behind", "InstanceId", "${INSTANCE_ID}", { "label": "${INSTANCE_ID}-${INSTANCE_NAME}" } ] + [ "CWAgent", "l2_minutes_behind", "InstanceId", "${INSTANCE_ID}", { "label": "minutes" } ] ], - "title": "L2 Blocks Behind" + "title": "L2 Minutes Behind" } }, { diff --git a/lib/base/lib/assets/sync-checker/syncchecker-base.sh b/lib/base/lib/assets/sync-checker/syncchecker-base.sh index fee8e2b9..b7c4aff4 100644 --- a/lib/base/lib/assets/sync-checker/syncchecker-base.sh +++ b/lib/base/lib/assets/sync-checker/syncchecker-base.sh @@ -14,22 +14,21 @@ else fi # L2 client stats -L2_CLIENT_HEAD=$(echo $OPTIMISM_SYNC_STATUS | jq -r ".queued_unsafe_l2.number") L2_CLIENT_CURRENT=$(echo $OPTIMISM_SYNC_STATUS | jq -r ".unsafe_l2.number") +L2_CLIENT_CURRENT_BLOCK_TIMESTAMP=$(echo $OPTIMISM_SYNC_STATUS | jq -r ".unsafe_l2.timestamp") +L2_CLIENT_CURRENT_BLOCK_MINUTES_BEHIND="$((($(date +%s) - L2_CLIENT_CURRENT_BLOCK_TIMESTAMP)/60))" -if [ "$L2_CLIENT_HEAD" == "null" ]; then - L2_CLIENT_BLOCKS_BEHIND=0 -else - L2_CLIENT_BLOCKS_BEHIND="$((L2_CLIENT_HEAD-L2_CLIENT_CURRENT))" +if [ "$L2_CLIENT_CURRENT" == "null" ]; then + L2_CLIENT_CURRENT=0 fi -echo "L1_CLIENT_HEAD="$L1_CLIENT_HEAD -echo "L1_CLIENT_CURRENT="$L1_CLIENT_CURRENT -echo "L1_CLIENT_BLOCKS_BEHIND="$L1_CLIENT_BLOCKS_BEHIND +# echo "L1_CLIENT_HEAD="$L1_CLIENT_HEAD +# echo "L1_CLIENT_CURRENT="$L1_CLIENT_CURRENT +# echo "L1_CLIENT_BLOCKS_BEHIND="$L1_CLIENT_BLOCKS_BEHIND -echo "L2_CLIENT_HEAD="$L2_CLIENT_HEAD -echo "L2_CLIENT_CURRENT="$L2_CLIENT_CURRENT -echo "L2_CLIENT_BLOCKS_BEHIND="$L2_CLIENT_BLOCKS_BEHIND +# echo "L2_CLIENT_CURRENT="$L2_CLIENT_CURRENT +# echo "L2_CLIENT_CURRENT_BLOCK_TIMESTAMP="$L2_CLIENT_CURRENT_BLOCK_TIMESTAMP +# echo "L2_CLIENT_CURRENT_BLOCK_MINUTES_BEHIND="$L2_CLIENT_CURRENT_BLOCK_MINUTES_BEHIND # Sending data to CloudWatch TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600") @@ -41,5 +40,5 @@ aws cloudwatch put-metric-data --metric-name l1_current_block --namespace CWAgen aws cloudwatch put-metric-data --metric-name l1_blocks_behind --namespace CWAgent --value $L1_CLIENT_BLOCKS_BEHIND --timestamp $TIMESTAMP --dimensions InstanceId=$INSTANCE_ID --region $REGION aws cloudwatch put-metric-data --metric-name l2_current_block --namespace CWAgent --value $L2_CLIENT_CURRENT --timestamp $TIMESTAMP --dimensions InstanceId=$INSTANCE_ID --region $REGION -aws cloudwatch put-metric-data --metric-name l2_blocks_behind --namespace CWAgent --value $L2_CLIENT_BLOCKS_BEHIND --timestamp $TIMESTAMP --dimensions InstanceId=$INSTANCE_ID --region $REGION +aws cloudwatch put-metric-data --metric-name l2_minutes_behind --namespace CWAgent --value $L2_CLIENT_CURRENT_BLOCK_MINUTES_BEHIND --timestamp $TIMESTAMP --dimensions InstanceId=$INSTANCE_ID --region $REGION diff --git a/lib/base/lib/assets/user-data/node.sh b/lib/base/lib/assets/user-data/node.sh index c26bff4f..7086a1e4 100644 --- a/lib/base/lib/assets/user-data/node.sh +++ b/lib/base/lib/assets/user-data/node.sh @@ -150,7 +150,7 @@ case $NETWORK_ID in sed -i "s#OP_NODE_L1_ETH_RPC=https://1rpc.io/eth#OP_NODE_L1_ETH_RPC=$L1_EXECUTION_ENDPOINT#g" /home/bcuser/node/.env.mainnet sed -i '/.env.mainnet/s/^#//g' /home/bcuser/node/docker-compose.yml sed -i '/OP_NODE_L1_BEACON/s/^#//g' /home/bcuser/node/.env.mainnet - sed -i "s#OP_NODE_L1_BEACON=https://your.mainet.beacon.node/endpoint-here#OP_NODE_L1_BEACON=$L1_CONSENSUS_ENDPOINT#g" /home/bcuser/node/.env.mainnet + sed -i "s#OP_NODE_L1_BEACON=https://your.mainnet.beacon.node/endpoint-here#OP_NODE_L1_BEACON=$L1_CONSENSUS_ENDPOINT#g" /home/bcuser/node/.env.mainnet ;; "sepolia") sed -i "s#OP_NODE_L1_ETH_RPC=https://rpc.sepolia.org#OP_NODE_L1_ETH_RPC=$L1_EXECUTION_ENDPOINT#g" /home/bcuser/node/.env.sepolia From 4794daf02116e19f09a1caf3391d91d58a696d10 Mon Sep 17 00:00:00 2001 From: Nikolay Vlasov Date: Mon, 8 Apr 2024 14:29:40 +1000 Subject: [PATCH 44/75] Base. Updates after e2e test --- lib/base/README.md | 26 +++++++++---------- .../constructs/base-node-security-group.ts | 5 ++++ 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/lib/base/README.md b/lib/base/README.md index 3a083a3b..3cc8713c 100644 --- a/lib/base/README.md +++ b/lib/base/README.md @@ -54,15 +54,15 @@ This is the Well-Architected checklist for Ethereum nodes implementation of the ## Hardware Requirements -**Minimum for Base node** +**Minimum for Base node sepolia** - Instance type [m7g.2xlarge](https://aws.amazon.com/ec2/instance-types/m7g/). -- 2500GB EBS gp3 storage with at least 5000 IOPS. +- 1500GB EBS gp3 storage with at least 5000 IOPS. -**Recommended for Base node on maiinet** +**Recommended for Base node on mainnet** -- Instance type [m7g.4xlarge](https://aws.amazon.com/ec2/instance-types/m7g/). -- 4100GB EBS gp3 storage with at least 6000 IOPS.` +- Instance type [m7g.2xlarge](https://aws.amazon.com/ec2/instance-types/m7g/). +- 4100GB EBS gp3 storage with at least 5000 IOPS.` @@ -124,7 +124,7 @@ npx cdk deploy base-common ### From your Cloud9: Deploy Single Node -1. For L1 node you you can set your own URLs in `BASE_L1_EXECUTION_ENDPOINT` and `BASE_L1_CONSENSUS_ENDPOINT` properties of `.env` file. It can be one of [the providers recommended by Base](https://docs.base.org/tools/node-providers) or you can run your own Ethereum node [with Node Runner blueprint](https://aws-samples.github.io/aws-blockchain-node-runners/docs/Blueprints/Ethereum). For example: +1. For L1 node you you can set your own URLs in `BASE_L1_EXECUTION_ENDPOINT` and `BASE_L1_CONSENSUS_ENDPOINT` properties of `.env` file. It can be one of [the providers recommended by Base](https://docs.base.org/tools/node-providers) or you can run your own Ethereum node [with Node Runner Ethereum blueprint](https://aws-samples.github.io/aws-blockchain-node-runners/docs/Blueprints/Ethereum) (tested with geth-lighthouse combination). For example: ```bash #For Sepolia: @@ -132,14 +132,14 @@ BASE_L1_EXECUTION_ENDPOINT="https://ethereum-sepolia-rpc.publicnode.com" BASE_L1_CONSENSUS_ENDPOINT="https://ethereum-sepolia-beacon-api.publicnode.com" ``` -2. Deploy Base RPC Node and wait for it to sync. For Mainnet it might take less than an hour when using snapshots (default) or multiple days if syncing from block 0. +2. Deploy Base RPC Node and wait for it to sync. For Mainnet it might a day when using snapshots or about a week if syncing from block 0. You can use snapshots provided by the Base team by setting `BASE_RESTORE_FROM_SNAPSHOT="true"` in `.env` file. ```bash pwd # Make sure you are in aws-blockchain-node-runners/lib/base npx cdk deploy base-single-node --json --outputs-file single-node-deploy.json ``` -A node connected to Sepolia network should start within an hour, while syncing nodes with Mainnet might take a while. Although you can use snapshots provided by the Base team by setting `BASE_RESTORE_FROM_SNAPSHOT="true"` in `.env` file, you might still have wait for your node to finish synchronizing. You can watch the progress with CloudWatch dashboard (see [Monitoring](#monitoring)) or check the progress manually. For manual access, use SSM to connect into EC2 first and watch the log like this: +After deployment you can watch the progress with CloudWatch dashboard (see [Monitoring](#monitoring)) or check the progress manually. For manual access, use SSM to connect into EC2 first and watch the log like this: ```bash export INSTANCE_ID=$(cat single-node-deploy.json | jq -r '..|.node-instance-id? | select(. != null)') @@ -164,7 +164,7 @@ curl -s -X POST -H "Content-Type: application/json" --data '{"jsonrpc":"2.0","me ``` ### Monitoring -A script on the Base node publishes current block and blocks behind metrics to CloudWatch metrics every 5 minutes. When the node is fully synced the blocks behind metric should get to 0, which might take about 1.5 days. To see the metrics: +Every 5 minutes a script on the Base node publishes to CloudWatch service the metrics for current block for L1/L2 clients as well as blocks behind metric for L1 and minutes buehind for L2. When the node is fully synced the blocks behind metric should get to 4 and minutes behind should get down to 0. To see the metrics: - Navigate to CloudWatch service (make sure you are in the region you have specified for AWS_REGION) - Open Dashboards and select `base-single-node--` from the list of dashboards. @@ -194,13 +194,13 @@ npx cdk destroy base-common 1. How to check the logs of the clients running on my Base node? - **Note:** In this tutorial we chose not to use SSH and use Session Manager instead. That allows you to log all sessions in AWS CloudTrail to see who logged into the server and when. If you receive an error similar to `SessionManagerPlugin is not found`, [install Session Manager plugin for AWS CLI](https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-working-with-install-plugin.html) + **Note:** In this tutorial we chose not to use SSH and use Session Manager instead. That allows you to log all sessions in AWS CloudTrail to see who logged into the server and when. If you receive an error saying `SessionManagerPlugin is not found`, [install Session Manager plugin for AWS CLI](https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-working-with-install-plugin.html) ```bash pwd # Make sure you are in aws-blockchain-node-runners/lib/base -export INSTANCE_ID=$(cat single-node-deploy.json | jq -r '..|.node-instance-id? | select(. != null)') +export INSTANCE_ID=$(cat single-node-deploy.json | jq -r '..|.nodeinstanceid? | select(. != null)') echo "INSTANCE_ID=" $INSTANCE_ID export AWS_REGION=us-east-1 aws ssm start-session --target $INSTANCE_ID --region $AWS_REGION @@ -216,7 +216,7 @@ docker logs --tail 50 node_node_1 -f pwd # Make sure you are in aws-blockchain-node-runners/lib/base -export INSTANCE_ID=$(cat single-node-deploy.json | jq -r '..|.node-instance-id? | select(. != null)') +export INSTANCE_ID=$(cat single-node-deploy.json | jq -r '..|.nodeinstanceid? | select(. != null)') echo "INSTANCE_ID=" $INSTANCE_ID export AWS_REGION=us-east-1 aws ssm start-session --target $INSTANCE_ID --region $AWS_REGION @@ -226,7 +226,7 @@ sudo cat /var/log/cloud-init-output.log 3. How can I restart the Base node? ``` bash -export INSTANCE_ID=$(cat single-node-deploy.json | jq -r '..|.node-instance-id? | select(. != null)') +export INSTANCE_ID=$(cat single-node-deploy.json | jq -r '..|.nodeinstanceid? | select(. != null)') echo "INSTANCE_ID=" $INSTANCE_ID export AWS_REGION=us-east-1 aws ssm start-session --target $INSTANCE_ID --region $AWS_REGION diff --git a/lib/base/lib/constructs/base-node-security-group.ts b/lib/base/lib/constructs/base-node-security-group.ts index 41e43bb1..cbe14aec 100644 --- a/lib/base/lib/constructs/base-node-security-group.ts +++ b/lib/base/lib/constructs/base-node-security-group.ts @@ -29,6 +29,11 @@ export interface BaseNodeSecurityGroupConstructProps { // Private port sg.addIngressRule(ec2.Peer.ipv4(vpc.vpcCidrBlock), ec2.Port.tcp(8545), "Base Client RPC"); + sg.addEgressRule(ec2.Peer.anyIpv4(), ec2.Port.tcpRange(0, 12999), "All outbound connections except 13000"); + sg.addEgressRule(ec2.Peer.anyIpv4(), ec2.Port.tcpRange(13001, 65535), "All outbound connections except 13000"); + sg.addEgressRule(ec2.Peer.anyIpv4(), ec2.Port.udpRange(0, 12999), "All outbound connections except 13000"); + sg.addEgressRule(ec2.Peer.anyIpv4(), ec2.Port.udpRange(13001, 65535), "All outbound connections except 13000"); + this.securityGroup = sg } } From aa3c8bc955ed228a0d444852d46ce9df4c07dd06 Mon Sep 17 00:00:00 2001 From: Nikolay Vlasov Date: Mon, 8 Apr 2024 14:40:53 +1000 Subject: [PATCH 45/75] Base. Fixed tests after security group changes --- .../constructs/base-node-security-group.ts | 2 +- lib/base/test/base-single-node.test.ts | 29 +++++++++++++++++-- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/lib/base/lib/constructs/base-node-security-group.ts b/lib/base/lib/constructs/base-node-security-group.ts index cbe14aec..ac71fccb 100644 --- a/lib/base/lib/constructs/base-node-security-group.ts +++ b/lib/base/lib/constructs/base-node-security-group.ts @@ -19,7 +19,7 @@ export interface BaseNodeSecurityGroupConstructProps { const sg = new ec2.SecurityGroup(this, `rpc-node-security-group`, { vpc, description: "Security Group for Blockchain nodes", - allowAllOutbound: true, + allowAllOutbound: false, }); // Public ports diff --git a/lib/base/test/base-single-node.test.ts b/lib/base/test/base-single-node.test.ts index ffdb7c94..e945f320 100644 --- a/lib/base/test/base-single-node.test.ts +++ b/lib/base/test/base-single-node.test.ts @@ -38,9 +38,32 @@ describe("BaseSingleNodeStack", () => { SecurityGroupEgress: [ { "CidrIp": "0.0.0.0/0", - "Description": "Allow all outbound traffic by default", - "IpProtocol": "-1" - } + "Description": "All outbound connections except 13000", + "FromPort": 0, + "IpProtocol": "tcp", + "ToPort": 12999 + }, + { + "CidrIp": "0.0.0.0/0", + "Description": "All outbound connections except 13000", + "FromPort": 13001, + "IpProtocol": "tcp", + "ToPort": 65535 + }, + { + "CidrIp": "0.0.0.0/0", + "Description": "All outbound connections except 13000", + "FromPort": 0, + "IpProtocol": "udp", + "ToPort": 12999 + }, + { + "CidrIp": "0.0.0.0/0", + "Description": "All outbound connections except 13000", + "FromPort": 13001, + "IpProtocol": "udp", + "ToPort": 65535 + } ], SecurityGroupIngress: [ { From 01f9490522018e2e7441a3cd2be5b00e0edc9ab0 Mon Sep 17 00:00:00 2001 From: Nikolay Vlasov Date: Mon, 8 Apr 2024 14:45:23 +1000 Subject: [PATCH 46/75] Base. Added README to the website --- website/docs/Blueprints/Base.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 website/docs/Blueprints/Base.md diff --git a/website/docs/Blueprints/Base.md b/website/docs/Blueprints/Base.md new file mode 100644 index 00000000..a7999de3 --- /dev/null +++ b/website/docs/Blueprints/Base.md @@ -0,0 +1,9 @@ +--- +sidebar_position: 6 +sidebar_label: Base +--- +# + +import Readme from '../../../lib/Base/README.md'; + + From 9ed7de61f32e4ef9198fe3b2b130d1e8fb4833d4 Mon Sep 17 00:00:00 2001 From: Nikolay Vlasov Date: Mon, 8 Apr 2024 15:04:03 +1000 Subject: [PATCH 47/75] Base. Updates after running code scanning tools --- .pre-commit-config.yaml | 4 ++++ lib/base/README.md | 2 +- lib/base/lib/assets/restore-from-snapshot.sh | 2 +- lib/base/lib/assets/sync-checker/syncchecker-base.sh | 5 ++--- lib/base/sample-configs/.env-sample-rpc | 4 ++-- 5 files changed, 10 insertions(+), 7 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e64ba280..cf3c4123 100755 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,3 +11,7 @@ repos: - id: detect-aws-credentials args: ['--allow-missing-credentials'] - id: forbid-submodules + - repo: https://github.com/iamthefij/docker-pre-commit + rev: master + hooks: + - id: docker-compose-check diff --git a/lib/base/README.md b/lib/base/README.md index 3cc8713c..7e9161a0 100644 --- a/lib/base/README.md +++ b/lib/base/README.md @@ -14,7 +14,7 @@ 1. A Base node deployed in the [Default VPC](https://docs.aws.amazon.com/vpc/latest/userguide/default-vpc.html) continuously synchronizes with the rest of nodes on Base blockchain network through [Internet Gateway](https://docs.aws.amazon.com/vpc/latest/userguide/VPC_Internet_Gateway.html). 2. The Base node is used by dApps or development tools internally from within the Default VPC. JSON RPC API is not exposed to the Internet directly to protect nodes from unauthorized access. -3. Your Base node needs access to a fully-synced [Ethereum Mainnet or Sepolia RPC endpoint](https://docs.base.org/tools/node-providers) . +3. Your Base node needs access to a fully-synced [Ethereum Mainnet or Sepolia RPC endpoint](https://docs.base.org/tools/node-providers) . 4. The Base node sends various monitoring metrics for both EC2 and Base nodes to Amazon CloudWatch. ## Additional materials diff --git a/lib/base/lib/assets/restore-from-snapshot.sh b/lib/base/lib/assets/restore-from-snapshot.sh index 0d9b14be..488826f1 100644 --- a/lib/base/lib/assets/restore-from-snapshot.sh +++ b/lib/base/lib/assets/restore-from-snapshot.sh @@ -18,4 +18,4 @@ chown -R bcuser:bcuser /data && \ echo "Sync finished at " $(date) && \ echo "$(($SECONDS / 60)) minutes and $(($SECONDS % 60)) seconds elapsed." && \ sudo su bcuser && \ -/usr/local/bin/docker-compose -f /home/bcuser/node/docker-compose.yml up -d \ No newline at end of file +/usr/local/bin/docker-compose -f /home/bcuser/node/docker-compose.yml up -d diff --git a/lib/base/lib/assets/sync-checker/syncchecker-base.sh b/lib/base/lib/assets/sync-checker/syncchecker-base.sh index b7c4aff4..6be5ca59 100644 --- a/lib/base/lib/assets/sync-checker/syncchecker-base.sh +++ b/lib/base/lib/assets/sync-checker/syncchecker-base.sh @@ -7,7 +7,7 @@ OPTIMISM_SYNC_STATUS=$(curl -s -X POST -H "Content-Type: application/json" --dat L1_CLIENT_HEAD=$(echo $OPTIMISM_SYNC_STATUS | jq -r ".head_l1.number") L1_CLIENT_CURRENT=$(echo $OPTIMISM_SYNC_STATUS | jq -r ".current_l1.number") -if [ $L1_CLIENT_HEAD -eq 0 ]; then +if [ $L1_CLIENT_HEAD -eq 0 ]; then L1_CLIENT_BLOCKS_BEHIND=0 else L1_CLIENT_BLOCKS_BEHIND="$((L1_CLIENT_HEAD-L1_CLIENT_CURRENT))" @@ -18,7 +18,7 @@ L2_CLIENT_CURRENT=$(echo $OPTIMISM_SYNC_STATUS | jq -r ".unsafe_l2.number") L2_CLIENT_CURRENT_BLOCK_TIMESTAMP=$(echo $OPTIMISM_SYNC_STATUS | jq -r ".unsafe_l2.timestamp") L2_CLIENT_CURRENT_BLOCK_MINUTES_BEHIND="$((($(date +%s) - L2_CLIENT_CURRENT_BLOCK_TIMESTAMP)/60))" -if [ "$L2_CLIENT_CURRENT" == "null" ]; then +if [ "$L2_CLIENT_CURRENT" == "null" ]; then L2_CLIENT_CURRENT=0 fi @@ -41,4 +41,3 @@ aws cloudwatch put-metric-data --metric-name l1_blocks_behind --namespace CWAgen aws cloudwatch put-metric-data --metric-name l2_current_block --namespace CWAgent --value $L2_CLIENT_CURRENT --timestamp $TIMESTAMP --dimensions InstanceId=$INSTANCE_ID --region $REGION aws cloudwatch put-metric-data --metric-name l2_minutes_behind --namespace CWAgent --value $L2_CLIENT_CURRENT_BLOCK_MINUTES_BEHIND --timestamp $TIMESTAMP --dimensions InstanceId=$INSTANCE_ID --region $REGION - diff --git a/lib/base/sample-configs/.env-sample-rpc b/lib/base/sample-configs/.env-sample-rpc index e0960664..3aed121e 100644 --- a/lib/base/sample-configs/.env-sample-rpc +++ b/lib/base/sample-configs/.env-sample-rpc @@ -4,7 +4,7 @@ ## Set the AWS account is and region for your environment ## AWS_ACCOUNT_ID="xxxxxxxx" -AWS_REGION="us-east-1" +AWS_REGION="us-east-1" ## Common configuration parameters ## BASE_NETWORK_ID="sepolia" # All options: "mainnet", "sepolia" @@ -25,4 +25,4 @@ BASE_L1_CONSENSUS_ENDPOINT="https://ethereum-sepolia-beacon-api.publicnode.com" #BASE_L1_CONSENSUS_ENDPOINT=https://ethereum-sepolia-beacon-api.publicnode.com # Example for Mainnet and with Ethereum Blueprint with Geth-Lighthouse client combination and private IP: #BASE_L1_EXECUTION_ENDPOINT=http://172.31.15.220:8545 -#BASE_L1_CONSENSUS_ENDPOINT=http://172.31.15.220:5052 \ No newline at end of file +#BASE_L1_CONSENSUS_ENDPOINT=http://172.31.15.220:5052 From e597f688bc62ecf0a9399f92935b2f82c97d8120 Mon Sep 17 00:00:00 2001 From: Nikolay Vlasov Date: Mon, 15 Apr 2024 12:08:41 +1000 Subject: [PATCH 48/75] Base. Fixed the website --- website/docs/Blueprints/Base.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/Blueprints/Base.md b/website/docs/Blueprints/Base.md index a7999de3..833b66c4 100644 --- a/website/docs/Blueprints/Base.md +++ b/website/docs/Blueprints/Base.md @@ -4,6 +4,6 @@ sidebar_label: Base --- # -import Readme from '../../../lib/Base/README.md'; +import Readme from '../../../lib/base/README.md'; From 31472b3d80ce286906b78bc81535d67ccd7e0824 Mon Sep 17 00:00:00 2001 From: Nikolay Vlasov Date: Mon, 6 May 2024 15:28:25 +1000 Subject: [PATCH 49/75] Base. Debugging deployments and snapshot download --- lib/base/app.ts | 4 +- lib/base/lib/assets/restore-from-snapshot.sh | 21 ----- lib/base/lib/assets/start-from-snapshot.sh | 59 +++++++++++++ lib/base/lib/assets/user-data/node.sh | 87 ++++++++++++++------ lib/base/lib/config/baseConfig.interface.ts | 6 +- lib/base/lib/config/baseConfig.ts | 2 + lib/base/lib/single-node-stack.ts | 5 ++ lib/base/sample-configs/.env-sample-rpc | 3 + 8 files changed, 138 insertions(+), 49 deletions(-) delete mode 100644 lib/base/lib/assets/restore-from-snapshot.sh create mode 100644 lib/base/lib/assets/start-from-snapshot.sh diff --git a/lib/base/app.ts b/lib/base/app.ts index eb61a5cd..94b55bba 100644 --- a/lib/base/app.ts +++ b/lib/base/app.ts @@ -15,14 +15,16 @@ new BaseCommonStack(app, "base-common", { }); new BaseSingleNodeStack(app, "base-single-node", { - stackName: `base-single-node-${config.baseNodeConfig.baseNetworkId}`, + stackName: `base-single-node-${config.baseNodeConfig.baseNodeConfiguration}-${config.baseNodeConfig.baseNetworkId}`, env: { account: config.baseConfig.accountId, region: config.baseConfig.region }, instanceType: config.baseNodeConfig.instanceType, instanceCpuType: config.baseNodeConfig.instanceCpuType, baseNetworkId: config.baseNodeConfig.baseNetworkId, + baseNodeConfiguration: config.baseNodeConfig.baseNodeConfiguration, restoreFromSnapshot: config.baseNodeConfig.restoreFromSnapshot, l1ExecutionEndpoint: config.baseNodeConfig.l1ExecutionEndpoint, l1ConsensusEndpoint: config.baseNodeConfig.l1ConsensusEndpoint, + snapshotUrl: config.baseNodeConfig.snapshotUrl, dataVolume: config.baseNodeConfig.dataVolume, }); diff --git a/lib/base/lib/assets/restore-from-snapshot.sh b/lib/base/lib/assets/restore-from-snapshot.sh deleted file mode 100644 index 488826f1..00000000 --- a/lib/base/lib/assets/restore-from-snapshot.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/bin/bash - -source /etc/environment -TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600") -INSTANCE_ID=$(curl -H "X-aws-ec2-metadata-token: $TOKEN" -s http://169.254.169.254/latest/meta-data/instance-id) -LATEST_SNAPSHOT_FILE_NAME=$(curl https://base-snapshots-$NETWORK_ID-archive.s3.amazonaws.com/latest) - -echo "Sync started at " $(date) -SECONDS=0 - -s5cmd --log error cp s3://base-snapshots-$NETWORK_ID-archive/$LATEST_SNAPSHOT_FILE_NAME /data && \ -tar -I zstdmt -xf /data/$LATEST_SNAPSHOT_FILE_NAME -C /data && \ -mv /data/snapshots/$NETWORK_ID/download/* /data && \ -rm -rf /data/snapshots && \ -rm -rf /data/$LATEST_SNAPSHOT_FILE_NAME - -chown -R bcuser:bcuser /data && \ -echo "Sync finished at " $(date) && \ -echo "$(($SECONDS / 60)) minutes and $(($SECONDS % 60)) seconds elapsed." && \ -sudo su bcuser && \ -/usr/local/bin/docker-compose -f /home/bcuser/node/docker-compose.yml up -d diff --git a/lib/base/lib/assets/start-from-snapshot.sh b/lib/base/lib/assets/start-from-snapshot.sh new file mode 100644 index 00000000..8583cc6b --- /dev/null +++ b/lib/base/lib/assets/start-from-snapshot.sh @@ -0,0 +1,59 @@ +#!/bin/bash + +source /etc/environment +echo "Downloading snpashot" + +cd /data + +BASE_SNAPSHOT_FILE_NAME=snalshot.tar.gz +BASE_SNAPSHOT_DIR=/data/ +BASE_SNAPSHOT_DOWNLOAD_STATUS=-1 +BASE_LATEST_SNAPSHOT_FILE_NAME=$(curl https://$NETWORK_ID-$NODE_CONFIG-snapshots.base.org/latest) + +if [ "$SNAPSHOT_URL" == "none" ] || [ -z "${SNAPSHOT_URL}" ]; then + SNAPSHOT_URL=https://$NETWORK_ID-$NODE_CONFIG-snapshots.base.org/$BASE_LATEST_SNAPSHOT_FILE_NAME +fi + +while (( BASE_SNAPSHOT_DOWNLOAD_STATUS != 0 )) +do + PIDS=$(pgrep aria2c) + if [ -z "$PIDS" ]; then + aria2c -x3 $SNAPSHOT_URL -d $BASE_SNAPSHOT_DIR -o $BASE_SNAPSHOT_FILE_NAME -l aria2c.log --log-level=warn --allow-piece-length-change=true + fi + BASE_SNAPSHOT_DOWNLOAD_STATUS=$? + pid=$(pidof aria2c) + wait $pid + echo "aria2c exit." + case $BASE_SNAPSHOT_DOWNLOAD_STATUS in + 3) + echo "file not exist." + exit 3 + ;; + 9) + echo "No space left on device." + exit 9 + ;; + *) + continue + ;; + esac +done + +echo "Downloading snapshot succeed" + +sleep 60 +# take about 2 hours to decompress the snapshot +echo "Decompression snapshot start ..." + +tar -I zstdmt -xf $BASE_SNAPSHOT_DIR/$BASE_SNAPSHOT_FILE_NAME -C /data 2>&1 | tee unzip.log && echo "decompression success..." || echo "decompression failed..." >> snapshots-decompression.log +echo "Decompressing snapshot success ..." + +mv /data/snapshots/$NETWORK_ID/download/* /data && \ +rm -rf /data/snapshots && \ +rm -rf /data/$BASE_SNAPSHOT_FILE_NAME + +echo "Snapshot is ready !!!" + +chown -R bcuser:bcuser /data && \ +sudo su bcuser && \ +/usr/local/bin/docker-compose -f /home/bcuser/node/docker-compose.yml up -d diff --git a/lib/base/lib/assets/user-data/node.sh b/lib/base/lib/assets/user-data/node.sh index 7086a1e4..0d2ef2ba 100644 --- a/lib/base/lib/assets/user-data/node.sh +++ b/lib/base/lib/assets/user-data/node.sh @@ -6,9 +6,11 @@ LIFECYCLE_HOOK_NAME=${_LIFECYCLE_HOOK_NAME_} AUTOSCALING_GROUP_NAME=${_AUTOSCALING_GROUP_NAME_} RESOURCE_ID=${_NODE_CF_LOGICAL_ID_} ASSETS_S3_PATH=${_ASSETS_S3_PATH_} -echo "LIFECYCLE_HOOK_NAME=$LIFECYCLE_HOOK_NAME" >> /etc/environment -echo "AUTOSCALING_GROUP_NAME=$AUTOSCALING_GROUP_NAME" >> /etc/environment -echo "ASSETS_S3_PATH=$ASSETS_S3_PATH" >> /etc/environment +{ + echo "LIFECYCLE_HOOK_NAME=$LIFECYCLE_HOOK_NAME" + echo "AUTOSCALING_GROUP_NAME=$AUTOSCALING_GROUP_NAME" + echo "ASSETS_S3_PATH=$ASSETS_S3_PATH" +} >> /etc/environment arch=$(uname -m) @@ -55,19 +57,22 @@ unzip -q awscliv2.zip rm /usr/bin/aws ln /usr/local/bin/aws /usr/bin/aws -aws configure set default.s3.max_concurrent_requests 50 -aws configure set default.s3.multipart_chunksize 256MB - echo 'Installing SSM Agent' yum install -y $SSM_AGENT_BINARY_URI -echo "Installing s5cmd" -cd /opt -wget -q $S5CMD_URI -O s5cmd.tar.gz -tar -xf s5cmd.tar.gz -chmod +x s5cmd -mv s5cmd /usr/bin -s5cmd version +# install aria2 a p2p downloader + +if [ "$arch" == "x86_64" ]; then + wget https://github.com/q3aql/aria2-static-builds/releases/download/v1.36.0/aria2-1.36.0-linux-gnu-64bit-build1.tar.bz2 + tar jxvf aria2-1.36.0-linux-gnu-64bit-build1.tar.bz2 + cd aria2-1.36.0-linux-gnu-64bit-build1/ + make install +else + wget https://github.com/q3aql/aria2-static-builds/releases/download/v1.36.0/aria2-1.36.0-linux-gnu-arm-rbpi-build1.tar.bz2 + tar jxvf aria2-1.36.0-linux-gnu-arm-rbpi-build1.tar.bz2 + cd aria2-1.36.0-linux-gnu-arm-rbpi-build1/ + make install +fi # Base specific setup starts here @@ -77,13 +82,19 @@ STACK_NAME=${_STACK_NAME_} RESTORE_FROM_SNAPSHOT=${_RESTORE_FROM_SNAPSHOT_} FORMAT_DISK=${_FORMAT_DISK_} NETWORK_ID=${_NETWORK_ID_} +NODE_CONFIG=${_NODE_CONFIG_} L1_EXECUTION_ENDPOINT=${_L1_EXECUTION_ENDPOINT_} L1_CONSENSUS_ENDPOINT=${_L1_CONSENSUS_ENDPOINT_} +SNAPSHOT_URL=${_SNAPSHOT_URL_} -echo "REGION=$REGION" >> /etc/environment -echo "NETWORK_ID=$NETWORK_ID" >> /etc/environment -echo "L1_EXECUTION_ENDPOINT=$L1_EXECUTION_ENDPOINT" >> /etc/environment -echo "L1_CONSENSUS_ENDPOINT=$L1_CONSENSUS_ENDPOINT" >> /etc/environment +{ + echo "REGION=$REGION" + echo "NETWORK_ID=$NETWORK_ID" + echo "NODE_CONFIG=$NODE_CONFIG" + echo "L1_EXECUTION_ENDPOINT=$L1_EXECUTION_ENDPOINT" + echo "L1_CONSENSUS_ENDPOINT=$L1_CONSENSUS_ENDPOINT" + echo "SNAPSHOT_URL=$SNAPSHOT_URL" +} >> /etc/environment GIT_URL=https://github.com/base-org/node.git SYNC_CHECKER_FILE_NAME=syncchecker-base.sh @@ -147,16 +158,42 @@ echo "Configuring node" case $NETWORK_ID in "mainnet") - sed -i "s#OP_NODE_L1_ETH_RPC=https://1rpc.io/eth#OP_NODE_L1_ETH_RPC=$L1_EXECUTION_ENDPOINT#g" /home/bcuser/node/.env.mainnet + OP_CONFIG_FILE_PATH=/home/bcuser/node/.env.mainnet + ;; + "sepolia") + OP_CONFIG_FILE_PATH=/home/bcuser/node/.env.sepolia + ;; + *) + echo "Network id is not valid." + exit 1 + ;; +esac + +case $NODE_CONFIG in + "full") + echo "OP_GETH_GCMODE=full" >> $OP_CONFIG_FILE_PATH + ;; + "archive") + echo "OP_GETH_GCMODE=archive" >> $OP_CONFIG_FILE_PATH + ;; + *) + echo "Network id is not valid." + exit 1 + ;; +esac + +case $NETWORK_ID in + "mainnet") + sed -i "s#OP_NODE_L1_ETH_RPC=https://1rpc.io/eth#OP_NODE_L1_ETH_RPC=$L1_EXECUTION_ENDPOINT#g" $OP_CONFIG_FILE_PATH sed -i '/.env.mainnet/s/^#//g' /home/bcuser/node/docker-compose.yml - sed -i '/OP_NODE_L1_BEACON/s/^#//g' /home/bcuser/node/.env.mainnet - sed -i "s#OP_NODE_L1_BEACON=https://your.mainnet.beacon.node/endpoint-here#OP_NODE_L1_BEACON=$L1_CONSENSUS_ENDPOINT#g" /home/bcuser/node/.env.mainnet + sed -i '/OP_NODE_L1_BEACON/s/^#//g' $OP_CONFIG_FILE_PATH + sed -i "s#OP_NODE_L1_BEACON=https://your.mainnet.beacon.node/endpoint-here#OP_NODE_L1_BEACON=$L1_CONSENSUS_ENDPOINT#g" $OP_CONFIG_FILE_PATH ;; "sepolia") - sed -i "s#OP_NODE_L1_ETH_RPC=https://rpc.sepolia.org#OP_NODE_L1_ETH_RPC=$L1_EXECUTION_ENDPOINT#g" /home/bcuser/node/.env.sepolia + sed -i "s#OP_NODE_L1_ETH_RPC=https://rpc.sepolia.org#OP_NODE_L1_ETH_RPC=$L1_EXECUTION_ENDPOINT#g" $OP_CONFIG_FILE_PATH sed -i "/.env.sepolia/s/^#//g" /home/bcuser/node/docker-compose.yml - sed -i '/OP_NODE_L1_BEACON/s/^#//g' /home/bcuser/node/.env.sepolia - sed -i "s#OP_NODE_L1_BEACON=https://your.sepolia.beacon.node/endpoint-here#OP_NODE_L1_BEACON=$L1_CONSENSUS_ENDPOINT#g" /home/bcuser/node/.env.sepolia + sed -i '/OP_NODE_L1_BEACON/s/^#//g' $OP_CONFIG_FILE_PATH + sed -i "s#OP_NODE_L1_BEACON=https://your.sepolia.beacon.node/endpoint-here#OP_NODE_L1_BEACON=$L1_CONSENSUS_ENDPOINT#g" $OP_CONFIG_FILE_PATH ;; *) echo "Network id is not valid." @@ -217,8 +254,8 @@ if [ "$RESTORE_FROM_SNAPSHOT" == "false" ]; then echo "sudo su bcuser && /usr/local/bin/docker-compose -f /home/bcuser/node/docker-compose.yml up -d" | at now +3 minutes else echo "Restoring data from snapshot" - chmod 766 /opt/restore-from-snapshot.sh - echo "/opt/restore-from-snapshot.sh" | at now +3 minutes + chmod 766 /opt/start-from-snapshot.sh + echo "/opt/start-from-snapshot.sh" | at now +3 minutes fi echo "All Done!!" diff --git a/lib/base/lib/config/baseConfig.interface.ts b/lib/base/lib/config/baseConfig.interface.ts index 24539158..e7e327a2 100644 --- a/lib/base/lib/config/baseConfig.interface.ts +++ b/lib/base/lib/config/baseConfig.interface.ts @@ -1,7 +1,7 @@ import * as configTypes from "../../../constructs/config.interface"; -export type BaseNetworkId = "mainnet" ; -export type BaseNodeConfiguration = "full" ; +export type BaseNetworkId = "mainnet" | "sepolia"; +export type BaseNodeConfiguration = "full" | "archive"; export {AMBEthereumNodeNetworkId} from "../../../constructs/config.interface"; @@ -16,8 +16,10 @@ export interface BaseBaseConfig extends configTypes.BaseConfig { export interface BaseBaseNodeConfig extends configTypes.BaseNodeConfig { baseNetworkId: BaseNetworkId; + baseNodeConfiguration: BaseNodeConfiguration; dataVolume: BaseDataVolumeConfig; restoreFromSnapshot: boolean; l1ExecutionEndpoint: string; l1ConsensusEndpoint: string; + snapshotUrl: string; } diff --git a/lib/base/lib/config/baseConfig.ts b/lib/base/lib/config/baseConfig.ts index 9bfd345e..4da2e404 100644 --- a/lib/base/lib/config/baseConfig.ts +++ b/lib/base/lib/config/baseConfig.ts @@ -27,9 +27,11 @@ export const baseNodeConfig: configTypes.BaseBaseNodeConfig = { instanceType: new ec2.InstanceType(process.env.BASE_INSTANCE_TYPE ? process.env.BASE_INSTANCE_TYPE : "m7g.2xlarge"), instanceCpuType: process.env.BASE_CPU_TYPE?.toLowerCase() == "x86_64" ? ec2.AmazonLinuxCpuType.X86_64 : ec2.AmazonLinuxCpuType.ARM_64, baseNetworkId: process.env.BASE_NETWORK_ID || "mainnet", + baseNodeConfiguration: process.env.BASE_NODE_CONFIGURATION || "full", restoreFromSnapshot: process.env.BASE_RESTORE_FROM_SNAPSHOT?.toLowerCase() == "true" ? true : false, l1ExecutionEndpoint: process.env.BASE_L1_EXECUTION_ENDPOINT || constants.NoneValue, l1ConsensusEndpoint: process.env.BASE_L1_CONSENSUS_ENDPOINT || constants.NoneValue, + snapshotUrl: process.env.BASE_SNAPSHOT_URL || constants.NoneValue, dataVolume: { sizeGiB: process.env.BASE_DATA_VOL_SIZE ? parseInt(process.env.BASE_DATA_VOL_SIZE): 1000, type: parseDataVolumeType(process.env.BASE_DATA_VOL_TYPE?.toLowerCase() ? process.env.BASE_DATA_VOL_TYPE?.toLowerCase() : "gp3"), diff --git a/lib/base/lib/single-node-stack.ts b/lib/base/lib/single-node-stack.ts index b32fa3cc..f47e7ac4 100644 --- a/lib/base/lib/single-node-stack.ts +++ b/lib/base/lib/single-node-stack.ts @@ -17,9 +17,11 @@ export interface BaseSingleNodeStackProps extends cdk.StackProps { instanceType: ec2.InstanceType; instanceCpuType: ec2.AmazonLinuxCpuType; baseNetworkId: configTypes.BaseNetworkId; + baseNodeConfiguration: configTypes.BaseNodeConfiguration; restoreFromSnapshot: boolean; l1ExecutionEndpoint: string, l1ConsensusEndpoint: string, + snapshotUrl: string, dataVolume: configTypes.BaseDataVolumeConfig; } @@ -39,6 +41,7 @@ export class BaseSingleNodeStack extends cdk.Stack { instanceType, instanceCpuType, baseNetworkId, + baseNodeConfiguration, restoreFromSnapshot, l1ExecutionEndpoint, l1ConsensusEndpoint, @@ -103,12 +106,14 @@ export class BaseSingleNodeStack extends cdk.Stack { _DATA_VOLUME_TYPE_: dataVolume.type, _DATA_VOLUME_SIZE_: dataVolumeSizeBytes.toString(), _NETWORK_ID_: baseNetworkId, + _NODE_CONFIG_: baseNodeConfiguration, _LIFECYCLE_HOOK_NAME_: constants.NoneValue, _AUTOSCALING_GROUP_NAME_: constants.NoneValue, _RESTORE_FROM_SNAPSHOT_: restoreFromSnapshot.toString(), _FORMAT_DISK_: "true", _L1_EXECUTION_ENDPOINT_: l1ExecutionEndpoint, _L1_CONSENSUS_ENDPOINT_: l1ConsensusEndpoint, + _SNAPSHOT_URL_: props.snapshotUrl, }); node.instance.addUserData(modifiedInitNodeScript); diff --git a/lib/base/sample-configs/.env-sample-rpc b/lib/base/sample-configs/.env-sample-rpc index 3aed121e..2d991859 100644 --- a/lib/base/sample-configs/.env-sample-rpc +++ b/lib/base/sample-configs/.env-sample-rpc @@ -8,6 +8,7 @@ AWS_REGION="us-east-1" ## Common configuration parameters ## BASE_NETWORK_ID="sepolia" # All options: "mainnet", "sepolia" +BASE_NODE_CONFIGURATION="full" # All options: "full", "archive" BASE_INSTANCE_TYPE="m7g.2xlarge" BASE_CPU_TYPE="ARM_64" # All options: "x86_64", "ARM_64". IMPORTANT: Make sure the CPU type matches the instance type used @@ -20,6 +21,8 @@ BASE_RESTORE_FROM_SNAPSHOT="true" # Download snapshot to speed up statup ti BASE_L1_EXECUTION_ENDPOINT="https://ethereum-sepolia-rpc.publicnode.com" # Set your own URL to Ethereum L1 node: https://docs.base.org/tools/node-providers BASE_L1_CONSENSUS_ENDPOINT="https://ethereum-sepolia-beacon-api.publicnode.com" +BASE_SNAPSHOT_URL="none" # Optionally provide the URL to download snpashot: https://docs.base.org/tutorials/run-a-base-node/#snapshots + # Example for Sepolia: #BASE_L1_EXECUTION_ENDPOINT=https://ethereum-sepolia-rpc.publicnode.com #BASE_L1_CONSENSUS_ENDPOINT=https://ethereum-sepolia-beacon-api.publicnode.com From de41e108c3b3e6ccf492dec86ae6c0f0ca98347f Mon Sep 17 00:00:00 2001 From: Nikolay Vlasov Date: Mon, 6 May 2024 15:30:13 +1000 Subject: [PATCH 50/75] Ethereum. Added S3 buckets for Amazon Linux 2023 repos --- lib/ethereum/lib/common-stack.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/ethereum/lib/common-stack.ts b/lib/ethereum/lib/common-stack.ts index ba9abb9d..067e6466 100644 --- a/lib/ethereum/lib/common-stack.ts +++ b/lib/ethereum/lib/common-stack.ts @@ -70,6 +70,8 @@ export class EthCommonStack extends cdk.Stack { resources: [ snapshotsBucket.bucketArn, snapshotsBucket.arnForObjects("*"), + `arn:aws:s3:::al2023-repos-${region}-*`, + `arn:aws:s3:::al2023-repos-${region}-*/*`, `arn:aws:s3:::amazonlinux-2-repos-${region}`, `arn:aws:s3:::amazonlinux-2-repos-${region}/*`, `arn:aws:s3:::${asset.s3BucketName}`, From 059bc7d82b5d8b8af90face30ac38196631519fa Mon Sep 17 00:00:00 2001 From: Nikolay Vlasov Date: Tue, 14 May 2024 16:02:06 +1000 Subject: [PATCH 51/75] Base. Moving to different download method --- lib/base/lib/assets/start-from-snapshot.sh | 28 +++++++++++----------- lib/base/lib/assets/user-data/node.sh | 14 ----------- 2 files changed, 14 insertions(+), 28 deletions(-) diff --git a/lib/base/lib/assets/start-from-snapshot.sh b/lib/base/lib/assets/start-from-snapshot.sh index 8583cc6b..543821e6 100644 --- a/lib/base/lib/assets/start-from-snapshot.sh +++ b/lib/base/lib/assets/start-from-snapshot.sh @@ -5,34 +5,34 @@ echo "Downloading snpashot" cd /data -BASE_SNAPSHOT_FILE_NAME=snalshot.tar.gz +BASE_SNAPSHOT_FILE_NAME=snapshot.tar.gz BASE_SNAPSHOT_DIR=/data/ BASE_SNAPSHOT_DOWNLOAD_STATUS=-1 -BASE_LATEST_SNAPSHOT_FILE_NAME=$(curl https://$NETWORK_ID-$NODE_CONFIG-snapshots.base.org/latest) if [ "$SNAPSHOT_URL" == "none" ] || [ -z "${SNAPSHOT_URL}" ]; then + BASE_LATEST_SNAPSHOT_FILE_NAME=$(curl https://$NETWORK_ID-$NODE_CONFIG-snapshots.base.org/latest) SNAPSHOT_URL=https://$NETWORK_ID-$NODE_CONFIG-snapshots.base.org/$BASE_LATEST_SNAPSHOT_FILE_NAME fi while (( BASE_SNAPSHOT_DOWNLOAD_STATUS != 0 )) do - PIDS=$(pgrep aria2c) + PIDS=$(pgrep wget) if [ -z "$PIDS" ]; then - aria2c -x3 $SNAPSHOT_URL -d $BASE_SNAPSHOT_DIR -o $BASE_SNAPSHOT_FILE_NAME -l aria2c.log --log-level=warn --allow-piece-length-change=true + wget --continue --retry-connrefused --waitretry=66 --read-timeout=20 --output-document$BASE_SNAPSHOT_DIR/$BASE_SNAPSHOT_FILE_NAME -o download.log -t 0 $SNAPSHOT_URL fi BASE_SNAPSHOT_DOWNLOAD_STATUS=$? - pid=$(pidof aria2c) + pid=$(pidof wget) wait $pid - echo "aria2c exit." + echo "wget exit." case $BASE_SNAPSHOT_DOWNLOAD_STATUS in + 2) + echo "CLI parsing error. Check variables." + exit 2 + ;; 3) - echo "file not exist." + echo "File I/O error." exit 3 ;; - 9) - echo "No space left on device." - exit 9 - ;; *) continue ;; @@ -45,14 +45,14 @@ sleep 60 # take about 2 hours to decompress the snapshot echo "Decompression snapshot start ..." -tar -I zstdmt -xf $BASE_SNAPSHOT_DIR/$BASE_SNAPSHOT_FILE_NAME -C /data 2>&1 | tee unzip.log && echo "decompression success..." || echo "decompression failed..." >> snapshots-decompression.log -echo "Decompressing snapshot success ..." +tar -zxvf $BASE_SNAPSHOT_DIR/$BASE_SNAPSHOT_FILE_NAME -C /data 2>&1 | tee unzip.log && echo "decompresed successfully..." || echo "decompression failed..." >> snapshots-decompression.log +echo "Decompresed snapshot ..." mv /data/snapshots/$NETWORK_ID/download/* /data && \ rm -rf /data/snapshots && \ rm -rf /data/$BASE_SNAPSHOT_FILE_NAME -echo "Snapshot is ready !!!" +echo "Processed snapshot" chown -R bcuser:bcuser /data && \ sudo su bcuser && \ diff --git a/lib/base/lib/assets/user-data/node.sh b/lib/base/lib/assets/user-data/node.sh index 0d2ef2ba..e61e6cbe 100644 --- a/lib/base/lib/assets/user-data/node.sh +++ b/lib/base/lib/assets/user-data/node.sh @@ -60,20 +60,6 @@ ln /usr/local/bin/aws /usr/bin/aws echo 'Installing SSM Agent' yum install -y $SSM_AGENT_BINARY_URI -# install aria2 a p2p downloader - -if [ "$arch" == "x86_64" ]; then - wget https://github.com/q3aql/aria2-static-builds/releases/download/v1.36.0/aria2-1.36.0-linux-gnu-64bit-build1.tar.bz2 - tar jxvf aria2-1.36.0-linux-gnu-64bit-build1.tar.bz2 - cd aria2-1.36.0-linux-gnu-64bit-build1/ - make install -else - wget https://github.com/q3aql/aria2-static-builds/releases/download/v1.36.0/aria2-1.36.0-linux-gnu-arm-rbpi-build1.tar.bz2 - tar jxvf aria2-1.36.0-linux-gnu-arm-rbpi-build1.tar.bz2 - cd aria2-1.36.0-linux-gnu-arm-rbpi-build1/ - make install -fi - # Base specific setup starts here # Set by Base-specic CDK components and stacks From 34ce0c1a741b3a65293837ebaa2a63fbecf565d6 Mon Sep 17 00:00:00 2001 From: Nikolay Vlasov Date: Thu, 16 May 2024 10:59:20 +1000 Subject: [PATCH 52/75] Base. Refactoring snapshot download. --- ...-from-snapshot.sh => download-snapshot.sh} | 6 +--- lib/base/lib/assets/user-data/node.sh | 10 +++++- lib/base/sample-configs/.env-sample-archive | 31 +++++++++++++++++++ .../{.env-sample-rpc => .env-sample-full} | 4 +-- 4 files changed, 43 insertions(+), 8 deletions(-) rename lib/base/lib/assets/{start-from-snapshot.sh => download-snapshot.sh} (83%) create mode 100644 lib/base/sample-configs/.env-sample-archive rename lib/base/sample-configs/{.env-sample-rpc => .env-sample-full} (94%) diff --git a/lib/base/lib/assets/start-from-snapshot.sh b/lib/base/lib/assets/download-snapshot.sh similarity index 83% rename from lib/base/lib/assets/start-from-snapshot.sh rename to lib/base/lib/assets/download-snapshot.sh index 543821e6..9f04707c 100644 --- a/lib/base/lib/assets/start-from-snapshot.sh +++ b/lib/base/lib/assets/download-snapshot.sh @@ -18,7 +18,7 @@ while (( BASE_SNAPSHOT_DOWNLOAD_STATUS != 0 )) do PIDS=$(pgrep wget) if [ -z "$PIDS" ]; then - wget --continue --retry-connrefused --waitretry=66 --read-timeout=20 --output-document$BASE_SNAPSHOT_DIR/$BASE_SNAPSHOT_FILE_NAME -o download.log -t 0 $SNAPSHOT_URL + wget --continue --retry-connrefused --waitretry=66 --read-timeout=20 --output-document $BASE_SNAPSHOT_DIR/$BASE_SNAPSHOT_FILE_NAME -nv -o download.log -t 0 $SNAPSHOT_URL fi BASE_SNAPSHOT_DOWNLOAD_STATUS=$? pid=$(pidof wget) @@ -53,7 +53,3 @@ rm -rf /data/snapshots && \ rm -rf /data/$BASE_SNAPSHOT_FILE_NAME echo "Processed snapshot" - -chown -R bcuser:bcuser /data && \ -sudo su bcuser && \ -/usr/local/bin/docker-compose -f /home/bcuser/node/docker-compose.yml up -d diff --git a/lib/base/lib/assets/user-data/node.sh b/lib/base/lib/assets/user-data/node.sh index e61e6cbe..06e803b0 100644 --- a/lib/base/lib/assets/user-data/node.sh +++ b/lib/base/lib/assets/user-data/node.sh @@ -241,7 +241,15 @@ if [ "$RESTORE_FROM_SNAPSHOT" == "false" ]; then else echo "Restoring data from snapshot" chmod 766 /opt/start-from-snapshot.sh - echo "/opt/start-from-snapshot.sh" | at now +3 minutes + /opt/download-snapshot.sh + if [ "$?" == 0 ]; then + echo "Snapshot download successful" + else + echo "Snapshot download failed, falling back to fresh sync" + fi + chown -R bcuser:bcuser /data + sudo su bcuser + /usr/local/bin/docker-compose -f /home/bcuser/node/docker-compose.yml up -d fi echo "All Done!!" diff --git a/lib/base/sample-configs/.env-sample-archive b/lib/base/sample-configs/.env-sample-archive new file mode 100644 index 00000000..12d99bea --- /dev/null +++ b/lib/base/sample-configs/.env-sample-archive @@ -0,0 +1,31 @@ +############################################################# +# Example configuration for Base nodes runner app on AWS # +############################################################# + +## Set the AWS account is and region for your environment ## +AWS_ACCOUNT_ID="xxxxxxxx" +AWS_REGION="us-east-1" + +## Common configuration parameters ## +BASE_NETWORK_ID="mainnet" # All options: "mainnet", "sepolia" +BASE_NODE_CONFIGURATION="archive" # All options: "full", "archive" +BASE_INSTANCE_TYPE="m7g.2xlarge" +BASE_CPU_TYPE="ARM_64" # All options: "x86_64", "ARM_64". IMPORTANT: Make sure the CPU type matches the instance type used + +# Data volume configuration +BASE_DATA_VOL_TYPE="gp3" # Other options: "io1" | "io2" | "gp3" | "instance-store" . IMPORTANT: Use "instance-store" option only with instance types that support that feature, like popular for node im4gn, d3, i3en, and i4i instance families +BASE_DATA_VOL_SIZE="7200" # Current required data size in GB to keep both snapshot archive and unarchived version of it. For Sepolia 1000 will be sufficient. +BASE_DATA_VOL_IOPS="5000" # Max IOPS for EBS volumes (not applicable for "instance-store") +BASE_DATA_VOL_THROUGHPUT="700" # Max throughput for EBS gp3 volumes (not applicable for "io1" | "io2" | "instance-store") +BASE_RESTORE_FROM_SNAPSHOT="true" # Download snapshot to speed up statup time +BASE_L1_EXECUTION_ENDPOINT="https://ethereum-sepolia-rpc.publicnode.com" # Set your own URL to Ethereum L1 node: https://docs.base.org/tools/node-providers +BASE_L1_CONSENSUS_ENDPOINT="https://ethereum-sepolia-beacon-api.publicnode.com" + +BASE_SNAPSHOT_URL="none" # Optionally provide the URL to download snpashot: https://docs.base.org/tutorials/run-a-base-node/#snapshots + +# Example for Sepolia: +#BASE_L1_EXECUTION_ENDPOINT=https://ethereum-sepolia-rpc.publicnode.com +#BASE_L1_CONSENSUS_ENDPOINT=https://ethereum-sepolia-beacon-api.publicnode.com +# Example for Mainnet and with Ethereum Blueprint with Geth-Lighthouse client combination and private IP: +#BASE_L1_EXECUTION_ENDPOINT=http://172.31.15.220:8545 +#BASE_L1_CONSENSUS_ENDPOINT=http://172.31.15.220:5052 diff --git a/lib/base/sample-configs/.env-sample-rpc b/lib/base/sample-configs/.env-sample-full similarity index 94% rename from lib/base/sample-configs/.env-sample-rpc rename to lib/base/sample-configs/.env-sample-full index 2d991859..a99c26b3 100644 --- a/lib/base/sample-configs/.env-sample-rpc +++ b/lib/base/sample-configs/.env-sample-full @@ -7,14 +7,14 @@ AWS_ACCOUNT_ID="xxxxxxxx" AWS_REGION="us-east-1" ## Common configuration parameters ## -BASE_NETWORK_ID="sepolia" # All options: "mainnet", "sepolia" +BASE_NETWORK_ID="mainnet" # All options: "mainnet", "sepolia" BASE_NODE_CONFIGURATION="full" # All options: "full", "archive" BASE_INSTANCE_TYPE="m7g.2xlarge" BASE_CPU_TYPE="ARM_64" # All options: "x86_64", "ARM_64". IMPORTANT: Make sure the CPU type matches the instance type used # Data volume configuration BASE_DATA_VOL_TYPE="gp3" # Other options: "io1" | "io2" | "gp3" | "instance-store" . IMPORTANT: Use "instance-store" option only with instance types that support that feature, like popular for node im4gn, d3, i3en, and i4i instance families -BASE_DATA_VOL_SIZE="5100" # Current required data size in GB to keep both snapshot archive and unarchived version of it. For Sepolia 1000 will be sufficient. +BASE_DATA_VOL_SIZE="4000" # Current required data size in GB to keep both snapshot archive and unarchived version of it. For Sepolia 1000 will be sufficient. BASE_DATA_VOL_IOPS="5000" # Max IOPS for EBS volumes (not applicable for "instance-store") BASE_DATA_VOL_THROUGHPUT="700" # Max throughput for EBS gp3 volumes (not applicable for "io1" | "io2" | "instance-store") BASE_RESTORE_FROM_SNAPSHOT="true" # Download snapshot to speed up statup time From 61657d9f792f5905fda9e1725e9402d7acf9d9c8 Mon Sep 17 00:00:00 2001 From: Nikolay Vlasov Date: Mon, 20 May 2024 16:39:57 +1000 Subject: [PATCH 53/75] Base. Debugging snapshot download --- lib/base/lib/assets/base/node.sh | 10 +++ lib/base/lib/assets/download-snapshot.sh | 55 ----------------- lib/base/lib/assets/restore-from-snapshot.sh | 63 +++++++++++++++++++ lib/base/lib/assets/user-data/node.sh | 65 +++++++++++++++----- 4 files changed, 124 insertions(+), 69 deletions(-) create mode 100644 lib/base/lib/assets/base/node.sh delete mode 100644 lib/base/lib/assets/download-snapshot.sh create mode 100644 lib/base/lib/assets/restore-from-snapshot.sh diff --git a/lib/base/lib/assets/base/node.sh b/lib/base/lib/assets/base/node.sh new file mode 100644 index 00000000..abdd35a5 --- /dev/null +++ b/lib/base/lib/assets/base/node.sh @@ -0,0 +1,10 @@ +#!/bin/bash +set -e +echo "Script is starting..." +ulimit -n 500000 + +# Start the node +cd /home/bcuser/node +/usr/local/bin/docker-compose -f /home/bcuser/node/docker-compose.yml up -d + +echo "Started" \ No newline at end of file diff --git a/lib/base/lib/assets/download-snapshot.sh b/lib/base/lib/assets/download-snapshot.sh deleted file mode 100644 index 9f04707c..00000000 --- a/lib/base/lib/assets/download-snapshot.sh +++ /dev/null @@ -1,55 +0,0 @@ -#!/bin/bash - -source /etc/environment -echo "Downloading snpashot" - -cd /data - -BASE_SNAPSHOT_FILE_NAME=snapshot.tar.gz -BASE_SNAPSHOT_DIR=/data/ -BASE_SNAPSHOT_DOWNLOAD_STATUS=-1 - -if [ "$SNAPSHOT_URL" == "none" ] || [ -z "${SNAPSHOT_URL}" ]; then - BASE_LATEST_SNAPSHOT_FILE_NAME=$(curl https://$NETWORK_ID-$NODE_CONFIG-snapshots.base.org/latest) - SNAPSHOT_URL=https://$NETWORK_ID-$NODE_CONFIG-snapshots.base.org/$BASE_LATEST_SNAPSHOT_FILE_NAME -fi - -while (( BASE_SNAPSHOT_DOWNLOAD_STATUS != 0 )) -do - PIDS=$(pgrep wget) - if [ -z "$PIDS" ]; then - wget --continue --retry-connrefused --waitretry=66 --read-timeout=20 --output-document $BASE_SNAPSHOT_DIR/$BASE_SNAPSHOT_FILE_NAME -nv -o download.log -t 0 $SNAPSHOT_URL - fi - BASE_SNAPSHOT_DOWNLOAD_STATUS=$? - pid=$(pidof wget) - wait $pid - echo "wget exit." - case $BASE_SNAPSHOT_DOWNLOAD_STATUS in - 2) - echo "CLI parsing error. Check variables." - exit 2 - ;; - 3) - echo "File I/O error." - exit 3 - ;; - *) - continue - ;; - esac -done - -echo "Downloading snapshot succeed" - -sleep 60 -# take about 2 hours to decompress the snapshot -echo "Decompression snapshot start ..." - -tar -zxvf $BASE_SNAPSHOT_DIR/$BASE_SNAPSHOT_FILE_NAME -C /data 2>&1 | tee unzip.log && echo "decompresed successfully..." || echo "decompression failed..." >> snapshots-decompression.log -echo "Decompresed snapshot ..." - -mv /data/snapshots/$NETWORK_ID/download/* /data && \ -rm -rf /data/snapshots && \ -rm -rf /data/$BASE_SNAPSHOT_FILE_NAME - -echo "Processed snapshot" diff --git a/lib/base/lib/assets/restore-from-snapshot.sh b/lib/base/lib/assets/restore-from-snapshot.sh new file mode 100644 index 00000000..c73bb927 --- /dev/null +++ b/lib/base/lib/assets/restore-from-snapshot.sh @@ -0,0 +1,63 @@ +#!/bin/bash +set +e + +source /etc/environment + +echo "Downloading Snapshot." + +cd /data + +SNAPSHOT_FILE_NAME=snapshot.tar.gz +SNAPSHOT_DIR=/data +SNAPSHOT_DOWNLOAD_STATUS=-1 + +if [ "$SNAPSHOT_URL" == "none" ] || [ -z "${SNAPSHOT_URL}" ]; then + LATEST_SNAPSHOT_FILE_NAME=$(curl https://$NETWORK_ID-$NODE_CONFIG-snapshots.base.org/latest) + SNAPSHOT_URL=https://$NETWORK_ID-$NODE_CONFIG-snapshots.base.org/$LATEST_SNAPSHOT_FILE_NAME +fi + +# take about 1 hour to download the Snapshot +while (( SNAPSHOT_DOWNLOAD_STATUS != 0 )) +do + PIDS=$(pgrep aria2c) + if [ -z "$PIDS" ]; then + aria2c $SNAPSHOT_URL -d $SNAPSHOT_DIR -o $SNAPSHOT_FILE_NAME -l /data/download.log --log-level=notice --allow-overwrite=true --allow-piece-length-change=true + fi + SNAPSHOT_DOWNLOAD_STATUS=$? + pid=$(pidof aria2c) + wait $pid + echo "aria2c exit." + case $SNAPSHOT_DOWNLOAD_STATUS in + 3) + echo "File does not exist." + exit 3 + ;; + 9) + echo "No space left on device." + exit 9 + ;; + *) + continue + ;; + esac +done +echo "Downloading Snapshot script finished" + +sleep 60 + +echo "Starting snapshot decompression ..." + +tar -zxvf $SNAPSHOT_DIR/$SNAPSHOT_FILE_NAME -C /data 2>&1 | tee unzip.log && echo "decompresed successfully..." || echo "decompression failed..." >> snapshots-decompression.log + +echo "Decompresed snapshot, cleaning up..." + +mv /data/snapshots/$NETWORK_ID/download/* /data && \ +rm -rf /data/snapshots && \ +rm -rf $SNAPSHOT_DIR/$SNAPSHOT_FILE_NAME + +echo "Snapshot is ready, starting the service.." + +chown -R bcuser:bcuser /data + +sudo systemctl daemon-reload +sudo systemctl enable --now base \ No newline at end of file diff --git a/lib/base/lib/assets/user-data/node.sh b/lib/base/lib/assets/user-data/node.sh index 06e803b0..d6f2d332 100644 --- a/lib/base/lib/assets/user-data/node.sh +++ b/lib/base/lib/assets/user-data/node.sh @@ -33,6 +33,20 @@ yum update -y yum -y install amazon-cloudwatch-agent collectd jq yq gcc ncurses-devel aws-cfn-bootstrap zstd wget $YQ_URI -O /usr/bin/yq && chmod +x /usr/bin/yq +# install aria2 a p2p downloader + +if [ "$arch" == "x86_64" ]; then + wget https://github.com/q3aql/aria2-static-builds/releases/download/v1.36.0/aria2-1.36.0-linux-gnu-64bit-build1.tar.bz2 + tar jxvf aria2-1.36.0-linux-gnu-64bit-build1.tar.bz2 + cd aria2-1.36.0-linux-gnu-64bit-build1/ + make install +else + wget https://github.com/q3aql/aria2-static-builds/releases/download/v1.36.0/aria2-1.36.0-linux-gnu-arm-rbpi-build1.tar.bz2 + tar jxvf aria2-1.36.0-linux-gnu-arm-rbpi-build1.tar.bz2 + cd aria2-1.36.0-linux-gnu-arm-rbpi-build1/ + make install +fi + cd /opt echo "Downloading assets zip file" @@ -187,6 +201,8 @@ case $NETWORK_ID in ;; esac +echo "OP_NODE_L1_TRUST_RPC=true" >> $OP_CONFIG_FILE_PATH + sed -i "s#GETH_HOST_DATA_DIR=./geth-data#GETH_HOST_DATA_DIR=/data/geth#g" /home/bcuser/node/.env chown -R bcuser:bcuser /home/bcuser/node @@ -198,6 +214,27 @@ chmod 766 /opt/syncchecker.sh echo "*/5 * * * * /opt/syncchecker.sh" | crontab crontab -l +echo "Configuring node as a service" +mkdir /home/bcuser/bin +mv /opt/base/node.sh /home/bcuser/bin/node.sh +chmod 766 /home/bcuser/bin/node.sh +chown -R bcuser:bcuser /home/bcuser + +sudo bash -c 'cat > /etc/systemd/system/base.service < Date: Tue, 21 May 2024 14:50:08 +1000 Subject: [PATCH 54/75] Base. Removed ulimit from service startup script --- lib/base/lib/assets/base/node.sh | 1 - lib/base/lib/assets/user-data/node.sh | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/base/lib/assets/base/node.sh b/lib/base/lib/assets/base/node.sh index abdd35a5..866c7fcd 100644 --- a/lib/base/lib/assets/base/node.sh +++ b/lib/base/lib/assets/base/node.sh @@ -1,7 +1,6 @@ #!/bin/bash set -e echo "Script is starting..." -ulimit -n 500000 # Start the node cd /home/bcuser/node diff --git a/lib/base/lib/assets/user-data/node.sh b/lib/base/lib/assets/user-data/node.sh index d6f2d332..00bf8c73 100644 --- a/lib/base/lib/assets/user-data/node.sh +++ b/lib/base/lib/assets/user-data/node.sh @@ -273,8 +273,8 @@ chmod -R 755 /data if [ "$RESTORE_FROM_SNAPSHOT" == "false" ]; then echo "Skipping restoration from snapshot. Starting node" - sudo systemctl daemon-reload - sudo systemctl enable --now base + systemctl daemon-reload + systemctl enable --now base else echo "Restoring node from snapshot" chmod +x /opt/restore-from-snapshot.sh From 035ace24941acf07eb49e9317a9aea40a4d0257b Mon Sep 17 00:00:00 2001 From: Nikolay Vlasov Date: Fri, 24 May 2024 20:06:59 +1000 Subject: [PATCH 55/75] Base. Added Instance Store volume option for storage --- lib/base/README.md | 43 ++- lib/base/app.ts | 20 ++ .../assets/setup-instance-store-volumes.sh | 39 +++ lib/base/lib/assets/user-data/node.sh | 54 ++-- lib/base/lib/config/baseConfig.interface.ts | 6 + lib/base/lib/config/baseConfig.ts | 6 + .../node-cw-dashboard.ts | 0 lib/base/lib/ha-nodes-stack.ts | 141 +++++++++ lib/base/lib/single-node-stack.ts | 2 +- lib/base/sample-configs/.env-sample-archive | 11 +- lib/base/sample-configs/.env-sample-full | 9 +- lib/base/single-archive-node-deploy.json | 5 + lib/base/test/.env-test | 10 +- lib/base/test/base-single-node.test.ts | 12 +- lib/base/test/ha-nodes-stack.test.ts | 273 ++++++++++++++++++ 15 files changed, 597 insertions(+), 34 deletions(-) create mode 100644 lib/base/lib/assets/setup-instance-store-volumes.sh rename lib/base/lib/{assets => constructs}/node-cw-dashboard.ts (100%) create mode 100644 lib/base/lib/ha-nodes-stack.ts create mode 100644 lib/base/single-archive-node-deploy.json create mode 100644 lib/base/test/ha-nodes-stack.test.ts diff --git a/lib/base/README.md b/lib/base/README.md index 7e9161a0..a54ea52a 100644 --- a/lib/base/README.md +++ b/lib/base/README.md @@ -122,7 +122,7 @@ npx cdk deploy base-common > cdk bootstrap aws://ACCOUNT-NUMBER/REGION > ``` -### From your Cloud9: Deploy Single Node +### Option 1: Deploy Single Node 1. For L1 node you you can set your own URLs in `BASE_L1_EXECUTION_ENDPOINT` and `BASE_L1_CONSENSUS_ENDPOINT` properties of `.env` file. It can be one of [the providers recommended by Base](https://docs.base.org/tools/node-providers) or you can run your own Ethereum node [with Node Runner Ethereum blueprint](https://aws-samples.github.io/aws-blockchain-node-runners/docs/Blueprints/Ethereum) (tested with geth-lighthouse combination). For example: @@ -163,12 +163,48 @@ aws ssm start-session --target $INSTANCE_ID --region $AWS_REGION curl -s -X POST -H "Content-Type: application/json" --data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' http://localhost:8545 ``` +### Option 2: Highly Available RPC Nodes + +1. For L1 node you you can set your own URLs in `BASE_L1_EXECUTION_ENDPOINT` and `BASE_L1_CONSENSUS_ENDPOINT` properties of `.env` file. It can be one of [the providers recommended by Base](https://docs.base.org/tools/node-providers) or you can run your own Ethereum node [with Node Runner Ethereum blueprint](https://aws-samples.github.io/aws-blockchain-node-runners/docs/Blueprints/Ethereum) (tested with geth-lighthouse combination). For example: + +```bash +#For Sepolia: +BASE_L1_EXECUTION_ENDPOINT="https://ethereum-sepolia-rpc.publicnode.com" +BASE_L1_CONSENSUS_ENDPOINT="https://ethereum-sepolia-beacon-api.publicnode.com" +``` + +2. Deploy Base RPC Node and wait for it to sync. For Mainnet it might a day when using snapshots or about a week if syncing from block 0. You can use snapshots provided by the Base team by setting `BASE_RESTORE_FROM_SNAPSHOT="true"` in `.env` file. + + ```bash + pwd + # Make sure you are in aws-blockchain-node-runners/lib/base + npx cdk deploy base-ha-nodes --json --outputs-file ha-nodes-deploy.json + ``` + +2. Give the new RPC **full** nodes about 2-3 hours (24 hours for **archive** node) to initialize and then run the following query against the load balancer behind the RPC node created + + ```bash + export RPC_ALB_URL=$(cat ha-nodes-deploy.json | jq -r '..|.alburl? | select(. != null)') + echo $RPC_ALB_URL + ``` + + Periodically check [Geth Syncing Status](https://geth.ethereum.org/docs/fundamentals/logs#syncing). Run the following query from within the same VPC and against the private IP of the load balancer fronting your nodes: + + ```bash + curl http://$RPC_ALB_URL:8545 -X POST -H "Content-Type: application/json" \ + --data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' + ``` + +**NOTE:** By default and for security reasons the load balancer is available only from within the default VPC in the region where it is deployed. It is not available from the Internet and is not open for external connections. Before opening it up please make sure you protect your RPC APIs. + ### Monitoring -Every 5 minutes a script on the Base node publishes to CloudWatch service the metrics for current block for L1/L2 clients as well as blocks behind metric for L1 and minutes buehind for L2. When the node is fully synced the blocks behind metric should get to 4 and minutes behind should get down to 0. To see the metrics: +Every 5 minutes a script on the Base node publishes to CloudWatch service the metrics for current block for L1/L2 clients as well as blocks behind metric for L1 and minutes behind for L2. When the node is fully synced the blocks behind metric should get to 4 and minutes behind should get down to 0. To see the metrics for **single node only**: - Navigate to CloudWatch service (make sure you are in the region you have specified for AWS_REGION) - Open Dashboards and select `base-single-node--` from the list of dashboards. +Metrics for **ha nodes** configuration is not yet implemented (contributions are welcome!) + ## From your Cloud9: Clear up and undeploy everything 1. Undeploy all Nodes and Common stacks @@ -184,6 +220,9 @@ pwd # Undeploy Single Node npx cdk destroy base-single-node +# Undeploy HA Nodes +npx cdk destroy base-ha-nodes + # Delete all common components like IAM role and Security Group npx cdk destroy base-common ``` diff --git a/lib/base/app.ts b/lib/base/app.ts index 94b55bba..ca401786 100644 --- a/lib/base/app.ts +++ b/lib/base/app.ts @@ -5,6 +5,7 @@ import * as cdk from 'aws-cdk-lib'; import * as config from "./lib/config/baseConfig"; import {BaseCommonStack} from "./lib/common-stack"; import {BaseSingleNodeStack} from "./lib/single-node-stack"; +import {BaseHANodesStack} from "./lib/ha-nodes-stack"; const app = new cdk.App(); cdk.Tags.of(app).add("Project", "AWSBase"); @@ -28,3 +29,22 @@ new BaseSingleNodeStack(app, "base-single-node", { snapshotUrl: config.baseNodeConfig.snapshotUrl, dataVolume: config.baseNodeConfig.dataVolume, }); + +new BaseHANodesStack(app, "base-ha-nodes", { + stackName: `base-ha-nodes-${config.baseNodeConfig.baseNodeConfiguration}-${config.baseNodeConfig.baseNetworkId}`, + env: { account: config.baseConfig.accountId, region: config.baseConfig.region }, + + instanceType: config.baseNodeConfig.instanceType, + instanceCpuType: config.baseNodeConfig.instanceCpuType, + baseNetworkId: config.baseNodeConfig.baseNetworkId, + baseNodeConfiguration: config.baseNodeConfig.baseNodeConfiguration, + restoreFromSnapshot: config.baseNodeConfig.restoreFromSnapshot, + l1ExecutionEndpoint: config.baseNodeConfig.l1ExecutionEndpoint, + l1ConsensusEndpoint: config.baseNodeConfig.l1ConsensusEndpoint, + snapshotUrl: config.baseNodeConfig.snapshotUrl, + dataVolume: config.baseNodeConfig.dataVolume, + + albHealthCheckGracePeriodMin: config.haNodeConfig.albHealthCheckGracePeriodMin, + heartBeatDelayMin: config.haNodeConfig.heartBeatDelayMin, + numberOfNodes: config.haNodeConfig.numberOfNodes +}); diff --git a/lib/base/lib/assets/setup-instance-store-volumes.sh b/lib/base/lib/assets/setup-instance-store-volumes.sh new file mode 100644 index 00000000..934d19fb --- /dev/null +++ b/lib/base/lib/assets/setup-instance-store-volumes.sh @@ -0,0 +1,39 @@ +#!/bin/bash + +source /etc/environment + +if [[ "$DATA_VOLUME_TYPE" == "instance-store" ]]; then + echo "Data volume type is instance store" + export DATA_VOLUME_ID=/dev/$(lsblk -lnb | awk 'max < $4 {max = $4; vol = $1} END {print vol}') +fi + +if [ -n "$DATA_VOLUME_ID" ]; then + if [ $(df --output=target | grep -c "/data") -lt 1 ]; then + echo "Checking fstab for Data volume" + + mkfs.ext4 $DATA_VOLUME_ID + echo "Data volume formatted. Mounting..." + # Waiting wihtouht using sleep as it sometimes just hangs.... + coproc read -t 10 && wait "$!" || true + DATA_VOLUME_UUID=$(lsblk -fn -o UUID $DATA_VOLUME_ID) + DATA_VOLUME_FSTAB_CONF="UUID=$DATA_VOLUME_UUID /data ext4 defaults 0 2" + echo "DATA_VOLUME_ID="$DATA_VOLUME_ID + echo "DATA_VOLUME_UUID="$DATA_VOLUME_UUID + echo "DATA_VOLUME_FSTAB_CONF="$DATA_VOLUME_FSTAB_CONF + + # Check if data disc is already in fstab and replace the line if it is with the new disc UUID + if [ $(grep -c "data" /etc/fstab) -gt 0 ]; then + SED_REPLACEMENT_STRING="$(grep -n "/data" /etc/fstab | cut -d: -f1)s#.*#$DATA_VOLUME_FSTAB_CONF#" + cp /etc/fstab /etc/fstab.bak + sed -i "$SED_REPLACEMENT_STRING" /etc/fstab + else + echo $DATA_VOLUME_FSTAB_CONF | sudo tee -a /etc/fstab + fi + + sudo mount -a + + chown bcuser:bcuser -R /data + else + echo "Data volume is mounted, nothing changed" + fi +fi \ No newline at end of file diff --git a/lib/base/lib/assets/user-data/node.sh b/lib/base/lib/assets/user-data/node.sh index 00bf8c73..9c6b46f5 100644 --- a/lib/base/lib/assets/user-data/node.sh +++ b/lib/base/lib/assets/user-data/node.sh @@ -6,10 +6,12 @@ LIFECYCLE_HOOK_NAME=${_LIFECYCLE_HOOK_NAME_} AUTOSCALING_GROUP_NAME=${_AUTOSCALING_GROUP_NAME_} RESOURCE_ID=${_NODE_CF_LOGICAL_ID_} ASSETS_S3_PATH=${_ASSETS_S3_PATH_} +DATA_VOLUME_TYPE=${_DATA_VOLUME_TYPE_} { echo "LIFECYCLE_HOOK_NAME=$LIFECYCLE_HOOK_NAME" echo "AUTOSCALING_GROUP_NAME=$AUTOSCALING_GROUP_NAME" echo "ASSETS_S3_PATH=$ASSETS_S3_PATH" + echo "DATA_VOLUME_TYPE=$DATA_VOLUME_TYPE" } >> /etc/environment arch=$(uname -m) @@ -34,6 +36,7 @@ yum -y install amazon-cloudwatch-agent collectd jq yq gcc ncurses-devel aws-cfn- wget $YQ_URI -O /usr/bin/yq && chmod +x /usr/bin/yq # install aria2 a p2p downloader +cd /tmp if [ "$arch" == "x86_64" ]; then wget https://github.com/q3aql/aria2-static-builds/releases/download/v1.36.0/aria2-1.36.0-linux-gnu-64bit-build1.tar.bz2 @@ -235,37 +238,48 @@ ExecStart=/home/bcuser/bin/node.sh WantedBy=multi-user.target EOF' -echo "Signaling completion to CloudFormation to continue with volume mount" -/opt/aws/bin/cfn-signal --stack $STACK_NAME --resource $RESOURCE_ID --region $REGION +if [[ "$LIFECYCLE_HOOK_NAME" == "none" ]]; then + echo "We run single node setup. Signaling completion to CloudFormation to continue with volume mount" + /opt/aws/bin/cfn-signal --stack $STACK_NAME --resource $RESOURCE_ID --region $REGION +fi echo "Preparing data volume" -echo "Wait for one minute for the volume to be available" -sleep 60 - -if $(lsblk | grep -q nvme1n1); then - echo "nvme1n1 is found. Configuring attached storage" +echo "Wait for one minute for the volume to become available" +sleep 60s - if [ "$FORMAT_DISK" == "false" ]; then - echo "Not creating a new filesystem in the disk. Existing data might be present!!" - else - mkfs -t ext4 /dev/nvme1n1 - fi +if [[ "$DATA_VOLUME_TYPE" == "instance-store" ]]; then + echo "Data volume type is instance store" - sleep 10 - # Define the line to add to fstab - uuid=$(lsblk -n -o UUID /dev/nvme1n1) - line="UUID=$uuid /data ext4 defaults 0 2" + cd /opt + chmod +x /opt/setup-instance-store-volumes.sh - # Write the line to fstab - echo $line | sudo tee -a /etc/fstab + (crontab -l; echo "@reboot /opt/setup-instance-store-volumes.sh >/tmp/setup-instance-store-volumes.log 2>&1") | crontab - + crontab -l - mount -a + DATA_VOLUME_ID=/dev/$(lsblk -lnb | awk 'max < $4 {max = $4; vol = $1} END {print vol}') else - echo "nvme1n1 is not found. Not doing anything" + echo "Data volume type is EBS" + + DATA_VOLUME_ID=/dev/$(lsblk -lnb | awk -v VOLUME_SIZE_BYTES="$DATA_VOLUME_SIZE" '{if ($4== VOLUME_SIZE_BYTES) {print $1}}') fi +mkfs -t ext4 $DATA_VOLUME_ID +echo "waiting for volume to get UUID" + OUTPUT=0; + while [ "$OUTPUT" = 0 ]; do + DATA_VOLUME_UUID=$(lsblk -fn -o UUID $DATA_VOLUME_ID) + OUTPUT=$(echo $DATA_VOLUME_UUID | grep -c - $2) + echo $OUTPUT + done +DATA_VOLUME_FSTAB_CONF="UUID=$DATA_VOLUME_UUID /data ext4 defaults 0 2" +echo "DATA_VOLUME_ID="$DATA_VOLUME_ID +echo "DATA_VOLUME_UUID="$DATA_VOLUME_UUID +echo "DATA_VOLUME_FSTAB_CONF="$DATA_VOLUME_FSTAB_CONF +echo $DATA_VOLUME_FSTAB_CONF | tee -a /etc/fstab +mount -a + lsblk -d chown -R bcuser:bcuser /data diff --git a/lib/base/lib/config/baseConfig.interface.ts b/lib/base/lib/config/baseConfig.interface.ts index e7e327a2..dc2f2300 100644 --- a/lib/base/lib/config/baseConfig.interface.ts +++ b/lib/base/lib/config/baseConfig.interface.ts @@ -23,3 +23,9 @@ export interface BaseBaseNodeConfig extends configTypes.BaseNodeConfig { l1ConsensusEndpoint: string; snapshotUrl: string; } + +export interface BaseHAConfig { + albHealthCheckGracePeriodMin: number; + heartBeatDelayMin: number; + numberOfNodes: number; +} \ No newline at end of file diff --git a/lib/base/lib/config/baseConfig.ts b/lib/base/lib/config/baseConfig.ts index 4da2e404..26727b08 100644 --- a/lib/base/lib/config/baseConfig.ts +++ b/lib/base/lib/config/baseConfig.ts @@ -39,3 +39,9 @@ export const baseNodeConfig: configTypes.BaseBaseNodeConfig = { throughput: process.env.BASE_DATA_VOL_THROUGHPUT ? parseInt(process.env.BASE_DATA_VOL_THROUGHPUT): 700, }, }; + +export const haNodeConfig: configTypes.BaseHAConfig = { + albHealthCheckGracePeriodMin: process.env.BASE_HA_ALB_HEALTHCHECK_GRACE_PERIOD_MIN ? parseInt(process.env.BASE_HA_ALB_HEALTHCHECK_GRACE_PERIOD_MIN) : 10, + heartBeatDelayMin: process.env.BASE_HA_NODES_HEARTBEAT_DELAY_MIN ? parseInt(process.env.BASE_HA_NODES_HEARTBEAT_DELAY_MIN) : 40, + numberOfNodes: process.env.BASE_HA_NUMBER_OF_NODES ? parseInt(process.env.BASE_HA_NUMBER_OF_NODES) : 2 +}; diff --git a/lib/base/lib/assets/node-cw-dashboard.ts b/lib/base/lib/constructs/node-cw-dashboard.ts similarity index 100% rename from lib/base/lib/assets/node-cw-dashboard.ts rename to lib/base/lib/constructs/node-cw-dashboard.ts diff --git a/lib/base/lib/ha-nodes-stack.ts b/lib/base/lib/ha-nodes-stack.ts new file mode 100644 index 00000000..d876281a --- /dev/null +++ b/lib/base/lib/ha-nodes-stack.ts @@ -0,0 +1,141 @@ +import * as cdk from "aws-cdk-lib"; +import * as cdkConstructs from "constructs"; +import * as ec2 from "aws-cdk-lib/aws-ec2"; +import * as iam from "aws-cdk-lib/aws-iam"; +import { AmazonLinuxGeneration, AmazonLinuxImage } from "aws-cdk-lib/aws-ec2"; +import * as s3Assets from "aws-cdk-lib/aws-s3-assets"; +import * as configTypes from "./config/baseConfig.interface"; +import { BaseNodeSecurityGroupConstruct } from "./constructs/base-node-security-group"; +import * as fs from "fs"; +import * as path from "path"; +import * as constants from "../../constructs/constants"; +import { HANodesConstruct } from "../../constructs/ha-rpc-nodes-with-alb"; +import * as nag from "cdk-nag"; + +export interface BaseHANodesStackProps extends cdk.StackProps { + instanceType: ec2.InstanceType; + instanceCpuType: ec2.AmazonLinuxCpuType; + baseNetworkId: configTypes.BaseNetworkId; + baseNodeConfiguration: configTypes.BaseNodeConfiguration; + restoreFromSnapshot: boolean; + l1ExecutionEndpoint: string, + l1ConsensusEndpoint: string, + snapshotUrl: string, + dataVolume: configTypes.BaseDataVolumeConfig; + albHealthCheckGracePeriodMin: number; + heartBeatDelayMin: number; + numberOfNodes: number; +} + +export class BaseHANodesStack extends cdk.Stack { + constructor(scope: cdkConstructs.Construct, id: string, props: BaseHANodesStackProps) { + super(scope, id, props); + + const REGION = cdk.Stack.of(this).region; + const STACK_NAME = cdk.Stack.of(this).stackName; + const lifecycleHookName = STACK_NAME; + const autoScalingGroupName = STACK_NAME; + + const { + instanceType, + instanceCpuType, + baseNetworkId, + baseNodeConfiguration, + restoreFromSnapshot, + l1ExecutionEndpoint, + l1ConsensusEndpoint, + dataVolume, + albHealthCheckGracePeriodMin, + heartBeatDelayMin, + numberOfNodes + } = props; + + // using default vpc + const vpc = ec2.Vpc.fromLookup(this, "vpc", { isDefault: true }); + + // setting up the security group for the node from BSC-specific construct + const instanceSG = new BaseNodeSecurityGroupConstruct(this, "security-group", { vpc: vpc }); + + // getting the IAM Role ARM from the common stack + const importedInstanceRoleArn = cdk.Fn.importValue("BaseNodeInstanceRoleArn"); + + const instanceRole = iam.Role.fromRoleArn(this, "iam-role", importedInstanceRoleArn); + + // making our scripts and configs from the local "assets" directory available for instance to download + const asset = new s3Assets.Asset(this, "assets", { + path: path.join(__dirname, "assets") + }); + + asset.bucket.grantRead(instanceRole); + + // parsing user data script and injecting necessary variables + const nodeScript = fs.readFileSync(path.join(__dirname, "assets", "user-data", "node.sh")).toString(); + const dataVolumeSizeBytes = dataVolume.sizeGiB * constants.GibibytesToBytesConversionCoefficient; + + const modifiedInitNodeScript = cdk.Fn.sub(nodeScript, { + _REGION_: REGION, + _ASSETS_S3_PATH_: `s3://${asset.s3BucketName}/${asset.s3ObjectKey}`, + _STACK_NAME_: STACK_NAME, + _NODE_CF_LOGICAL_ID_: constants.NoneValue, + _DATA_VOLUME_TYPE_: dataVolume.type, + _DATA_VOLUME_SIZE_: dataVolumeSizeBytes.toString(), + _NETWORK_ID_: baseNetworkId, + _NODE_CONFIG_: baseNodeConfiguration, + _LIFECYCLE_HOOK_NAME_: lifecycleHookName, + _AUTOSCALING_GROUP_NAME_: autoScalingGroupName, + _RESTORE_FROM_SNAPSHOT_: restoreFromSnapshot.toString(), + _FORMAT_DISK_: "true", + _L1_EXECUTION_ENDPOINT_: l1ExecutionEndpoint, + _L1_CONSENSUS_ENDPOINT_: l1ConsensusEndpoint, + _SNAPSHOT_URL_: props.snapshotUrl, + }); + + const rpcNodes = new HANodesConstruct(this, "rpc-nodes", { + instanceType, + dataVolumes: [dataVolume], + machineImage: new ec2.AmazonLinuxImage({ + generation: AmazonLinuxGeneration.AMAZON_LINUX_2, + kernel:ec2.AmazonLinuxKernel.KERNEL5_X, + cpuType: instanceCpuType + }), + role: instanceRole, + vpc, + securityGroup: instanceSG.securityGroup, + userData: modifiedInitNodeScript, + numberOfNodes, + rpcPortForALB: 8545, + albHealthCheckGracePeriodMin, + heartBeatDelayMin, + lifecycleHookName: lifecycleHookName, + autoScalingGroupName: autoScalingGroupName + }); + + + + new cdk.CfnOutput(this, "alb-url", { value: rpcNodes.loadBalancerDnsName }); + + // Adding suppressions to the stack + nag.NagSuppressions.addResourceSuppressions( + this, + [ + { + id: "AwsSolutions-AS3", + reason: "No notifications needed" + }, + { + id: "AwsSolutions-S1", + reason: "No access log needed for ALB logs bucket" + }, + { + id: "AwsSolutions-EC28", + reason: "Using basic monitoring to save costs" + }, + { + id: "AwsSolutions-IAM5", + reason: "Need read access to the S3 bucket with assets" + } + ], + true + ); + } +} diff --git a/lib/base/lib/single-node-stack.ts b/lib/base/lib/single-node-stack.ts index f47e7ac4..8c6eda38 100644 --- a/lib/base/lib/single-node-stack.ts +++ b/lib/base/lib/single-node-stack.ts @@ -5,7 +5,7 @@ import * as iam from "aws-cdk-lib/aws-iam"; import * as s3Assets from "aws-cdk-lib/aws-s3-assets"; import * as path from "path"; import * as fs from "fs"; -import * as nodeCwDashboard from "./assets/node-cw-dashboard" +import * as nodeCwDashboard from "./constructs/node-cw-dashboard" import * as cw from 'aws-cdk-lib/aws-cloudwatch'; import * as nag from "cdk-nag"; import { SingleNodeConstruct } from "../../constructs/single-node" diff --git a/lib/base/sample-configs/.env-sample-archive b/lib/base/sample-configs/.env-sample-archive index 12d99bea..b8f8efbd 100644 --- a/lib/base/sample-configs/.env-sample-archive +++ b/lib/base/sample-configs/.env-sample-archive @@ -8,8 +8,8 @@ AWS_REGION="us-east-1" ## Common configuration parameters ## BASE_NETWORK_ID="mainnet" # All options: "mainnet", "sepolia" -BASE_NODE_CONFIGURATION="archive" # All options: "full", "archive" -BASE_INSTANCE_TYPE="m7g.2xlarge" +BASE_NODE_CONFIGURATION="archive" # All options: "full", "archive" +BASE_INSTANCE_TYPE="m7g.2xlarge" # Reconneded for Insance Store: i3en.3xlarge, "x86_64" BASE_CPU_TYPE="ARM_64" # All options: "x86_64", "ARM_64". IMPORTANT: Make sure the CPU type matches the instance type used # Data volume configuration @@ -17,10 +17,10 @@ BASE_DATA_VOL_TYPE="gp3" # Other options: "io1" | "io2" | "gp3" | BASE_DATA_VOL_SIZE="7200" # Current required data size in GB to keep both snapshot archive and unarchived version of it. For Sepolia 1000 will be sufficient. BASE_DATA_VOL_IOPS="5000" # Max IOPS for EBS volumes (not applicable for "instance-store") BASE_DATA_VOL_THROUGHPUT="700" # Max throughput for EBS gp3 volumes (not applicable for "io1" | "io2" | "instance-store") -BASE_RESTORE_FROM_SNAPSHOT="true" # Download snapshot to speed up statup time BASE_L1_EXECUTION_ENDPOINT="https://ethereum-sepolia-rpc.publicnode.com" # Set your own URL to Ethereum L1 node: https://docs.base.org/tools/node-providers BASE_L1_CONSENSUS_ENDPOINT="https://ethereum-sepolia-beacon-api.publicnode.com" +BASE_RESTORE_FROM_SNAPSHOT="true" # Download snapshot to speed up statup time BASE_SNAPSHOT_URL="none" # Optionally provide the URL to download snpashot: https://docs.base.org/tutorials/run-a-base-node/#snapshots # Example for Sepolia: @@ -29,3 +29,8 @@ BASE_SNAPSHOT_URL="none" # Optionally provide the URL to download # Example for Mainnet and with Ethereum Blueprint with Geth-Lighthouse client combination and private IP: #BASE_L1_EXECUTION_ENDPOINT=http://172.31.15.220:8545 #BASE_L1_CONSENSUS_ENDPOINT=http://172.31.15.220:5052 + +## HA nodes configuration ## +BASE_HA_NUMBER_OF_NODES="2" # Total number of RPC nodes to be provisioned. Default: 2 +BASE_HA_ALB_HEALTHCHECK_GRACE_PERIOD_MIN="300" # Time enough to initialize the instance +BASE_HA_NODES_HEARTBEAT_DELAY_MIN="1440" # Time sufficient enough for a node do sync \ No newline at end of file diff --git a/lib/base/sample-configs/.env-sample-full b/lib/base/sample-configs/.env-sample-full index a99c26b3..0449d94a 100644 --- a/lib/base/sample-configs/.env-sample-full +++ b/lib/base/sample-configs/.env-sample-full @@ -9,7 +9,7 @@ AWS_REGION="us-east-1" ## Common configuration parameters ## BASE_NETWORK_ID="mainnet" # All options: "mainnet", "sepolia" BASE_NODE_CONFIGURATION="full" # All options: "full", "archive" -BASE_INSTANCE_TYPE="m7g.2xlarge" +BASE_INSTANCE_TYPE="m7g.2xlarge" # Reconneded for Insance Store: i3en.3xlarge, "x86_64" BASE_CPU_TYPE="ARM_64" # All options: "x86_64", "ARM_64". IMPORTANT: Make sure the CPU type matches the instance type used # Data volume configuration @@ -17,10 +17,10 @@ BASE_DATA_VOL_TYPE="gp3" # Other options: "io1" | "io2" | "gp3" | BASE_DATA_VOL_SIZE="4000" # Current required data size in GB to keep both snapshot archive and unarchived version of it. For Sepolia 1000 will be sufficient. BASE_DATA_VOL_IOPS="5000" # Max IOPS for EBS volumes (not applicable for "instance-store") BASE_DATA_VOL_THROUGHPUT="700" # Max throughput for EBS gp3 volumes (not applicable for "io1" | "io2" | "instance-store") -BASE_RESTORE_FROM_SNAPSHOT="true" # Download snapshot to speed up statup time BASE_L1_EXECUTION_ENDPOINT="https://ethereum-sepolia-rpc.publicnode.com" # Set your own URL to Ethereum L1 node: https://docs.base.org/tools/node-providers BASE_L1_CONSENSUS_ENDPOINT="https://ethereum-sepolia-beacon-api.publicnode.com" +BASE_RESTORE_FROM_SNAPSHOT="true" # Download snapshot to speed up statup time BASE_SNAPSHOT_URL="none" # Optionally provide the URL to download snpashot: https://docs.base.org/tutorials/run-a-base-node/#snapshots # Example for Sepolia: @@ -29,3 +29,8 @@ BASE_SNAPSHOT_URL="none" # Optionally provide the URL to download # Example for Mainnet and with Ethereum Blueprint with Geth-Lighthouse client combination and private IP: #BASE_L1_EXECUTION_ENDPOINT=http://172.31.15.220:8545 #BASE_L1_CONSENSUS_ENDPOINT=http://172.31.15.220:5052 + +## HA nodes configuration ## +BASE_HA_NUMBER_OF_NODES="2" # Total number of RPC nodes to be provisioned. Default: 2 +BASE_HA_ALB_HEALTHCHECK_GRACE_PERIOD_MIN="300" # Time enough to initialize the instance +BASE_HA_NODES_HEARTBEAT_DELAY_MIN="120" # Time sufficient enough for a node do sync \ No newline at end of file diff --git a/lib/base/single-archive-node-deploy.json b/lib/base/single-archive-node-deploy.json new file mode 100644 index 00000000..98a02002 --- /dev/null +++ b/lib/base/single-archive-node-deploy.json @@ -0,0 +1,5 @@ +{ + "base-single-node-archive-mainnet": { + "nodeinstanceid": "i-0206eadc184b36db5" + } +} diff --git a/lib/base/test/.env-test b/lib/base/test/.env-test index f9a14ccd..46c9f3f7 100644 --- a/lib/base/test/.env-test +++ b/lib/base/test/.env-test @@ -8,10 +8,13 @@ AWS_REGION="us-east-1" # Regions supported by Amazon Ma ## Common configuration parameters ## BASE_NETWORK_ID="mainnet" # All options: "mainnet" +BASE_NODE_CONFIGURATION="full" # All options: "full", "archive" +BASE_INSTANCE_TYPE="m7g.4xlarge" +BASE_CPU_TYPE="ARM_64" # All options: "x86_64", "ARM_64". IMPORTANT: Make sure the CPU type matches the instance type used # Data volume configuration BASE_DATA_VOL_TYPE="gp3" # Other options: "io1" | "io2" | "gp3" | "instance-store" . IMPORTANT: Use "instance-store" option only with instance types that support that feature, like popular for node im4gn, d3, i3en, and i4i instance families -BASE_DATA_VOL_SIZE="5100" # Current required data size in GB to keep both snapshot archive and unarchived version of it. For Sepolia 1000 will be sufficient. +BASE_DATA_VOL_SIZE="7200" # Current required data size in GB to keep both snapshot archive and unarchived version of it. For Sepolia 1000 will be sufficient. BASE_DATA_VOL_IOPS="5000" # Max IOPS for EBS volumes (not applicable for "instance-store") BASE_DATA_VOL_THROUGHPUT="700" # Max throughput for EBS gp3 volumes (not applicable for "io1" | "io2" | "instance-store") BASE_RESTORE_FROM_SNAPSHOT="true" # Download snapshot to speed up statup time @@ -24,3 +27,8 @@ BASE_L1_CONSENSUS_ENDPOINT=https://ethereum-sepolia-beacon-api.publicnode.com # Example for Mainnet and with Ethereum Blueprint with Geth-Lighthouse client combination and private IP: #BASE_L1_EXECUTION_ENDPOINT=http://172.31.15.220:8545 #BASE_L1_CONSENSUS_ENDPOINT=http://172.31.15.220:5052 + +## HA nodes configuration ## +BASE_HA_NUMBER_OF_NODES="2" # Total number of RPC nodes to be provisioned. Default: 2 +BASE_HA_ALB_HEALTHCHECK_GRACE_PERIOD_MIN="300" # Time enough to initialize the instance +BASE_HA_NODES_HEARTBEAT_DELAY_MIN="120" # Time sufficient enough for a node do sync \ No newline at end of file diff --git a/lib/base/test/base-single-node.test.ts b/lib/base/test/base-single-node.test.ts index e945f320..9cbc5a39 100644 --- a/lib/base/test/base-single-node.test.ts +++ b/lib/base/test/base-single-node.test.ts @@ -15,15 +15,17 @@ describe("BaseSingleNodeStack", () => { // Create the BaseSingleNodeStack. baseSingleNodeStack = new BaseSingleNodeStack(app, "base-single-node", { - stackName: `base-single-node-${config.baseNodeConfig.baseNetworkId}`, + stackName: `base-single-node-${config.baseNodeConfig.baseNodeConfiguration}-${config.baseNodeConfig.baseNetworkId}`, env: { account: config.baseConfig.accountId, region: config.baseConfig.region }, - + instanceType: config.baseNodeConfig.instanceType, instanceCpuType: config.baseNodeConfig.instanceCpuType, baseNetworkId: config.baseNodeConfig.baseNetworkId, + baseNodeConfiguration: config.baseNodeConfig.baseNodeConfiguration, restoreFromSnapshot: config.baseNodeConfig.restoreFromSnapshot, l1ExecutionEndpoint: config.baseNodeConfig.l1ExecutionEndpoint, l1ConsensusEndpoint: config.baseNodeConfig.l1ConsensusEndpoint, + snapshotUrl: config.baseNodeConfig.snapshotUrl, dataVolume: config.baseNodeConfig.dataVolume, }); @@ -111,7 +113,7 @@ describe("BaseSingleNodeStack", () => { ], IamInstanceProfile: Match.anyValue(), ImageId: Match.anyValue(), - InstanceType: "m7g.2xlarge", + InstanceType: "m7g.4xlarge", Monitoring: true, PropagateTagsToVolumeOnCreation: true, SecurityGroupIds: Match.anyValue(), @@ -124,7 +126,7 @@ describe("BaseSingleNodeStack", () => { Encrypted: true, Iops: 5000, MultiAttachEnabled: false, - Size: 5100, + Size: 7200, Throughput: 700, VolumeType: "gp3" }) @@ -145,7 +147,7 @@ describe("BaseSingleNodeStack", () => { "Fn::Join": [ "", [ - "base-single-node-mainnet-", + "base-single-node-full-mainnet-", { "Ref": Match.anyValue() } diff --git a/lib/base/test/ha-nodes-stack.test.ts b/lib/base/test/ha-nodes-stack.test.ts new file mode 100644 index 00000000..f7d4ee8c --- /dev/null +++ b/lib/base/test/ha-nodes-stack.test.ts @@ -0,0 +1,273 @@ +import { Match, Template } from "aws-cdk-lib/assertions"; +import * as cdk from "aws-cdk-lib"; +import * as dotenv from 'dotenv'; +dotenv.config({ path: './test/.env-test' }); +import * as config from "../lib/config/baseConfig"; +import * as configTypes from "../lib/config/baseConfig.interface"; +import { BaseHANodesStack } from "../lib/ha-nodes-stack"; + +describe("BaseHANodesStack", () => { + test("synthesizes the way we expect", () => { + const app = new cdk.App(); + + // Create the BaseHANodesStack. + const baseHANodesStack = new BaseHANodesStack(app, "base-sync-node", { + stackName: `base-ha-nodes-${config.baseNodeConfig.baseNodeConfiguration}-${config.baseNodeConfig.baseNetworkId}`, + env: { account: config.baseConfig.accountId, region: config.baseConfig.region }, + + instanceType: config.baseNodeConfig.instanceType, + instanceCpuType: config.baseNodeConfig.instanceCpuType, + baseNetworkId: config.baseNodeConfig.baseNetworkId, + baseNodeConfiguration: config.baseNodeConfig.baseNodeConfiguration, + restoreFromSnapshot: config.baseNodeConfig.restoreFromSnapshot, + l1ExecutionEndpoint: config.baseNodeConfig.l1ExecutionEndpoint, + l1ConsensusEndpoint: config.baseNodeConfig.l1ConsensusEndpoint, + snapshotUrl: config.baseNodeConfig.snapshotUrl, + dataVolume: config.baseNodeConfig.dataVolume, + + albHealthCheckGracePeriodMin: config.haNodeConfig.albHealthCheckGracePeriodMin, + heartBeatDelayMin: config.haNodeConfig.heartBeatDelayMin, + numberOfNodes: config.haNodeConfig.numberOfNodes + }); + + // Prepare the stack for assertions. + const template = Template.fromStack(baseHANodesStack); + + // Has EC2 instance security group. + template.hasResourceProperties("AWS::EC2::SecurityGroup", { + GroupDescription: Match.anyValue(), + VpcId: Match.anyValue(), + SecurityGroupEgress: [ + { + "CidrIp": "0.0.0.0/0", + "Description": "All outbound connections except 13000", + "FromPort": 0, + "IpProtocol": "tcp", + "ToPort": 12999 + }, + { + "CidrIp": "0.0.0.0/0", + "Description": "All outbound connections except 13000", + "FromPort": 13001, + "IpProtocol": "tcp", + "ToPort": 65535 + }, + { + "CidrIp": "0.0.0.0/0", + "Description": "All outbound connections except 13000", + "FromPort": 0, + "IpProtocol": "udp", + "ToPort": 12999 + }, + { + "CidrIp": "0.0.0.0/0", + "Description": "All outbound connections except 13000", + "FromPort": 13001, + "IpProtocol": "udp", + "ToPort": 65535 + } + ], + SecurityGroupIngress: [ + { + "CidrIp": "0.0.0.0/0", + "Description": "P2P", + "FromPort": 9222, + "IpProtocol": "tcp", + "ToPort": 9222 + }, + { + "CidrIp": "0.0.0.0/0", + "Description": "P2P", + "FromPort": 9222, + "IpProtocol": "udp", + "ToPort": 9222 + }, + { + "CidrIp": "1.2.3.4/5", + "Description": "Base Client RPC", + "FromPort": 8545, + "IpProtocol": "tcp", + "ToPort": 8545 + }, + { + "Description": "Allow access from ALB to Blockchain Node", + "FromPort": 0, + "IpProtocol": "tcp", + "SourceSecurityGroupId": { + "Fn::GetAtt": [ + Match.anyValue(), + "GroupId" + ] + }, + "ToPort": 65535 + }, + ] + }) + + // Has security group from ALB to EC2. + template.hasResourceProperties("AWS::EC2::SecurityGroupIngress", { + Description: "Load balancer to target", + FromPort: 8545, + GroupId: Match.anyValue(), + IpProtocol: "tcp", + SourceSecurityGroupId: Match.anyValue(), + ToPort: 8545, + }) + + // Has launch template profile for EC2 instances. + template.hasResourceProperties("AWS::IAM::InstanceProfile", { + Roles: [Match.anyValue()] + }); + + // Has EC2 launch template. + template.hasResourceProperties("AWS::EC2::LaunchTemplate", { + LaunchTemplateData: { + BlockDeviceMappings: [ + { + "DeviceName": "/dev/xvda", + "Ebs": { + "DeleteOnTermination": true, + "Encrypted": true, + "Iops": 3000, + "Throughput": 125, + "VolumeSize": 46, + "VolumeType": "gp3" + } + }, + { + "DeviceName": "/dev/sdf", + "Ebs": { + "DeleteOnTermination": true, + "Encrypted": true, + "Iops": 5000, + "Throughput": 700, + "VolumeSize": 7200, + "VolumeType": "gp3" + } + } + ], + EbsOptimized: true, + IamInstanceProfile: Match.anyValue(), + ImageId: Match.anyValue(), + InstanceType:"m7g.4xlarge", + SecurityGroupIds: [Match.anyValue()], + UserData: Match.anyValue(), + TagSpecifications: Match.anyValue(), + } + }) + + // Has Auto Scaling Group. + template.hasResourceProperties("AWS::AutoScaling::AutoScalingGroup", { + AutoScalingGroupName: `base-ha-nodes-${config.baseNodeConfig.baseNodeConfiguration}-${config.baseNodeConfig.baseNetworkId}`, + HealthCheckGracePeriod: config.haNodeConfig.albHealthCheckGracePeriodMin * 60, + HealthCheckType: "ELB", + DefaultInstanceWarmup: 60, + MinSize: "0", + MaxSize: "4", + DesiredCapacity: config.haNodeConfig.numberOfNodes.toString(), + VPCZoneIdentifier: Match.anyValue(), + TargetGroupARNs: Match.anyValue(), + }); + + // Has Auto Scaling Lifecycle Hook. + template.hasResourceProperties("AWS::AutoScaling::LifecycleHook", { + DefaultResult: "ABANDON", + HeartbeatTimeout: config.haNodeConfig.heartBeatDelayMin * 60, + LifecycleHookName: `base-ha-nodes-${config.baseNodeConfig.baseNodeConfiguration}-${config.baseNodeConfig.baseNetworkId}`, + LifecycleTransition: "autoscaling:EC2_INSTANCE_LAUNCHING", + }); + + // Has Auto Scaling Security Group. + template.hasResourceProperties("AWS::EC2::SecurityGroup", { + GroupDescription: Match.anyValue(), + SecurityGroupEgress: [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + SecurityGroupIngress: [ + { + "CidrIp": "1.2.3.4/5", + "Description": "Blockchain Node RPC", + "FromPort": 8545, + "IpProtocol": "tcp", + "ToPort": 8545 + } + ], + VpcId: Match.anyValue(), + }); + + // Has ALB. + template.hasResourceProperties("AWS::ElasticLoadBalancingV2::LoadBalancer", { + LoadBalancerAttributes: [ + { + Key: "deletion_protection.enabled", + Value: "false" + }, + { + Key: "access_logs.s3.enabled", + Value: "true" + }, + { + Key: "access_logs.s3.bucket", + Value: Match.anyValue(), + }, + { + Key: "access_logs.s3.prefix", + Value: `base-ha-nodes-${config.baseNodeConfig.baseNodeConfiguration}-${config.baseNodeConfig.baseNetworkId}` + } + ], + Scheme: "internal", + SecurityGroups: [ + Match.anyValue() + ], + "Subnets": [ + Match.anyValue(), + Match.anyValue() + ], + Type: "application", + }); + + // Has ALB listener. + template.hasResourceProperties("AWS::ElasticLoadBalancingV2::Listener", { + "DefaultActions": [ + { + "TargetGroupArn": Match.anyValue(), + Type: "forward" + } + ], + LoadBalancerArn: Match.anyValue(), + Port: 8545, + Protocol: "HTTP" + }) + + // Has ALB target group. + template.hasResourceProperties("AWS::ElasticLoadBalancingV2::TargetGroup", { + HealthCheckEnabled: true, + HealthCheckIntervalSeconds: 30, + HealthCheckPath: "/", + HealthCheckPort: "8545", + HealthyThresholdCount: 3, + Matcher: { + HttpCode: "200-299" + }, + Port: 8545, + Protocol: "HTTP", + TargetGroupAttributes: [ + { + Key: "deregistration_delay.timeout_seconds", + Value: "30" + }, + { + Key: "stickiness.enabled", + Value: "false" + } + ], + TargetType: "instance", + UnhealthyThresholdCount: 2, + VpcId: Match.anyValue(), + }) + }); +}); From 8f8e43384fe564cf05e0a3629fd016a133a901f7 Mon Sep 17 00:00:00 2001 From: Nikolay Vlasov Date: Mon, 27 May 2024 15:00:44 +1000 Subject: [PATCH 56/75] Base. Added alternative snapshot downloads from S3 URL. --- .../restore-from-snapshot-archive-s3.sh | 31 +++++++++++++++++++ ...pshot.sh => restore-from-snapshot-http.sh} | 1 - .../assets/setup-instance-store-volumes.sh | 9 ++++-- lib/base/lib/assets/user-data/node.sh | 23 +++++++++++--- lib/base/single-archive-node-deploy.json | 2 +- 5 files changed, 57 insertions(+), 9 deletions(-) create mode 100644 lib/base/lib/assets/restore-from-snapshot-archive-s3.sh rename lib/base/lib/assets/{restore-from-snapshot.sh => restore-from-snapshot-http.sh} (97%) diff --git a/lib/base/lib/assets/restore-from-snapshot-archive-s3.sh b/lib/base/lib/assets/restore-from-snapshot-archive-s3.sh new file mode 100644 index 00000000..16473ea6 --- /dev/null +++ b/lib/base/lib/assets/restore-from-snapshot-archive-s3.sh @@ -0,0 +1,31 @@ +#!/bin/bash +set +e + +source /etc/environment + +echo "Downloading Snapshot." + +cd /data + +SNAPSHOT_FILE_NAME=snapshot.tar.gz +SNAPSHOT_DIR=/data + +LATEST_SNAPSHOT_FILE_NAME=$(curl https://$NETWORK_ID-$NODE_CONFIG-snapshots.base.org/latest) && \ +s5cmd --log error cp s3://base-snapshots-$NETWORK_ID-archive/$LATEST_SNAPSHOT_FILE_NAME /data && \ +echo "Downloading Snapshot script finished" && \ +sleep 60 &&\ +echo "Starting snapshot decompression ..." && \ +tar -zxvf $SNAPSHOT_DIR/$SNAPSHOT_FILE_NAME -C /data 2>&1 | tee unzip.log && echo "decompresed successfully..." || echo "decompression failed..." >> snapshots-decompression.log + +echo "Decompresed snapshot, cleaning up..." + +mv /data/snapshots/$NETWORK_ID/download/* /data && \ +rm -rf /data/snapshots && \ +rm -rf $SNAPSHOT_DIR/$SNAPSHOT_FILE_NAME + +echo "Snapshot is ready, starting the service.." + +chown -R bcuser:bcuser /data + +sudo systemctl daemon-reload +sudo systemctl enable --now base \ No newline at end of file diff --git a/lib/base/lib/assets/restore-from-snapshot.sh b/lib/base/lib/assets/restore-from-snapshot-http.sh similarity index 97% rename from lib/base/lib/assets/restore-from-snapshot.sh rename to lib/base/lib/assets/restore-from-snapshot-http.sh index c73bb927..018974eb 100644 --- a/lib/base/lib/assets/restore-from-snapshot.sh +++ b/lib/base/lib/assets/restore-from-snapshot-http.sh @@ -16,7 +16,6 @@ if [ "$SNAPSHOT_URL" == "none" ] || [ -z "${SNAPSHOT_URL}" ]; then SNAPSHOT_URL=https://$NETWORK_ID-$NODE_CONFIG-snapshots.base.org/$LATEST_SNAPSHOT_FILE_NAME fi -# take about 1 hour to download the Snapshot while (( SNAPSHOT_DOWNLOAD_STATUS != 0 )) do PIDS=$(pgrep aria2c) diff --git a/lib/base/lib/assets/setup-instance-store-volumes.sh b/lib/base/lib/assets/setup-instance-store-volumes.sh index 934d19fb..0b919b61 100644 --- a/lib/base/lib/assets/setup-instance-store-volumes.sh +++ b/lib/base/lib/assets/setup-instance-store-volumes.sh @@ -13,8 +13,13 @@ if [ -n "$DATA_VOLUME_ID" ]; then mkfs.ext4 $DATA_VOLUME_ID echo "Data volume formatted. Mounting..." - # Waiting wihtouht using sleep as it sometimes just hangs.... - coproc read -t 10 && wait "$!" || true + echo "waiting for volume to get UUID" + OUTPUT=0; + while [ "$OUTPUT" = 0 ]; do + DATA_VOLUME_UUID=$(lsblk -fn -o UUID $DATA_VOLUME_ID) + OUTPUT=$(echo $DATA_VOLUME_UUID | grep -c - $2) + echo $OUTPUT + done DATA_VOLUME_UUID=$(lsblk -fn -o UUID $DATA_VOLUME_ID) DATA_VOLUME_FSTAB_CONF="UUID=$DATA_VOLUME_UUID /data ext4 defaults 0 2" echo "DATA_VOLUME_ID="$DATA_VOLUME_ID diff --git a/lib/base/lib/assets/user-data/node.sh b/lib/base/lib/assets/user-data/node.sh index 9c6b46f5..99575bae 100644 --- a/lib/base/lib/assets/user-data/node.sh +++ b/lib/base/lib/assets/user-data/node.sh @@ -35,7 +35,7 @@ yum update -y yum -y install amazon-cloudwatch-agent collectd jq yq gcc ncurses-devel aws-cfn-bootstrap zstd wget $YQ_URI -O /usr/bin/yq && chmod +x /usr/bin/yq -# install aria2 a p2p downloader +echo " Installing aria2 a p2p downloader" cd /tmp if [ "$arch" == "x86_64" ]; then @@ -50,6 +50,14 @@ else make install fi +echo " Installing s5cmd" +cd /opt +wget -q $S5CMD_URI -O s5cmd.tar.gz +tar -xf s5cmd.tar.gz +chmod +x s5cmd +mv s5cmd /usr/bin +s5cmd version + cd /opt echo "Downloading assets zip file" @@ -83,7 +91,6 @@ yum install -y $SSM_AGENT_BINARY_URI REGION=${_REGION_} STACK_NAME=${_STACK_NAME_} RESTORE_FROM_SNAPSHOT=${_RESTORE_FROM_SNAPSHOT_} -FORMAT_DISK=${_FORMAT_DISK_} NETWORK_ID=${_NETWORK_ID_} NODE_CONFIG=${_NODE_CONFIG_} L1_EXECUTION_ENDPOINT=${_L1_EXECUTION_ENDPOINT_} @@ -290,9 +297,15 @@ if [ "$RESTORE_FROM_SNAPSHOT" == "false" ]; then systemctl daemon-reload systemctl enable --now base else - echo "Restoring node from snapshot" - chmod +x /opt/restore-from-snapshot.sh - echo "/opt/restore-from-snapshot.sh" | at now + 1 min + if [ "$NODE_CONFIG" == "archive" ]; then + echo "Restoring archive node from snapshot over s3" + chmod +x /opt/restore-from-snapshot-archive-s3.sh + echo "/opt/restore-from-snapshot-archive-s3.sh" | at now + 1 min + else + echo "Restoring full node from snapshot over http" + chmod +x /opt/restore-from-snapshot-http.sh + echo "/opt/restore-from-snapshot-http.sh" | at now + 1 min + fi fi if [[ "$LIFECYCLE_HOOK_NAME" != "none" ]]; then diff --git a/lib/base/single-archive-node-deploy.json b/lib/base/single-archive-node-deploy.json index 98a02002..800bf5ad 100644 --- a/lib/base/single-archive-node-deploy.json +++ b/lib/base/single-archive-node-deploy.json @@ -1,5 +1,5 @@ { "base-single-node-archive-mainnet": { - "nodeinstanceid": "i-0206eadc184b36db5" + "nodeinstanceid": "i-03efb8b36f80c2fd1" } } From e3e5dca5227d57d2baeb9c4d8ee27f25d72d71d7 Mon Sep 17 00:00:00 2001 From: Nikolay Vlasov Date: Tue, 28 May 2024 13:45:48 +1000 Subject: [PATCH 57/75] Base. Refactoring service start stop configuration --- lib/base/lib/assets/base/{node.sh => node-start.sh} | 0 lib/base/lib/assets/base/node-stop.sh | 9 +++++++++ lib/base/lib/assets/user-data/node.sh | 8 +++++--- 3 files changed, 14 insertions(+), 3 deletions(-) rename lib/base/lib/assets/base/{node.sh => node-start.sh} (100%) create mode 100644 lib/base/lib/assets/base/node-stop.sh diff --git a/lib/base/lib/assets/base/node.sh b/lib/base/lib/assets/base/node-start.sh similarity index 100% rename from lib/base/lib/assets/base/node.sh rename to lib/base/lib/assets/base/node-start.sh diff --git a/lib/base/lib/assets/base/node-stop.sh b/lib/base/lib/assets/base/node-stop.sh new file mode 100644 index 00000000..9ec0285f --- /dev/null +++ b/lib/base/lib/assets/base/node-stop.sh @@ -0,0 +1,9 @@ +#!/bin/bash +set -e +echo "Script is stopping the node..." + +# Stop the node +cd /home/bcuser/node +/usr/local/bin/docker-compose -f /home/bcuser/node/docker-compose.yml down + +echo "Stopped" \ No newline at end of file diff --git a/lib/base/lib/assets/user-data/node.sh b/lib/base/lib/assets/user-data/node.sh index 99575bae..90af651a 100644 --- a/lib/base/lib/assets/user-data/node.sh +++ b/lib/base/lib/assets/user-data/node.sh @@ -226,8 +226,9 @@ crontab -l echo "Configuring node as a service" mkdir /home/bcuser/bin -mv /opt/base/node.sh /home/bcuser/bin/node.sh -chmod 766 /home/bcuser/bin/node.sh +mv /opt/base/node-start.sh /home/bcuser/bin/node-start.sh +mv /opt/base/node-stop.sh /home/bcuser/bin/node-stop.sh +chmod 766 /home/bcuser/bin/* chown -R bcuser:bcuser /home/bcuser sudo bash -c 'cat > /etc/systemd/system/base.service < Date: Wed, 29 May 2024 11:33:28 +1000 Subject: [PATCH 58/75] Base. Refactoring restoration of Archive nodes from snapshots --- lib/base/lib/assets/restore-from-snapshot-archive-s3.sh | 6 +++--- lib/base/lib/assets/user-data/node.sh | 3 ++- lib/base/sample-configs/.env-sample-archive | 2 +- lib/base/single-archive-node-deploy.json | 2 +- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/lib/base/lib/assets/restore-from-snapshot-archive-s3.sh b/lib/base/lib/assets/restore-from-snapshot-archive-s3.sh index 16473ea6..a85cd044 100644 --- a/lib/base/lib/assets/restore-from-snapshot-archive-s3.sh +++ b/lib/base/lib/assets/restore-from-snapshot-archive-s3.sh @@ -11,7 +11,7 @@ SNAPSHOT_FILE_NAME=snapshot.tar.gz SNAPSHOT_DIR=/data LATEST_SNAPSHOT_FILE_NAME=$(curl https://$NETWORK_ID-$NODE_CONFIG-snapshots.base.org/latest) && \ -s5cmd --log error cp s3://base-snapshots-$NETWORK_ID-archive/$LATEST_SNAPSHOT_FILE_NAME /data && \ +s5cmd --log error cp s3://base-snapshots-$NETWORK_ID-archive/$LATEST_SNAPSHOT_FILE_NAME $SNAPSHOT_DIR/$SNAPSHOT_FILE_NAME && \ echo "Downloading Snapshot script finished" && \ sleep 60 &&\ echo "Starting snapshot decompression ..." && \ @@ -19,13 +19,13 @@ tar -zxvf $SNAPSHOT_DIR/$SNAPSHOT_FILE_NAME -C /data 2>&1 | tee unzip.log && ec echo "Decompresed snapshot, cleaning up..." -mv /data/snapshots/$NETWORK_ID/download/* /data && \ +mv /data/snapshots/$NETWORK_ID/download/* $SNAPSHOT_DIR && \ rm -rf /data/snapshots && \ rm -rf $SNAPSHOT_DIR/$SNAPSHOT_FILE_NAME echo "Snapshot is ready, starting the service.." -chown -R bcuser:bcuser /data +chown -R bcuser:bcuser $SNAPSHOT_DIR sudo systemctl daemon-reload sudo systemctl enable --now base \ No newline at end of file diff --git a/lib/base/lib/assets/user-data/node.sh b/lib/base/lib/assets/user-data/node.sh index 90af651a..02e832a8 100644 --- a/lib/base/lib/assets/user-data/node.sh +++ b/lib/base/lib/assets/user-data/node.sh @@ -7,11 +7,13 @@ AUTOSCALING_GROUP_NAME=${_AUTOSCALING_GROUP_NAME_} RESOURCE_ID=${_NODE_CF_LOGICAL_ID_} ASSETS_S3_PATH=${_ASSETS_S3_PATH_} DATA_VOLUME_TYPE=${_DATA_VOLUME_TYPE_} +DATA_VOLUME_SIZE=${_DATA_VOLUME_SIZE_} { echo "LIFECYCLE_HOOK_NAME=$LIFECYCLE_HOOK_NAME" echo "AUTOSCALING_GROUP_NAME=$AUTOSCALING_GROUP_NAME" echo "ASSETS_S3_PATH=$ASSETS_S3_PATH" echo "DATA_VOLUME_TYPE=$DATA_VOLUME_TYPE" + echo "DATA_VOLUME_SIZE=$DATA_VOLUME_SIZE" } >> /etc/environment arch=$(uname -m) @@ -242,7 +244,6 @@ RestartSec=30 User=bcuser Environment="PATH=/bin:/usr/bin:/home/bcuser/bin" ExecStart=/home/bcuser/bin/node-start.sh -ExecStop=/home/bcuser/bin/node-stop.sh [Install] WantedBy=multi-user.target EOF' diff --git a/lib/base/sample-configs/.env-sample-archive b/lib/base/sample-configs/.env-sample-archive index b8f8efbd..1c9b078e 100644 --- a/lib/base/sample-configs/.env-sample-archive +++ b/lib/base/sample-configs/.env-sample-archive @@ -14,7 +14,7 @@ BASE_CPU_TYPE="ARM_64" # All options: "x86_64", "ARM_64". IMPORT # Data volume configuration BASE_DATA_VOL_TYPE="gp3" # Other options: "io1" | "io2" | "gp3" | "instance-store" . IMPORTANT: Use "instance-store" option only with instance types that support that feature, like popular for node im4gn, d3, i3en, and i4i instance families -BASE_DATA_VOL_SIZE="7200" # Current required data size in GB to keep both snapshot archive and unarchived version of it. For Sepolia 1000 will be sufficient. +BASE_DATA_VOL_SIZE="1000" # Current required data size in GB to keep both snapshot archive and unarchived version of it. For Sepolia 1000 will be sufficient. BASE_DATA_VOL_IOPS="5000" # Max IOPS for EBS volumes (not applicable for "instance-store") BASE_DATA_VOL_THROUGHPUT="700" # Max throughput for EBS gp3 volumes (not applicable for "io1" | "io2" | "instance-store") BASE_L1_EXECUTION_ENDPOINT="https://ethereum-sepolia-rpc.publicnode.com" # Set your own URL to Ethereum L1 node: https://docs.base.org/tools/node-providers diff --git a/lib/base/single-archive-node-deploy.json b/lib/base/single-archive-node-deploy.json index 800bf5ad..ff4abbd0 100644 --- a/lib/base/single-archive-node-deploy.json +++ b/lib/base/single-archive-node-deploy.json @@ -1,5 +1,5 @@ { "base-single-node-archive-mainnet": { - "nodeinstanceid": "i-03efb8b36f80c2fd1" + "nodeinstanceid": "i-09785d994a9adcadd" } } From b11dd083dd2da3cf82da0402b7053ce80ec8b864 Mon Sep 17 00:00:00 2001 From: Nikolay Vlasov Date: Wed, 12 Jun 2024 10:20:29 +1000 Subject: [PATCH 59/75] Base. Fixes in handling snapshot download from Cloudflare --- lib/base/README.md | 2 +- lib/base/lib/assets/user-data/node.sh | 12 +++--------- lib/base/single-archive-node-deploy.json | 5 ----- 3 files changed, 4 insertions(+), 15 deletions(-) delete mode 100644 lib/base/single-archive-node-deploy.json diff --git a/lib/base/README.md b/lib/base/README.md index a54ea52a..527f39c1 100644 --- a/lib/base/README.md +++ b/lib/base/README.md @@ -132,7 +132,7 @@ BASE_L1_EXECUTION_ENDPOINT="https://ethereum-sepolia-rpc.publicnode.com" BASE_L1_CONSENSUS_ENDPOINT="https://ethereum-sepolia-beacon-api.publicnode.com" ``` -2. Deploy Base RPC Node and wait for it to sync. For Mainnet it might a day when using snapshots or about a week if syncing from block 0. You can use snapshots provided by the Base team by setting `BASE_RESTORE_FROM_SNAPSHOT="true"` in `.env` file. +2. Deploy Base RPC Node and wait for it to sync. For Full node on Mainnet it might take a day when using snapshots or about a week if syncing from block 0. You can use snapshots provided by the Base team by setting `BASE_RESTORE_FROM_SNAPSHOT="true"` in `.env` file. ```bash pwd diff --git a/lib/base/lib/assets/user-data/node.sh b/lib/base/lib/assets/user-data/node.sh index 02e832a8..9d0a0059 100644 --- a/lib/base/lib/assets/user-data/node.sh +++ b/lib/base/lib/assets/user-data/node.sh @@ -300,15 +300,9 @@ if [ "$RESTORE_FROM_SNAPSHOT" == "false" ]; then systemctl daemon-reload systemctl enable --now base else - if [ "$NODE_CONFIG" == "archive" ]; then - echo "Restoring archive node from snapshot over s3" - chmod +x /opt/restore-from-snapshot-archive-s3.sh - echo "/opt/restore-from-snapshot-archive-s3.sh" | at now + 1 min - else - echo "Restoring full node from snapshot over http" - chmod +x /opt/restore-from-snapshot-http.sh - echo "/opt/restore-from-snapshot-http.sh" | at now + 1 min - fi + echo "Restoring full node from snapshot over http" + chmod +x /opt/restore-from-snapshot-http.sh + echo "/opt/restore-from-snapshot-http.sh" | at now + 1 min fi if [[ "$LIFECYCLE_HOOK_NAME" != "none" ]]; then diff --git a/lib/base/single-archive-node-deploy.json b/lib/base/single-archive-node-deploy.json deleted file mode 100644 index ff4abbd0..00000000 --- a/lib/base/single-archive-node-deploy.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "base-single-node-archive-mainnet": { - "nodeinstanceid": "i-09785d994a9adcadd" - } -} From 7f3eb45b6821f7da23c908e0f4383ccd22cd4391 Mon Sep 17 00:00:00 2001 From: Nikolay Vlasov Date: Mon, 17 Jun 2024 14:14:30 +1000 Subject: [PATCH 60/75] Base. Pre-review fixes --- lib/base/README.md | 6 ++-- lib/base/lib/assets/base/node-start.sh | 2 +- lib/base/lib/assets/base/node-stop.sh | 2 +- .../restore-from-snapshot-archive-s3.sh | 2 +- .../lib/assets/restore-from-snapshot-http.sh | 2 +- .../assets/setup-instance-store-volumes.sh | 4 +-- lib/base/lib/assets/user-data/node.sh | 8 ++--- lib/base/lib/config/baseConfig.interface.ts | 2 +- lib/base/sample-configs/.env-sample-archive | 2 +- lib/base/sample-configs/.env-sample-full | 4 +-- lib/base/sample-configs/.env-sample-full-ha | 36 +++++++++++++++++++ lib/base/test/.env-test | 2 +- lib/base/test/base-single-node.test.ts | 2 +- lib/base/test/ha-nodes-stack.test.ts | 4 +-- website/docs/Blueprints/Base.md | 2 +- 15 files changed, 59 insertions(+), 21 deletions(-) create mode 100644 lib/base/sample-configs/.env-sample-full-ha diff --git a/lib/base/README.md b/lib/base/README.md index 527f39c1..796c452d 100644 --- a/lib/base/README.md +++ b/lib/base/README.md @@ -124,7 +124,7 @@ npx cdk deploy base-common ### Option 1: Deploy Single Node -1. For L1 node you you can set your own URLs in `BASE_L1_EXECUTION_ENDPOINT` and `BASE_L1_CONSENSUS_ENDPOINT` properties of `.env` file. It can be one of [the providers recommended by Base](https://docs.base.org/tools/node-providers) or you can run your own Ethereum node [with Node Runner Ethereum blueprint](https://aws-samples.github.io/aws-blockchain-node-runners/docs/Blueprints/Ethereum) (tested with geth-lighthouse combination). For example: +1. For L1 node you you can set your own URLs in `BASE_L1_EXECUTION_ENDPOINT` and `BASE_L1_CONSENSUS_ENDPOINT` properties of `.env` file. It can be one of [the providers recommended by Base](https://docs.base.org/tools/node-providers) or you can run your own Ethereum node [with Node Runner Ethereum blueprint](https://aws-samples.github.io/aws-blockchain-node-runners/docs/Blueprints/Ethereum) (tested with single-node geth-lighthouse combination). For example: ```bash #For Sepolia: @@ -181,7 +181,7 @@ BASE_L1_CONSENSUS_ENDPOINT="https://ethereum-sepolia-beacon-api.publicnode.com" npx cdk deploy base-ha-nodes --json --outputs-file ha-nodes-deploy.json ``` -2. Give the new RPC **full** nodes about 2-3 hours (24 hours for **archive** node) to initialize and then run the following query against the load balancer behind the RPC node created +2. Give the new RPC **full** nodes about 5 hours to initialize and then run the following query against the load balancer behind the RPC node created. ```bash export RPC_ALB_URL=$(cat ha-nodes-deploy.json | jq -r '..|.alburl? | select(. != null)') @@ -197,6 +197,8 @@ BASE_L1_CONSENSUS_ENDPOINT="https://ethereum-sepolia-beacon-api.publicnode.com" **NOTE:** By default and for security reasons the load balancer is available only from within the default VPC in the region where it is deployed. It is not available from the Internet and is not open for external connections. Before opening it up please make sure you protect your RPC APIs. +**NOTE:** We currently don't recommend running **archive** nodes in HA setup, because it takes way too long to get them synced. Use single-node setup instead. + ### Monitoring Every 5 minutes a script on the Base node publishes to CloudWatch service the metrics for current block for L1/L2 clients as well as blocks behind metric for L1 and minutes behind for L2. When the node is fully synced the blocks behind metric should get to 4 and minutes behind should get down to 0. To see the metrics for **single node only**: diff --git a/lib/base/lib/assets/base/node-start.sh b/lib/base/lib/assets/base/node-start.sh index 866c7fcd..728a7db3 100644 --- a/lib/base/lib/assets/base/node-start.sh +++ b/lib/base/lib/assets/base/node-start.sh @@ -6,4 +6,4 @@ echo "Script is starting..." cd /home/bcuser/node /usr/local/bin/docker-compose -f /home/bcuser/node/docker-compose.yml up -d -echo "Started" \ No newline at end of file +echo "Started" diff --git a/lib/base/lib/assets/base/node-stop.sh b/lib/base/lib/assets/base/node-stop.sh index 9ec0285f..5a4d392d 100644 --- a/lib/base/lib/assets/base/node-stop.sh +++ b/lib/base/lib/assets/base/node-stop.sh @@ -6,4 +6,4 @@ echo "Script is stopping the node..." cd /home/bcuser/node /usr/local/bin/docker-compose -f /home/bcuser/node/docker-compose.yml down -echo "Stopped" \ No newline at end of file +echo "Stopped" diff --git a/lib/base/lib/assets/restore-from-snapshot-archive-s3.sh b/lib/base/lib/assets/restore-from-snapshot-archive-s3.sh index a85cd044..6bafba14 100644 --- a/lib/base/lib/assets/restore-from-snapshot-archive-s3.sh +++ b/lib/base/lib/assets/restore-from-snapshot-archive-s3.sh @@ -28,4 +28,4 @@ echo "Snapshot is ready, starting the service.." chown -R bcuser:bcuser $SNAPSHOT_DIR sudo systemctl daemon-reload -sudo systemctl enable --now base \ No newline at end of file +sudo systemctl enable --now base diff --git a/lib/base/lib/assets/restore-from-snapshot-http.sh b/lib/base/lib/assets/restore-from-snapshot-http.sh index 018974eb..09714d7e 100644 --- a/lib/base/lib/assets/restore-from-snapshot-http.sh +++ b/lib/base/lib/assets/restore-from-snapshot-http.sh @@ -59,4 +59,4 @@ echo "Snapshot is ready, starting the service.." chown -R bcuser:bcuser /data sudo systemctl daemon-reload -sudo systemctl enable --now base \ No newline at end of file +sudo systemctl enable --now base diff --git a/lib/base/lib/assets/setup-instance-store-volumes.sh b/lib/base/lib/assets/setup-instance-store-volumes.sh index 0b919b61..ee2825d7 100644 --- a/lib/base/lib/assets/setup-instance-store-volumes.sh +++ b/lib/base/lib/assets/setup-instance-store-volumes.sh @@ -15,7 +15,7 @@ if [ -n "$DATA_VOLUME_ID" ]; then echo "Data volume formatted. Mounting..." echo "waiting for volume to get UUID" OUTPUT=0; - while [ "$OUTPUT" = 0 ]; do + while [ "$OUTPUT" = 0 ]; do DATA_VOLUME_UUID=$(lsblk -fn -o UUID $DATA_VOLUME_ID) OUTPUT=$(echo $DATA_VOLUME_UUID | grep -c - $2) echo $OUTPUT @@ -41,4 +41,4 @@ if [ -n "$DATA_VOLUME_ID" ]; then else echo "Data volume is mounted, nothing changed" fi -fi \ No newline at end of file +fi diff --git a/lib/base/lib/assets/user-data/node.sh b/lib/base/lib/assets/user-data/node.sh index 9d0a0059..ef488978 100644 --- a/lib/base/lib/assets/user-data/node.sh +++ b/lib/base/lib/assets/user-data/node.sh @@ -99,7 +99,7 @@ L1_EXECUTION_ENDPOINT=${_L1_EXECUTION_ENDPOINT_} L1_CONSENSUS_ENDPOINT=${_L1_CONSENSUS_ENDPOINT_} SNAPSHOT_URL=${_SNAPSHOT_URL_} -{ +{ echo "REGION=$REGION" echo "NETWORK_ID=$NETWORK_ID" echo "NODE_CONFIG=$NODE_CONFIG" @@ -278,7 +278,7 @@ fi mkfs -t ext4 $DATA_VOLUME_ID echo "waiting for volume to get UUID" OUTPUT=0; - while [ "$OUTPUT" = 0 ]; do + while [ "$OUTPUT" = 0 ]; do DATA_VOLUME_UUID=$(lsblk -fn -o UUID $DATA_VOLUME_ID) OUTPUT=$(echo $DATA_VOLUME_UUID | grep -c - $2) echo $OUTPUT @@ -309,8 +309,8 @@ if [[ "$LIFECYCLE_HOOK_NAME" != "none" ]]; then echo "Signaling ASG lifecycle hook to complete" TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600") INSTANCE_ID=$(curl -H "X-aws-ec2-metadata-token: $TOKEN" -s http://169.254.169.254/latest/meta-data/instance-id) - aws autoscaling complete-lifecycle-action --lifecycle-action-result CONTINUE --instance-id $INSTANCE_ID --lifecycle-hook-name "$LIFECYCLE_HOOK_NAME" --auto-scaling-group-name "$AUTOSCALING_GROUP_NAME" --region $AWS_REGION + aws autoscaling complete-lifecycle-action --lifecycle-action-result CONTINUE --instance-id $INSTANCE_ID --lifecycle-hook-name "$LIFECYCLE_HOOK_NAME" --auto-scaling-group-name "$AUTOSCALING_GROUP_NAME" --region $REGION fi echo "All Done!!" -set -e \ No newline at end of file +set -e diff --git a/lib/base/lib/config/baseConfig.interface.ts b/lib/base/lib/config/baseConfig.interface.ts index dc2f2300..b6d0fc3f 100644 --- a/lib/base/lib/config/baseConfig.interface.ts +++ b/lib/base/lib/config/baseConfig.interface.ts @@ -28,4 +28,4 @@ export interface BaseHAConfig { albHealthCheckGracePeriodMin: number; heartBeatDelayMin: number; numberOfNodes: number; -} \ No newline at end of file +} diff --git a/lib/base/sample-configs/.env-sample-archive b/lib/base/sample-configs/.env-sample-archive index 1c9b078e..eeb088a5 100644 --- a/lib/base/sample-configs/.env-sample-archive +++ b/lib/base/sample-configs/.env-sample-archive @@ -33,4 +33,4 @@ BASE_SNAPSHOT_URL="none" # Optionally provide the URL to download ## HA nodes configuration ## BASE_HA_NUMBER_OF_NODES="2" # Total number of RPC nodes to be provisioned. Default: 2 BASE_HA_ALB_HEALTHCHECK_GRACE_PERIOD_MIN="300" # Time enough to initialize the instance -BASE_HA_NODES_HEARTBEAT_DELAY_MIN="1440" # Time sufficient enough for a node do sync \ No newline at end of file +BASE_HA_NODES_HEARTBEAT_DELAY_MIN="1440" # Time sufficient enough for a node do sync diff --git a/lib/base/sample-configs/.env-sample-full b/lib/base/sample-configs/.env-sample-full index 0449d94a..ae78c77b 100644 --- a/lib/base/sample-configs/.env-sample-full +++ b/lib/base/sample-configs/.env-sample-full @@ -32,5 +32,5 @@ BASE_SNAPSHOT_URL="none" # Optionally provide the URL to download ## HA nodes configuration ## BASE_HA_NUMBER_OF_NODES="2" # Total number of RPC nodes to be provisioned. Default: 2 -BASE_HA_ALB_HEALTHCHECK_GRACE_PERIOD_MIN="300" # Time enough to initialize the instance -BASE_HA_NODES_HEARTBEAT_DELAY_MIN="120" # Time sufficient enough for a node do sync \ No newline at end of file +BASE_HA_ALB_HEALTHCHECK_GRACE_PERIOD_MIN="500" # Time enough to initialize the instance +BASE_HA_NODES_HEARTBEAT_DELAY_MIN="120" # Time sufficient enough for a node do sync diff --git a/lib/base/sample-configs/.env-sample-full-ha b/lib/base/sample-configs/.env-sample-full-ha new file mode 100644 index 00000000..ae78c77b --- /dev/null +++ b/lib/base/sample-configs/.env-sample-full-ha @@ -0,0 +1,36 @@ +############################################################# +# Example configuration for Base nodes runner app on AWS # +############################################################# + +## Set the AWS account is and region for your environment ## +AWS_ACCOUNT_ID="xxxxxxxx" +AWS_REGION="us-east-1" + +## Common configuration parameters ## +BASE_NETWORK_ID="mainnet" # All options: "mainnet", "sepolia" +BASE_NODE_CONFIGURATION="full" # All options: "full", "archive" +BASE_INSTANCE_TYPE="m7g.2xlarge" # Reconneded for Insance Store: i3en.3xlarge, "x86_64" +BASE_CPU_TYPE="ARM_64" # All options: "x86_64", "ARM_64". IMPORTANT: Make sure the CPU type matches the instance type used + +# Data volume configuration +BASE_DATA_VOL_TYPE="gp3" # Other options: "io1" | "io2" | "gp3" | "instance-store" . IMPORTANT: Use "instance-store" option only with instance types that support that feature, like popular for node im4gn, d3, i3en, and i4i instance families +BASE_DATA_VOL_SIZE="4000" # Current required data size in GB to keep both snapshot archive and unarchived version of it. For Sepolia 1000 will be sufficient. +BASE_DATA_VOL_IOPS="5000" # Max IOPS for EBS volumes (not applicable for "instance-store") +BASE_DATA_VOL_THROUGHPUT="700" # Max throughput for EBS gp3 volumes (not applicable for "io1" | "io2" | "instance-store") +BASE_L1_EXECUTION_ENDPOINT="https://ethereum-sepolia-rpc.publicnode.com" # Set your own URL to Ethereum L1 node: https://docs.base.org/tools/node-providers +BASE_L1_CONSENSUS_ENDPOINT="https://ethereum-sepolia-beacon-api.publicnode.com" + +BASE_RESTORE_FROM_SNAPSHOT="true" # Download snapshot to speed up statup time +BASE_SNAPSHOT_URL="none" # Optionally provide the URL to download snpashot: https://docs.base.org/tutorials/run-a-base-node/#snapshots + +# Example for Sepolia: +#BASE_L1_EXECUTION_ENDPOINT=https://ethereum-sepolia-rpc.publicnode.com +#BASE_L1_CONSENSUS_ENDPOINT=https://ethereum-sepolia-beacon-api.publicnode.com +# Example for Mainnet and with Ethereum Blueprint with Geth-Lighthouse client combination and private IP: +#BASE_L1_EXECUTION_ENDPOINT=http://172.31.15.220:8545 +#BASE_L1_CONSENSUS_ENDPOINT=http://172.31.15.220:5052 + +## HA nodes configuration ## +BASE_HA_NUMBER_OF_NODES="2" # Total number of RPC nodes to be provisioned. Default: 2 +BASE_HA_ALB_HEALTHCHECK_GRACE_PERIOD_MIN="500" # Time enough to initialize the instance +BASE_HA_NODES_HEARTBEAT_DELAY_MIN="120" # Time sufficient enough for a node do sync diff --git a/lib/base/test/.env-test b/lib/base/test/.env-test index 46c9f3f7..2796c5d3 100644 --- a/lib/base/test/.env-test +++ b/lib/base/test/.env-test @@ -31,4 +31,4 @@ BASE_L1_CONSENSUS_ENDPOINT=https://ethereum-sepolia-beacon-api.publicnode.com ## HA nodes configuration ## BASE_HA_NUMBER_OF_NODES="2" # Total number of RPC nodes to be provisioned. Default: 2 BASE_HA_ALB_HEALTHCHECK_GRACE_PERIOD_MIN="300" # Time enough to initialize the instance -BASE_HA_NODES_HEARTBEAT_DELAY_MIN="120" # Time sufficient enough for a node do sync \ No newline at end of file +BASE_HA_NODES_HEARTBEAT_DELAY_MIN="120" # Time sufficient enough for a node do sync diff --git a/lib/base/test/base-single-node.test.ts b/lib/base/test/base-single-node.test.ts index 9cbc5a39..809efc53 100644 --- a/lib/base/test/base-single-node.test.ts +++ b/lib/base/test/base-single-node.test.ts @@ -17,7 +17,7 @@ describe("BaseSingleNodeStack", () => { baseSingleNodeStack = new BaseSingleNodeStack(app, "base-single-node", { stackName: `base-single-node-${config.baseNodeConfig.baseNodeConfiguration}-${config.baseNodeConfig.baseNetworkId}`, env: { account: config.baseConfig.accountId, region: config.baseConfig.region }, - + instanceType: config.baseNodeConfig.instanceType, instanceCpuType: config.baseNodeConfig.instanceCpuType, baseNetworkId: config.baseNodeConfig.baseNetworkId, diff --git a/lib/base/test/ha-nodes-stack.test.ts b/lib/base/test/ha-nodes-stack.test.ts index f7d4ee8c..09a2473e 100644 --- a/lib/base/test/ha-nodes-stack.test.ts +++ b/lib/base/test/ha-nodes-stack.test.ts @@ -14,7 +14,7 @@ describe("BaseHANodesStack", () => { const baseHANodesStack = new BaseHANodesStack(app, "base-sync-node", { stackName: `base-ha-nodes-${config.baseNodeConfig.baseNodeConfiguration}-${config.baseNodeConfig.baseNetworkId}`, env: { account: config.baseConfig.accountId, region: config.baseConfig.region }, - + instanceType: config.baseNodeConfig.instanceType, instanceCpuType: config.baseNodeConfig.instanceCpuType, baseNetworkId: config.baseNodeConfig.baseNetworkId, @@ -24,7 +24,7 @@ describe("BaseHANodesStack", () => { l1ConsensusEndpoint: config.baseNodeConfig.l1ConsensusEndpoint, snapshotUrl: config.baseNodeConfig.snapshotUrl, dataVolume: config.baseNodeConfig.dataVolume, - + albHealthCheckGracePeriodMin: config.haNodeConfig.albHealthCheckGracePeriodMin, heartBeatDelayMin: config.haNodeConfig.heartBeatDelayMin, numberOfNodes: config.haNodeConfig.numberOfNodes diff --git a/website/docs/Blueprints/Base.md b/website/docs/Blueprints/Base.md index 833b66c4..042bf851 100644 --- a/website/docs/Blueprints/Base.md +++ b/website/docs/Blueprints/Base.md @@ -1,5 +1,5 @@ --- -sidebar_position: 6 +sidebar_position: 10 sidebar_label: Base --- # From 4e53fa87caf2601d1693437d428f9899f492fcb9 Mon Sep 17 00:00:00 2001 From: Nikolay Vlasov Date: Thu, 8 Aug 2024 12:24:00 -0700 Subject: [PATCH 61/75] Base. Changed Cloud9 to CloudShell in README --- lib/base/README.md | 37 ++++++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/lib/base/README.md b/lib/base/README.md index 796c452d..6c381915 100644 --- a/lib/base/README.md +++ b/lib/base/README.md @@ -68,9 +68,13 @@ This is the Well-Architected checklist for Ethereum nodes implementation of the ## Setup Instructions -### Setup Cloud9 +### Open AWS CloudShell -We will use AWS Cloud9 to execute the subsequent commands. Follow the instructions in [Cloud9 Setup](../../docs/setup-cloud9.md) +To begin, ensure you login to your AWS account with permissions to create and modify resources in IAM, EC2, EBS, VPC, S3, KMS, and Secrets Manager. + +From the AWS Management Console, open the [AWS CloudShell](https://docs.aws.amazon.com/cloudshell/latest/userguide/welcome.html), a web-based shell environment. If unfamiliar, review the [2-minute YouTube video](https://youtu.be/fz4rbjRaiQM) for an overview and check out [CloudShell with VPC environment](https://docs.aws.amazon.com/cloudshell/latest/userguide/creating-vpc-environment.html) that we'll use to test nodes API from internal IP address space. + +Once ready, you can run the commands to deploy and test blueprints in the CloudShell. ### Make sure you have access to Ethereum L1 node @@ -175,25 +179,28 @@ BASE_L1_CONSENSUS_ENDPOINT="https://ethereum-sepolia-beacon-api.publicnode.com" 2. Deploy Base RPC Node and wait for it to sync. For Mainnet it might a day when using snapshots or about a week if syncing from block 0. You can use snapshots provided by the Base team by setting `BASE_RESTORE_FROM_SNAPSHOT="true"` in `.env` file. - ```bash - pwd - # Make sure you are in aws-blockchain-node-runners/lib/base - npx cdk deploy base-ha-nodes --json --outputs-file ha-nodes-deploy.json - ``` +```bash +pwd +# Make sure you are in aws-blockchain-node-runners/lib/base +npx cdk deploy base-ha-nodes --json --outputs-file ha-nodes-deploy.json +``` 2. Give the new RPC **full** nodes about 5 hours to initialize and then run the following query against the load balancer behind the RPC node created. - ```bash - export RPC_ALB_URL=$(cat ha-nodes-deploy.json | jq -r '..|.alburl? | select(. != null)') - echo $RPC_ALB_URL - ``` +```bash +export RPC_ALB_URL=$(cat ha-nodes-deploy.json | jq -r '..|.alburl? | select(. != null)') +echo RPC_ALB_URL=$RPC_ALB_URL +``` Periodically check [Geth Syncing Status](https://geth.ethereum.org/docs/fundamentals/logs#syncing). Run the following query from within the same VPC and against the private IP of the load balancer fronting your nodes: - ```bash - curl http://$RPC_ALB_URL:8545 -X POST -H "Content-Type: application/json" \ - --data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' - ``` + Copy output from the last `echo` command with `RPC_ALB_URL=` and open [CloudShell tab with VPC environment](https://docs.aws.amazon.com/cloudshell/latest/userguide/creating-vpc-environment.html) to access internal IP address space. Paste `RPC_ALB_URL=` into the new CloudShell tab. Then query the API: + +``` bash +# IMPORTANT: Run from CloudShell VPC environment tab +curl http://$RPC_ALB_URL:8545 -X POST -H "Content-Type: application/json" \ +--data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' +``` **NOTE:** By default and for security reasons the load balancer is available only from within the default VPC in the region where it is deployed. It is not available from the Internet and is not open for external connections. Before opening it up please make sure you protect your RPC APIs. From 46d7a7ff17a1523461185120fe44da6e5f3ed08e Mon Sep 17 00:00:00 2001 From: Nikolay Vlasov Date: Thu, 8 Aug 2024 15:47:35 -0700 Subject: [PATCH 62/75] Base. Changed Cloud9 for CloudShell in README --- lib/base/README.md | 283 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 283 insertions(+) diff --git a/lib/base/README.md b/lib/base/README.md index e69de29b..96db62d3 100644 --- a/lib/base/README.md +++ b/lib/base/README.md @@ -0,0 +1,283 @@ +# Sample AWS Blockchain Node Runner app for Base Nodes + +| Contributed by | +|:---------------| +|[@frbrkoala](https://github.com/frbrkoala), [@danyalprout](https://github.com/danyalprout)| + +[Base](https://base.org/) is a "Layer 2" scaling solution for Ethereum. This blueprint helps to deploy Base RPC nodes on AWS. It is meant to be used for development, testing or Proof of Concept purposes. + +## Overview of Deployment Architectures for Single Node setups + +### Single node setup + +![Single Node Deployment](./doc/assets/Architecture-SingleNode-v3.png) + +1. A Base node deployed in the [Default VPC](https://docs.aws.amazon.com/vpc/latest/userguide/default-vpc.html) continuously synchronizes with the rest of nodes on Base blockchain network through [Internet Gateway](https://docs.aws.amazon.com/vpc/latest/userguide/VPC_Internet_Gateway.html). +2. The Base node is used by dApps or development tools internally from within the Default VPC. JSON RPC API is not exposed to the Internet directly to protect nodes from unauthorized access. +3. Your Base node needs access to a fully-synced [Ethereum Mainnet or Sepolia RPC endpoint](https://docs.base.org/tools/node-providers) . +4. The Base node sends various monitoring metrics for both EC2 and Base nodes to Amazon CloudWatch. + +## Additional materials + +
+Review the for pros and cons of this solution. + +### Well-Architected Checklist + +This is the Well-Architected checklist for Ethereum nodes implementation of the AWS Blockchain Node Runner app. This checklist takes into account questions from the [AWS Well-Architected Framework](https://aws.amazon.com/architecture/well-architected/) which are relevant to this workload. Please feel free to add more checks from the framework if required for your workload. + +| Pillar | Control | Question/Check | Remarks | +|:------------------------|:----------------------------------|:---------------------------------------------------------------------------------|:-----------------| +| Security | Network protection | Are there unnecessary open ports in security groups? | Please note that port 9222 (TCP/UDP) for Base are open to public to support P2P protocols. We have to rely on the protection mechanisms built into the Base software to protect those ports. | +| | | Traffic inspection | AWS WAF could be implemented for traffic inspection. Additional charges will apply. | +| | Compute protection | Reduce attack surface | This solution uses Amazon Linux 2 AMI. You may choose to run hardening scripts on it. | +| | | Enable people to perform actions at a distance | This solution uses AWS Systems Manager for terminal session, not ssh ports. | +| | Data protection at rest | Use encrypted Amazon Elastic Block Store (Amazon EBS) volumes | This solution uses encrypted Amazon EBS volumes. | +| | Data protection in transit | Use TLS | By design TLS is not used in Base RPC and P2P protocols because the data is considered public. To protect RPC traffic we expose the port only for internal use. | +| | Authorization and access control | Use instance profile with Amazon Elastic Compute Cloud (Amazon EC2) instances | This solution uses AWS Identity and Access Management (AWS IAM) role instead of IAM user. | +| | | Following principle of least privilege access | In the node, root user is not used (using special user "bcuser" instead). | +| | Application security | Security focused development practices | cdk-nag is being used with documented suppressions. | +| Cost optimization | Service selection | Use cost effective resources | Base nodes works well on ARM architecture and we use Graviton3-powered EC2 instances for better cost effectiveness. | +| | Cost awareness | Estimate costs | One Base node with on-Demand priced m7g.2xlarge and 3TiB EBS gp3 volume will cost around US$599.27 per month in the US East (N. Virginia) region. Additional charges will apply for Ethereum L1 node and will depend on the service used. | +| Reliability | Resiliency implementation | Withstand component failures | This solution currently does not have high availability and is deployed to a single availability zone. | +| | Data backup | How is data backed up? | The data is not specially backed up. The node will have to re-sync its state from other nodes in the Base network to recover. | +| | Resource monitoring | How are workload resources monitored? | Resources are being monitored using Amazon CloudWatch dashboards. Amazon CloudWatch custom metrics are being pushed via CloudWatch Agent. | +| Performance efficiency | Compute selection | How is compute solution selected? | Compute solution is selected based on the recommendations the from Base community to provide stable and cost-effective operations. | +| | Storage selection | How is storage solution selected? | Storage solution is selected based on the recommendations the from Base community to provide stable and cost-effective operations. | +| | Architecture selection | How is the best performance architecture selected? | In this solution we try to balance price and performance to achieve better cost efficiency, but not necessarily the best performance. | +| Operational excellence | Workload health | How is health of workload determined? | We rely on the standard EC2 instance monitoring tool to detect stalled instances. | +| Sustainability | Hardware & services | Select most efficient hardware for your workload | We use ARM-powered EC2 instance type for better cost/performance balance. | +
+ +
+Recommended Infrastructure + +## Hardware Requirements + +**Minimum for Base node sepolia** + +- Instance type [m7g.2xlarge](https://aws.amazon.com/ec2/instance-types/m7g/). +- 1500GB EBS gp3 storage with at least 5000 IOPS. + +**Recommended for Base node on mainnet** + +- Instance type [m7g.2xlarge](https://aws.amazon.com/ec2/instance-types/m7g/). +- 4100GB EBS gp3 storage with at least 5000 IOPS.` + +
+ +## Setup Instructions + +### Open AWS CloudShell + +To begin, ensure you login to your AWS account with permissions to create and modify resources in IAM, EC2, EBS, VPC, S3, KMS, and Secrets Manager. + +From the AWS Management Console, open the [AWS CloudShell](https://docs.aws.amazon.com/cloudshell/latest/userguide/welcome.html), a web-based shell environment. If unfamiliar, review the [2-minute YouTube video](https://youtu.be/fz4rbjRaiQM) for an overview and check out [CloudShell with VPC environment](https://docs.aws.amazon.com/cloudshell/latest/userguide/creating-vpc-environment.html) that we'll use to test nodes API from internal IP address space. + +Once ready, you can run the commands to deploy and test blueprints in the CloudShell. + +### Make sure you have access to Ethereum L1 node + +Base node needs a URL to a Full Ethereum Node to validate blocks it receives. You can run your own with [Ethereum node blueprint](https://aws-samples.github.io/aws-blockchain-node-runners/docs/Blueprints/Ethereum) or use [one of partners of Base](https://docs.base.org/tools/node-providers). + +### On your CloudShell: Clone this repository and install dependencies + +```bash + git clone https://github.com/aws-samples/aws-blockchain-node-runners + cd aws-blockchain-node-runners + npm install +``` + +### From your CloudShell: Deploy required dependencies + +1. Make sure you are in the root directory of the cloned repository + +2. If you have deleted or don't have the default VPC, create default VPC + + ```bash + aws ec2 create-default-vpc + ``` + + > NOTE: + > You may see the following error if the default VPC already exists: `An error occurred (DefaultVpcAlreadyExists) when calling the CreateDefaultVpc operation: A Default VPC already exists for this account in this region.`. That means you can just continue with the following steps. + +3. Configure your setup + + Create your own copy of `.env` file and edit it to update with your AWS Account ID and Region: +```bash +# Make sure you are in aws-blockchain-node-runners/lib/base +cd lib/base +pwd +cp ./sample-configs/.env-sample-rpc .env +nano .env +``` + +4. Deploy common components such as IAM role + +```bash +pwd +# Make sure you are in aws-blockchain-node-runners/lib/base +npx cdk deploy base-common +``` + + > IMPORTANT: + > All AWS CDK v2 deployments use dedicated AWS resources to hold data during deployment. Therefore, your AWS account and Region must be [bootstrapped](https://docs.aws.amazon.com/cdk/v2/guide/bootstrapping.html) to create these resources before you can deploy. If you haven't already bootstrapped, issue the following command: + > ```bash + > cdk bootstrap aws://ACCOUNT-NUMBER/REGION + > ``` + +### Option 1: Deploy Single Node + +1. For L1 node you you can set your own URLs in `BASE_L1_EXECUTION_ENDPOINT` and `BASE_L1_CONSENSUS_ENDPOINT` properties of `.env` file. It can be one of [the providers recommended by Base](https://docs.base.org/tools/node-providers) or you can run your own Ethereum node [with Node Runner Ethereum blueprint](https://aws-samples.github.io/aws-blockchain-node-runners/docs/Blueprints/Ethereum) (tested with single-node geth-lighthouse combination). For example: + +```bash +#For Sepolia: +BASE_L1_EXECUTION_ENDPOINT="https://ethereum-sepolia-rpc.publicnode.com" +BASE_L1_CONSENSUS_ENDPOINT="https://ethereum-sepolia-beacon-api.publicnode.com" +``` + +2. Deploy Base RPC Node and wait for it to sync. For Full node on Mainnet it might take a day when using snapshots or about a week if syncing from block 0. You can use snapshots provided by the Base team by setting `BASE_RESTORE_FROM_SNAPSHOT="true"` in `.env` file. + +```bash +pwd +# Make sure you are in aws-blockchain-node-runners/lib/base +npx cdk deploy base-single-node --json --outputs-file single-node-deploy.json +``` +After deployment you can watch the progress with CloudWatch dashboard (see [Monitoring](#monitoring)) or check the progress manually. For manual access, use SSM to connect into EC2 first and watch the log like this: + +```bash +export INSTANCE_ID=$(cat single-node-deploy.json | jq -r '..|.node-instance-id? | select(. != null)') +echo "INSTANCE_ID=" $INSTANCE_ID +export AWS_REGION=us-east-1 +aws ssm start-session --target $INSTANCE_ID --region $AWS_REGION +echo Latest synced block behind by: $((($(date +%s)-$( \ +curl -d '{"id":0,"jsonrpc":"2.0","method":"optimism_syncStatus"}' \ +-H "Content-Type: application/json" http://localhost:7545 | \ +jq -r .result.unsafe_l2.timestamp))/60)) minutes +``` + +3. Test Base RPC API + Use curl to query from within the node instance: +```bash +export INSTANCE_ID=$(cat single-node-deploy.json | jq -r '..|.node-instance-id? | select(. != null)') +echo "INSTANCE_ID=" $INSTANCE_ID +export AWS_REGION=us-east-1 +aws ssm start-session --target $INSTANCE_ID --region $AWS_REGION + +curl -s -X POST -H "Content-Type: application/json" --data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' http://localhost:8545 +``` + +### Option 2: Highly Available RPC Nodes + +1. For L1 node you you can set your own URLs in `BASE_L1_EXECUTION_ENDPOINT` and `BASE_L1_CONSENSUS_ENDPOINT` properties of `.env` file. It can be one of [the providers recommended by Base](https://docs.base.org/tools/node-providers) or you can run your own Ethereum node [with Node Runner Ethereum blueprint](https://aws-samples.github.io/aws-blockchain-node-runners/docs/Blueprints/Ethereum) (tested with geth-lighthouse combination). For example: + +```bash +#For Sepolia: +BASE_L1_EXECUTION_ENDPOINT="https://ethereum-sepolia-rpc.publicnode.com" +BASE_L1_CONSENSUS_ENDPOINT="https://ethereum-sepolia-beacon-api.publicnode.com" +``` + +2. Deploy Base RPC Node and wait for it to sync. For Mainnet it might a day when using snapshots or about a week if syncing from block 0. You can use snapshots provided by the Base team by setting `BASE_RESTORE_FROM_SNAPSHOT="true"` in `.env` file. + +```bash +pwd +# Make sure you are in aws-blockchain-node-runners/lib/base +npx cdk deploy base-ha-nodes --json --outputs-file ha-nodes-deploy.json +``` + +2. Give the new RPC **full** nodes about 5 hours to initialize and then run the following query against the load balancer behind the RPC node created. + +```bash +export RPC_ALB_URL=$(cat ha-nodes-deploy.json | jq -r '..|.alburl? | select(. != null)') +echo RPC_ALB_URL=$RPC_ALB_URL +``` + +Copy output from the last `echo` command with `RPC_ALB_URL=` and open [CloudShell tab with VPC environment](https://docs.aws.amazon.com/cloudshell/latest/userguide/creating-vpc-environment.html) to access internal IP address space. Paste `RPC_ALB_URL=` into the new CloudShell tab. Then query the API: + +```bash +# IMPORTANT: Run from CloudShell VPC environment tab +curl http://$RPC_ALB_URL:8545 -X POST -H "Content-Type: application/json" \ +--data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' +``` + +**NOTE:** By default and for security reasons the load balancer is available only from within the default VPC in the region where it is deployed. It is not available from the Internet and is not open for external connections. Before opening it up please make sure you protect your RPC APIs. + +**NOTE:** We currently don't recommend running **archive** nodes in HA setup, because it takes way too long to get them synced. Use single-node setup instead. + +### Monitoring +Every 5 minutes a script on the Base node publishes to CloudWatch service the metrics for current block for L1/L2 clients as well as blocks behind metric for L1 and minutes behind for L2. When the node is fully synced the blocks behind metric should get to 4 and minutes behind should get down to 0. To see the metrics for **single node only**: + +- Navigate to CloudWatch service (make sure you are in the region you have specified for AWS_REGION) +- Open Dashboards and select `base-single-node--` from the list of dashboards. + +Metrics for **ha nodes** configuration is not yet implemented (contributions are welcome!) + +## From your CloudShell: Clear up and undeploy everything + +1. Undeploy all Nodes and Common stacks + +```bash +# Setting the AWS account id and region in case local .env file is lost +export AWS_ACCOUNT_ID= +export AWS_REGION= + +pwd +# Make sure you are in aws-blockchain-node-runners/lib/base + +# Undeploy Single Node +npx cdk destroy base-single-node + +# Undeploy HA Nodes +npx cdk destroy base-ha-nodes + +# Delete all common components like IAM role and Security Group +npx cdk destroy base-common +``` + +## FAQ + +1. How to check the logs of the clients running on my Base node? + + **Note:** In this tutorial we chose not to use SSH and use Session Manager instead. That allows you to log all sessions in AWS CloudTrail to see who logged into the server and when. If you receive an error saying `SessionManagerPlugin is not found`, [install Session Manager plugin for AWS CLI](https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-working-with-install-plugin.html) + +```bash +pwd +# Make sure you are in aws-blockchain-node-runners/lib/base + +export INSTANCE_ID=$(cat single-node-deploy.json | jq -r '..|.nodeinstanceid? | select(. != null)') +echo "INSTANCE_ID=" $INSTANCE_ID +export AWS_REGION=us-east-1 +aws ssm start-session --target $INSTANCE_ID --region $AWS_REGION +sudo su bcuser +# Geth logs: +docker logs --tail 50 node_geth_1 -f +# Base logs: +docker logs --tail 50 node_node_1 -f +``` +2. How to check the logs from the EC2 user-data script? + +```bash +pwd +# Make sure you are in aws-blockchain-node-runners/lib/base + +export INSTANCE_ID=$(cat single-node-deploy.json | jq -r '..|.nodeinstanceid? | select(. != null)') +echo "INSTANCE_ID=" $INSTANCE_ID +export AWS_REGION=us-east-1 +aws ssm start-session --target $INSTANCE_ID --region $AWS_REGION +sudo cat /var/log/cloud-init-output.log +``` + +3. How can I restart the Base node? + +``` bash +export INSTANCE_ID=$(cat single-node-deploy.json | jq -r '..|.nodeinstanceid? | select(. != null)') +echo "INSTANCE_ID=" $INSTANCE_ID +export AWS_REGION=us-east-1 +aws ssm start-session --target $INSTANCE_ID --region $AWS_REGION +sudo su bcuser +/usr/local/bin/docker-compose -f /home/bcuser/node/docker-compose.yml down && \ +/usr/local/bin/docker-compose -f /home/bcuser/node/docker-compose.yml up -d +``` +4. Where to find the key Base client directories? + + - The data directory is `/data` \ No newline at end of file From f9afa413925771590e3af2cbe08afb6ee870f319 Mon Sep 17 00:00:00 2001 From: Varsha Narmat Date: Thu, 15 Aug 2024 17:12:30 -0400 Subject: [PATCH 63/75] updated blocksbehind logic for bsc and .env file for base --- lib/base/README.md | 2 +- lib/bsc/lib/assets/bsc-checker/syncchecker-bsc.sh | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/base/README.md b/lib/base/README.md index 96db62d3..d37665c3 100644 --- a/lib/base/README.md +++ b/lib/base/README.md @@ -108,7 +108,7 @@ Base node needs a URL to a Full Ethereum Node to validate blocks it receives. Yo # Make sure you are in aws-blockchain-node-runners/lib/base cd lib/base pwd -cp ./sample-configs/.env-sample-rpc .env +cp ./sample-configs/.env-sample-full .env nano .env ``` diff --git a/lib/bsc/lib/assets/bsc-checker/syncchecker-bsc.sh b/lib/bsc/lib/assets/bsc-checker/syncchecker-bsc.sh index 68e61d1c..71cd8f66 100644 --- a/lib/bsc/lib/assets/bsc-checker/syncchecker-bsc.sh +++ b/lib/bsc/lib/assets/bsc-checker/syncchecker-bsc.sh @@ -14,6 +14,11 @@ BSC_HIGHEST_BLOCK=$(echo $((${BSC_HIGHEST_BLOCK_HEX}))) BSC_SYNC_BLOCK=$(echo $((${BSC_SYNC_BLOCK_HEX}))) BSC_BLOCKS_BEHIND="$((BSC_HIGHEST_BLOCK-BSC_SYNC_BLOCK))" +# Handle negative values if current block is bigger than highest block +if [[ "$BSC_BLOCKS_BEHIND" -lt "0" ]]; then + BSC_BLOCKS_BEHIND=0 +fi + # Sending data to CloudWatch TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600") INSTANCE_ID=$(curl -H "X-aws-ec2-metadata-token: $TOKEN" -s http://169.254.169.254/latest/meta-data/instance-id) From 855bc2f7f597eca2ca1f2ec976dc8ba843828ac2 Mon Sep 17 00:00:00 2001 From: Nikolay Vlasov Date: Fri, 16 Aug 2024 15:04:16 +1000 Subject: [PATCH 64/75] Base. Fixed user-data after breaking changes in snapshot archive and docker-compose startup parameters --- lib/base/README.md | 4 ++-- lib/base/lib/assets/base/node-start.sh | 3 ++- lib/base/lib/assets/base/node-stop.sh | 4 ++-- lib/base/lib/assets/restore-from-snapshot-archive-s3.sh | 2 +- lib/base/lib/assets/restore-from-snapshot-http.sh | 2 +- 5 files changed, 8 insertions(+), 7 deletions(-) diff --git a/lib/base/README.md b/lib/base/README.md index 96db62d3..4dec9018 100644 --- a/lib/base/README.md +++ b/lib/base/README.md @@ -146,7 +146,7 @@ npx cdk deploy base-single-node --json --outputs-file single-node-deploy.json After deployment you can watch the progress with CloudWatch dashboard (see [Monitoring](#monitoring)) or check the progress manually. For manual access, use SSM to connect into EC2 first and watch the log like this: ```bash -export INSTANCE_ID=$(cat single-node-deploy.json | jq -r '..|.node-instance-id? | select(. != null)') +export INSTANCE_ID=$(cat single-node-deploy.json | jq -r '..|.nodeinstanceid? | select(. != null)') echo "INSTANCE_ID=" $INSTANCE_ID export AWS_REGION=us-east-1 aws ssm start-session --target $INSTANCE_ID --region $AWS_REGION @@ -159,7 +159,7 @@ jq -r .result.unsafe_l2.timestamp))/60)) minutes 3. Test Base RPC API Use curl to query from within the node instance: ```bash -export INSTANCE_ID=$(cat single-node-deploy.json | jq -r '..|.node-instance-id? | select(. != null)') +export INSTANCE_ID=$(cat single-node-deploy.json | jq -r '..|.nodeinstanceid? | select(. != null)') echo "INSTANCE_ID=" $INSTANCE_ID export AWS_REGION=us-east-1 aws ssm start-session --target $INSTANCE_ID --region $AWS_REGION diff --git a/lib/base/lib/assets/base/node-start.sh b/lib/base/lib/assets/base/node-start.sh index 728a7db3..bdd37569 100644 --- a/lib/base/lib/assets/base/node-start.sh +++ b/lib/base/lib/assets/base/node-start.sh @@ -1,7 +1,8 @@ #!/bin/bash set -e -echo "Script is starting..." +export CLIENT=geth +echo "Script is starting client $CLIENT" # Start the node cd /home/bcuser/node /usr/local/bin/docker-compose -f /home/bcuser/node/docker-compose.yml up -d diff --git a/lib/base/lib/assets/base/node-stop.sh b/lib/base/lib/assets/base/node-stop.sh index 5a4d392d..613e58f8 100644 --- a/lib/base/lib/assets/base/node-stop.sh +++ b/lib/base/lib/assets/base/node-stop.sh @@ -1,7 +1,7 @@ #!/bin/bash set -e -echo "Script is stopping the node..." - +export CLIENT=geth +echo "Script is stopping client $CLIENT" # Stop the node cd /home/bcuser/node /usr/local/bin/docker-compose -f /home/bcuser/node/docker-compose.yml down diff --git a/lib/base/lib/assets/restore-from-snapshot-archive-s3.sh b/lib/base/lib/assets/restore-from-snapshot-archive-s3.sh index 6bafba14..2f0cbaff 100644 --- a/lib/base/lib/assets/restore-from-snapshot-archive-s3.sh +++ b/lib/base/lib/assets/restore-from-snapshot-archive-s3.sh @@ -15,7 +15,7 @@ s5cmd --log error cp s3://base-snapshots-$NETWORK_ID-archive/$LATEST_SNAPSHOT_FI echo "Downloading Snapshot script finished" && \ sleep 60 &&\ echo "Starting snapshot decompression ..." && \ -tar -zxvf $SNAPSHOT_DIR/$SNAPSHOT_FILE_NAME -C /data 2>&1 | tee unzip.log && echo "decompresed successfully..." || echo "decompression failed..." >> snapshots-decompression.log +tar --use-compress-program=unzstd -xvf $SNAPSHOT_DIR/$SNAPSHOT_FILE_NAME -C /data 2>&1 | tee unzip.log && echo "decompresed successfully..." || echo "decompression failed..." >> snapshots-decompression.log echo "Decompresed snapshot, cleaning up..." diff --git a/lib/base/lib/assets/restore-from-snapshot-http.sh b/lib/base/lib/assets/restore-from-snapshot-http.sh index 09714d7e..5611f87e 100644 --- a/lib/base/lib/assets/restore-from-snapshot-http.sh +++ b/lib/base/lib/assets/restore-from-snapshot-http.sh @@ -46,7 +46,7 @@ sleep 60 echo "Starting snapshot decompression ..." -tar -zxvf $SNAPSHOT_DIR/$SNAPSHOT_FILE_NAME -C /data 2>&1 | tee unzip.log && echo "decompresed successfully..." || echo "decompression failed..." >> snapshots-decompression.log +tar --use-compress-program=unzstd -xvf $SNAPSHOT_DIR/$SNAPSHOT_FILE_NAME -C /data 2>&1 | tee unzip.log && echo "decompresed successfully..." || echo "decompression failed..." >> snapshots-decompression.log echo "Decompresed snapshot, cleaning up..." From dfbac2e0450977425dd3462d33e94b53961fa00f Mon Sep 17 00:00:00 2001 From: Nikolay Vlasov Date: Mon, 19 Aug 2024 12:21:27 +1000 Subject: [PATCH 65/75] Base. Fixed sample configs after testing. --- lib/base/README.md | 2 +- lib/base/sample-configs/.env-sample-archive | 2 +- lib/base/sample-configs/.env-sample-full | 2 +- lib/base/sample-configs/.env-sample-full-ha | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/base/README.md b/lib/base/README.md index 4dec9018..585340ff 100644 --- a/lib/base/README.md +++ b/lib/base/README.md @@ -108,7 +108,7 @@ Base node needs a URL to a Full Ethereum Node to validate blocks it receives. Yo # Make sure you are in aws-blockchain-node-runners/lib/base cd lib/base pwd -cp ./sample-configs/.env-sample-rpc .env +cp ./sample-configs/.env-sample-full .env nano .env ``` diff --git a/lib/base/sample-configs/.env-sample-archive b/lib/base/sample-configs/.env-sample-archive index eeb088a5..4292e8d4 100644 --- a/lib/base/sample-configs/.env-sample-archive +++ b/lib/base/sample-configs/.env-sample-archive @@ -14,7 +14,7 @@ BASE_CPU_TYPE="ARM_64" # All options: "x86_64", "ARM_64". IMPORT # Data volume configuration BASE_DATA_VOL_TYPE="gp3" # Other options: "io1" | "io2" | "gp3" | "instance-store" . IMPORTANT: Use "instance-store" option only with instance types that support that feature, like popular for node im4gn, d3, i3en, and i4i instance families -BASE_DATA_VOL_SIZE="1000" # Current required data size in GB to keep both snapshot archive and unarchived version of it. For Sepolia 1000 will be sufficient. +BASE_DATA_VOL_SIZE="5000" # Current required data size in GB to keep both snapshot archive and unarchived version of it. For Sepolia 1000 will be sufficient. BASE_DATA_VOL_IOPS="5000" # Max IOPS for EBS volumes (not applicable for "instance-store") BASE_DATA_VOL_THROUGHPUT="700" # Max throughput for EBS gp3 volumes (not applicable for "io1" | "io2" | "instance-store") BASE_L1_EXECUTION_ENDPOINT="https://ethereum-sepolia-rpc.publicnode.com" # Set your own URL to Ethereum L1 node: https://docs.base.org/tools/node-providers diff --git a/lib/base/sample-configs/.env-sample-full b/lib/base/sample-configs/.env-sample-full index ae78c77b..9609249e 100644 --- a/lib/base/sample-configs/.env-sample-full +++ b/lib/base/sample-configs/.env-sample-full @@ -14,7 +14,7 @@ BASE_CPU_TYPE="ARM_64" # All options: "x86_64", "ARM_64". IMPORT # Data volume configuration BASE_DATA_VOL_TYPE="gp3" # Other options: "io1" | "io2" | "gp3" | "instance-store" . IMPORTANT: Use "instance-store" option only with instance types that support that feature, like popular for node im4gn, d3, i3en, and i4i instance families -BASE_DATA_VOL_SIZE="4000" # Current required data size in GB to keep both snapshot archive and unarchived version of it. For Sepolia 1000 will be sufficient. +BASE_DATA_VOL_SIZE="1000" # Current required data size in GB to keep both snapshot archive and unarchived version of it. For Sepolia 1000 will be sufficient. BASE_DATA_VOL_IOPS="5000" # Max IOPS for EBS volumes (not applicable for "instance-store") BASE_DATA_VOL_THROUGHPUT="700" # Max throughput for EBS gp3 volumes (not applicable for "io1" | "io2" | "instance-store") BASE_L1_EXECUTION_ENDPOINT="https://ethereum-sepolia-rpc.publicnode.com" # Set your own URL to Ethereum L1 node: https://docs.base.org/tools/node-providers diff --git a/lib/base/sample-configs/.env-sample-full-ha b/lib/base/sample-configs/.env-sample-full-ha index ae78c77b..9609249e 100644 --- a/lib/base/sample-configs/.env-sample-full-ha +++ b/lib/base/sample-configs/.env-sample-full-ha @@ -14,7 +14,7 @@ BASE_CPU_TYPE="ARM_64" # All options: "x86_64", "ARM_64". IMPORT # Data volume configuration BASE_DATA_VOL_TYPE="gp3" # Other options: "io1" | "io2" | "gp3" | "instance-store" . IMPORTANT: Use "instance-store" option only with instance types that support that feature, like popular for node im4gn, d3, i3en, and i4i instance families -BASE_DATA_VOL_SIZE="4000" # Current required data size in GB to keep both snapshot archive and unarchived version of it. For Sepolia 1000 will be sufficient. +BASE_DATA_VOL_SIZE="1000" # Current required data size in GB to keep both snapshot archive and unarchived version of it. For Sepolia 1000 will be sufficient. BASE_DATA_VOL_IOPS="5000" # Max IOPS for EBS volumes (not applicable for "instance-store") BASE_DATA_VOL_THROUGHPUT="700" # Max throughput for EBS gp3 volumes (not applicable for "io1" | "io2" | "instance-store") BASE_L1_EXECUTION_ENDPOINT="https://ethereum-sepolia-rpc.publicnode.com" # Set your own URL to Ethereum L1 node: https://docs.base.org/tools/node-providers From 6dc9a21caf64810020e4bf7ee47786d419087320 Mon Sep 17 00:00:00 2001 From: Nikolay Vlasov Date: Tue, 20 Aug 2024 13:51:30 +1000 Subject: [PATCH 66/75] Base. Added a note on build time to the README --- lib/base/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/base/README.md b/lib/base/README.md index 585340ff..66fcc939 100644 --- a/lib/base/README.md +++ b/lib/base/README.md @@ -136,7 +136,7 @@ BASE_L1_EXECUTION_ENDPOINT="https://ethereum-sepolia-rpc.publicnode.com" BASE_L1_CONSENSUS_ENDPOINT="https://ethereum-sepolia-beacon-api.publicnode.com" ``` -2. Deploy Base RPC Node and wait for it to sync. For Full node on Mainnet it might take a day when using snapshots or about a week if syncing from block 0. You can use snapshots provided by the Base team by setting `BASE_RESTORE_FROM_SNAPSHOT="true"` in `.env` file. +2. Deploy Base RPC Node and wait for it to build binaries and sync. For Full node it takes about 10 minutes to build binaries from the source code and on Mainnet it might take a day to sync when using snapshots or about a week if syncing from block 0. You can use snapshots provided by the Base team by setting `BASE_RESTORE_FROM_SNAPSHOT="true"` in `.env` file. ```bash pwd From 3d513a98ac366df3782364cce0c7edb56fadec18 Mon Sep 17 00:00:00 2001 From: Nikolay Vlasov Date: Fri, 23 Aug 2024 12:42:50 +1000 Subject: [PATCH 67/75] Base. Moved configuration of CloudWatch Agent to the end of user-data scrt --- lib/base/lib/assets/user-data/node.sh | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/base/lib/assets/user-data/node.sh b/lib/base/lib/assets/user-data/node.sh index ef488978..d46f33e7 100644 --- a/lib/base/lib/assets/user-data/node.sh +++ b/lib/base/lib/assets/user-data/node.sh @@ -66,14 +66,6 @@ echo "Downloading assets zip file" aws s3 cp $ASSETS_S3_PATH ./assets.zip unzip -q assets.zip -echo 'Configuring CloudWatch Agent' -cp /opt/cw-agent.json /opt/aws/amazon-cloudwatch-agent/etc/custom-amazon-cloudwatch-agent.json - -echo "Starting CloudWatch Agent" -/opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl \ --a fetch-config -c file:/opt/aws/amazon-cloudwatch-agent/etc/custom-amazon-cloudwatch-agent.json -m ec2 -s -systemctl status amazon-cloudwatch-agent - echo 'Uninstalling AWS CLI v1' yum remove awscli @@ -295,6 +287,14 @@ lsblk -d chown -R bcuser:bcuser /data chmod -R 755 /data +echo 'Configuring CloudWatch Agent' +cp /opt/cw-agent.json /opt/aws/amazon-cloudwatch-agent/etc/custom-amazon-cloudwatch-agent.json + +echo "Starting CloudWatch Agent" +/opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl \ +-a fetch-config -c file:/opt/aws/amazon-cloudwatch-agent/etc/custom-amazon-cloudwatch-agent.json -m ec2 -s +systemctl restart amazon-cloudwatch-agent + if [ "$RESTORE_FROM_SNAPSHOT" == "false" ]; then echo "Skipping restoration from snapshot. Starting node" systemctl daemon-reload From a031ef7ed4b7550d1ad6dd4bf2bb24a603ae1c33 Mon Sep 17 00:00:00 2001 From: Nikolay Vlasov Date: Tue, 27 Aug 2024 09:27:55 +1000 Subject: [PATCH 68/75] Base. Switched all sample configs to Sepolia --- lib/base/README.md | 2 +- .../{.env-sample-archive => .env-sample-archive-sepolia} | 2 +- .../{.env-sample-full => .env-sample-full-ha-sepolia} | 2 +- .../{.env-sample-full-ha => .env-sample-full-sepolia} | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) rename lib/base/sample-configs/{.env-sample-archive => .env-sample-archive-sepolia} (97%) rename lib/base/sample-configs/{.env-sample-full => .env-sample-full-ha-sepolia} (97%) rename lib/base/sample-configs/{.env-sample-full-ha => .env-sample-full-sepolia} (97%) diff --git a/lib/base/README.md b/lib/base/README.md index 66fcc939..e771eb43 100644 --- a/lib/base/README.md +++ b/lib/base/README.md @@ -108,7 +108,7 @@ Base node needs a URL to a Full Ethereum Node to validate blocks it receives. Yo # Make sure you are in aws-blockchain-node-runners/lib/base cd lib/base pwd -cp ./sample-configs/.env-sample-full .env +cp ./sample-configs/.env-sample-full-sepolia .env nano .env ``` diff --git a/lib/base/sample-configs/.env-sample-archive b/lib/base/sample-configs/.env-sample-archive-sepolia similarity index 97% rename from lib/base/sample-configs/.env-sample-archive rename to lib/base/sample-configs/.env-sample-archive-sepolia index 4292e8d4..b55c9d91 100644 --- a/lib/base/sample-configs/.env-sample-archive +++ b/lib/base/sample-configs/.env-sample-archive-sepolia @@ -7,7 +7,7 @@ AWS_ACCOUNT_ID="xxxxxxxx" AWS_REGION="us-east-1" ## Common configuration parameters ## -BASE_NETWORK_ID="mainnet" # All options: "mainnet", "sepolia" +BASE_NETWORK_ID="sepolia" # All options: "mainnet", "sepolia" BASE_NODE_CONFIGURATION="archive" # All options: "full", "archive" BASE_INSTANCE_TYPE="m7g.2xlarge" # Reconneded for Insance Store: i3en.3xlarge, "x86_64" BASE_CPU_TYPE="ARM_64" # All options: "x86_64", "ARM_64". IMPORTANT: Make sure the CPU type matches the instance type used diff --git a/lib/base/sample-configs/.env-sample-full b/lib/base/sample-configs/.env-sample-full-ha-sepolia similarity index 97% rename from lib/base/sample-configs/.env-sample-full rename to lib/base/sample-configs/.env-sample-full-ha-sepolia index 9609249e..62a43370 100644 --- a/lib/base/sample-configs/.env-sample-full +++ b/lib/base/sample-configs/.env-sample-full-ha-sepolia @@ -7,7 +7,7 @@ AWS_ACCOUNT_ID="xxxxxxxx" AWS_REGION="us-east-1" ## Common configuration parameters ## -BASE_NETWORK_ID="mainnet" # All options: "mainnet", "sepolia" +BASE_NETWORK_ID="sepolia" # All options: "mainnet", "sepolia" BASE_NODE_CONFIGURATION="full" # All options: "full", "archive" BASE_INSTANCE_TYPE="m7g.2xlarge" # Reconneded for Insance Store: i3en.3xlarge, "x86_64" BASE_CPU_TYPE="ARM_64" # All options: "x86_64", "ARM_64". IMPORTANT: Make sure the CPU type matches the instance type used diff --git a/lib/base/sample-configs/.env-sample-full-ha b/lib/base/sample-configs/.env-sample-full-sepolia similarity index 97% rename from lib/base/sample-configs/.env-sample-full-ha rename to lib/base/sample-configs/.env-sample-full-sepolia index 9609249e..62a43370 100644 --- a/lib/base/sample-configs/.env-sample-full-ha +++ b/lib/base/sample-configs/.env-sample-full-sepolia @@ -7,7 +7,7 @@ AWS_ACCOUNT_ID="xxxxxxxx" AWS_REGION="us-east-1" ## Common configuration parameters ## -BASE_NETWORK_ID="mainnet" # All options: "mainnet", "sepolia" +BASE_NETWORK_ID="sepolia" # All options: "mainnet", "sepolia" BASE_NODE_CONFIGURATION="full" # All options: "full", "archive" BASE_INSTANCE_TYPE="m7g.2xlarge" # Reconneded for Insance Store: i3en.3xlarge, "x86_64" BASE_CPU_TYPE="ARM_64" # All options: "x86_64", "ARM_64". IMPORTANT: Make sure the CPU type matches the instance type used From 96c392f3ed797488901e1184a8c8e63b8be4f3ca Mon Sep 17 00:00:00 2001 From: Nikolay Vlasov Date: Wed, 28 Aug 2024 18:03:55 +1000 Subject: [PATCH 69/75] Base. Added cdk-nag to app.ts and more run commends to package.json --- lib/base/app.ts | 10 ++++++++++ .../lib/constructs/base-node-security-group.ts | 16 ++++++++++++++++ lib/base/package.json | 5 ++++- 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/lib/base/app.ts b/lib/base/app.ts index ca401786..adfc05c9 100644 --- a/lib/base/app.ts +++ b/lib/base/app.ts @@ -2,6 +2,7 @@ import 'dotenv/config' import 'source-map-support/register'; import * as cdk from 'aws-cdk-lib'; +import * as nag from "cdk-nag"; import * as config from "./lib/config/baseConfig"; import {BaseCommonStack} from "./lib/common-stack"; import {BaseSingleNodeStack} from "./lib/single-node-stack"; @@ -48,3 +49,12 @@ new BaseHANodesStack(app, "base-ha-nodes", { heartBeatDelayMin: config.haNodeConfig.heartBeatDelayMin, numberOfNodes: config.haNodeConfig.numberOfNodes }); + +// Security Check +cdk.Aspects.of(app).add( + new nag.AwsSolutionsChecks({ + verbose: false, + reports: true, + logIgnores: false, + }) +); \ No newline at end of file diff --git a/lib/base/lib/constructs/base-node-security-group.ts b/lib/base/lib/constructs/base-node-security-group.ts index ac71fccb..baf92053 100644 --- a/lib/base/lib/constructs/base-node-security-group.ts +++ b/lib/base/lib/constructs/base-node-security-group.ts @@ -1,6 +1,7 @@ import * as cdk from "aws-cdk-lib"; import * as cdkConstructs from 'constructs'; import * as ec2 from "aws-cdk-lib/aws-ec2"; +import * as nag from "cdk-nag"; export interface BaseNodeSecurityGroupConstructProps { vpc: cdk.aws_ec2.IVpc; @@ -35,5 +36,20 @@ export interface BaseNodeSecurityGroupConstructProps { sg.addEgressRule(ec2.Peer.anyIpv4(), ec2.Port.udpRange(13001, 65535), "All outbound connections except 13000"); this.securityGroup = sg + + /** + * cdk-nag suppressions + */ + + nag.NagSuppressions.addResourceSuppressions( + this, + [ + { + id: "AwsSolutions-EC23", + reason: "Ethereum requires wildcard inbound for specific ports", + }, + ], + true + ); } } diff --git a/lib/base/package.json b/lib/base/package.json index 3fb94aea..bb155970 100644 --- a/lib/base/package.json +++ b/lib/base/package.json @@ -9,7 +9,10 @@ "cdk_deploy_common": "cdk deploy base-common", "cdk_synth_single_node": "cdk synth base-single-node", "cdk_deploy_single_node": "cdk deploy base-single-node", - "cdk_destroy_single_node": "cdk destroy base-single-node" + "cdk_destroy_single_node": "cdk destroy base-single-node", + "cdk_synth_ha_nodes": "cdk synth base-ha-nodes", + "cdk_deploy_ha_nodes": "cdk deploy base-ha-nodes", + "cdk_destroy_ha_nodes": "cdk destroy base-ha-nodes" }, "dependencies": { "@types/node": "^20.10.0" From 6675342608d5a9bc558d212c89522bceb9bccadf Mon Sep 17 00:00:00 2001 From: Nikolay Vlasov Date: Wed, 28 Aug 2024 18:13:32 +1000 Subject: [PATCH 70/75] Base. Updated documentation to change the docker container names for checking logs --- lib/base/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/base/README.md b/lib/base/README.md index e771eb43..c7a04516 100644 --- a/lib/base/README.md +++ b/lib/base/README.md @@ -249,9 +249,9 @@ echo "INSTANCE_ID=" $INSTANCE_ID export AWS_REGION=us-east-1 aws ssm start-session --target $INSTANCE_ID --region $AWS_REGION sudo su bcuser -# Geth logs: -docker logs --tail 50 node_geth_1 -f -# Base logs: +# Execution client logs: +docker logs --tail 50 node_execution_1 -f +# Base client logs: docker logs --tail 50 node_node_1 -f ``` 2. How to check the logs from the EC2 user-data script? From 979996f2218567c8673a0bd373a7d47f7898abc7 Mon Sep 17 00:00:00 2001 From: Nikolay Vlasov Date: Fri, 30 Aug 2024 13:58:17 +1000 Subject: [PATCH 71/75] Base. Reduced swap size to 5GB only --- lib/base/lib/assets/user-data/node.sh | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/lib/base/lib/assets/user-data/node.sh b/lib/base/lib/assets/user-data/node.sh index d46f33e7..9454be2c 100644 --- a/lib/base/lib/assets/user-data/node.sh +++ b/lib/base/lib/assets/user-data/node.sh @@ -119,11 +119,8 @@ if [ -f /swapfile ]; then fi # Create a new swap file -total_mem=$(grep MemTotal /proc/meminfo | awk '{print $2}') -# Calculate the swap size -swap_size=$((total_mem / 3)) -# Convert the swap size to MB -swap_size_mb=$((swap_size / 1024)) +# Set swap size to fixed 5 GB +swap_size_mb=5120 unit=M fallocate -l $swap_size_mb$unit /swapfile chmod 600 /swapfile From 9e0878d7e15f8e595416b1e633cfade084f5bc08 Mon Sep 17 00:00:00 2001 From: Nikolay Vlasov Date: Tue, 3 Sep 2024 11:14:43 +1000 Subject: [PATCH 72/75] Base. Restricted snapshot download to a single conection only --- lib/base/lib/assets/restore-from-snapshot-http.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/base/lib/assets/restore-from-snapshot-http.sh b/lib/base/lib/assets/restore-from-snapshot-http.sh index 5611f87e..5b4c7352 100644 --- a/lib/base/lib/assets/restore-from-snapshot-http.sh +++ b/lib/base/lib/assets/restore-from-snapshot-http.sh @@ -20,7 +20,7 @@ while (( SNAPSHOT_DOWNLOAD_STATUS != 0 )) do PIDS=$(pgrep aria2c) if [ -z "$PIDS" ]; then - aria2c $SNAPSHOT_URL -d $SNAPSHOT_DIR -o $SNAPSHOT_FILE_NAME -l /data/download.log --log-level=notice --allow-overwrite=true --allow-piece-length-change=true + aria2c --max-connection-per-server=1 $SNAPSHOT_URL -d $SNAPSHOT_DIR -o $SNAPSHOT_FILE_NAME -l /data/download.log --log-level=notice --allow-overwrite=true --allow-piece-length-change=true fi SNAPSHOT_DOWNLOAD_STATUS=$? pid=$(pidof aria2c) From db26b474ab039e4677d0ab103a27d3aa78fd6c4c Mon Sep 17 00:00:00 2001 From: Nikolay Vlasov Date: Wed, 4 Sep 2024 17:30:15 +1000 Subject: [PATCH 73/75] Base. Final changes in documentation after review --- lib/base/README.md | 129 ++++++------ lib/base/doc/assets/Architecture-HA-nodes.png | Bin 0 -> 151628 bytes .../doc/assets/Architecture-SingleNode.drawio | 77 ------- lib/base/doc/assets/Architecture.drawio | 194 ++++++++++++++++++ 4 files changed, 264 insertions(+), 136 deletions(-) create mode 100644 lib/base/doc/assets/Architecture-HA-nodes.png delete mode 100644 lib/base/doc/assets/Architecture-SingleNode.drawio create mode 100644 lib/base/doc/assets/Architecture.drawio diff --git a/lib/base/README.md b/lib/base/README.md index c7a04516..aa2869c0 100644 --- a/lib/base/README.md +++ b/lib/base/README.md @@ -4,18 +4,31 @@ |:---------------| |[@frbrkoala](https://github.com/frbrkoala), [@danyalprout](https://github.com/danyalprout)| -[Base](https://base.org/) is a "Layer 2" scaling solution for Ethereum. This blueprint helps to deploy Base RPC nodes on AWS. It is meant to be used for development, testing or Proof of Concept purposes. +[Base](https://base.org/) is a Layer 2 blockchain network built on the Ethereum protocol. The network is governed by [Coinbase](https://www.coinbase.com/en-au/about), a leading cryptocurrency exchange. Base utilizes the [OP Stack](https://docs.optimism.io/stack/getting-started), a common development stack for Layer 2 blockchain networks. The Base blueprint provides a framework to deploy [Sequencer nodes](https://docs.optimism.io/builders/chain-operators/architecture) on Amazon Web Services (AWS) for development, testing, or proof-of-concept purposes. ## Overview of Deployment Architectures for Single Node setups ### Single node setup +The single node architecture blueprint illustrates the deployment of multiple Base nodes within the AWS Default Virtual Private Cloud (VPC) environment. This configuration is suitable for development, testing, or small-scale proof-of-concept scenarios. + ![Single Node Deployment](./doc/assets/Architecture-SingleNode-v3.png) -1. A Base node deployed in the [Default VPC](https://docs.aws.amazon.com/vpc/latest/userguide/default-vpc.html) continuously synchronizes with the rest of nodes on Base blockchain network through [Internet Gateway](https://docs.aws.amazon.com/vpc/latest/userguide/VPC_Internet_Gateway.html). -2. The Base node is used by dApps or development tools internally from within the Default VPC. JSON RPC API is not exposed to the Internet directly to protect nodes from unauthorized access. -3. Your Base node needs access to a fully-synced [Ethereum Mainnet or Sepolia RPC endpoint](https://docs.base.org/tools/node-providers) . -4. The Base node sends various monitoring metrics for both EC2 and Base nodes to Amazon CloudWatch. +1. The Base node in the [Default VPC](https://docs.aws.amazon.com/vpc/latest/userguide/default-vpc.html) is continuously synchronized with the broader Base blockchain network through the VPC's [Internet Gateway](https://docs.aws.amazon.com/vpc/latest/userguide/VPC_Internet_Gateway.html). This ensures the node maintains up-to-date ledger information. +2. The Base node is designed to be accessed internally by decentralized applications (dApps) or development tools within the Default VPC. To protect the node from unauthorized access, the JSON-RPC API is not directly exposed to the internet. +3. The Base node requires access to a fully synchronized [Ethereum Mainnet or Sepolia testnet RPC endpoint](https://docs.base.org/tools/node-providers), as specified in the Base documentation, to interface with the broader Ethereum ecosystem. +4. The architecture includes integration with Amazon CloudWatch, which collects and monitors various metrics from both the EC2 instance hosting the Base node and the node software. This provides visibility into the node's operational status and performance. + +### Highly Available setup + +The Highly Available node architecture blueprint illustrates the deployment of two Base nodes across availability zones within the AWS Default Virtual Private Cloud (VPC) environment. This configuration is suitable for small-scale proof-of-concept scenarios. + +![Highly Available Nodes Deployment](./doc/assets/Architecture-HA-nodes.png) + +1. Two RPC Base nodes in the [Auto Scaling Group](https://docs.aws.amazon.com/autoscaling/ec2/userguide/auto-scaling-groups.html) in the [Default VPC](https://docs.aws.amazon.com/vpc/latest/userguide/default-vpc.html) is continuously synchronized with the broader Base blockchain network through the VPC's [Internet Gateway](https://docs.aws.amazon.com/vpc/latest/userguide/VPC_Internet_Gateway.html). +2. dApps or development tools access Base nodes internally through [Application Load Balancer](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/introduction.html). To protect the node from unauthorized access, the JSON-RPC API is not directly exposed to the internet. dApps need to handle user authentication and API protection, like [in this example for dApps on AWS](https://aws.amazon.com/blogs/architecture/dapp-authentication-with-amazon-cognito-and-web3-proxy-with-amazon-api-gateway/). +3. The Base nodes needs access to a fully-synced [Ethereum Mainnet or Sepolia RPC endpoint](https://docs.base.org/tools/node-providers). +4. The architecture includes integration with Amazon CloudWatch, which collects and monitors various metrics from the EC2 instances hosting the Base node and the nodes software. This provides visibility into the operational status and performance of the nodes. ## Additional materials @@ -24,7 +37,7 @@ ### Well-Architected Checklist -This is the Well-Architected checklist for Ethereum nodes implementation of the AWS Blockchain Node Runner app. This checklist takes into account questions from the [AWS Well-Architected Framework](https://aws.amazon.com/architecture/well-architected/) which are relevant to this workload. Please feel free to add more checks from the framework if required for your workload. +This is the Well-Architected checklist for Base nodes implementation of the AWS Blockchain Node Runner app. This checklist takes into account questions from the [AWS Well-Architected Framework](https://aws.amazon.com/architecture/well-architected/) which are relevant to this workload. Please feel free to add more checks from the framework if required for your workload. | Pillar | Control | Question/Check | Remarks | |:------------------------|:----------------------------------|:---------------------------------------------------------------------------------|:-----------------| @@ -70,7 +83,7 @@ This is the Well-Architected checklist for Ethereum nodes implementation of the ### Open AWS CloudShell -To begin, ensure you login to your AWS account with permissions to create and modify resources in IAM, EC2, EBS, VPC, S3, KMS, and Secrets Manager. +To begin, ensure you login to your AWS account with permissions to create and modify resources in IAM, EC2, EBS, VPC, and S3. From the AWS Management Console, open the [AWS CloudShell](https://docs.aws.amazon.com/cloudshell/latest/userguide/welcome.html), a web-based shell environment. If unfamiliar, review the [2-minute YouTube video](https://youtu.be/fz4rbjRaiQM) for an overview and check out [CloudShell with VPC environment](https://docs.aws.amazon.com/cloudshell/latest/userguide/creating-vpc-environment.html) that we'll use to test nodes API from internal IP address space. @@ -78,14 +91,14 @@ Once ready, you can run the commands to deploy and test blueprints in the CloudS ### Make sure you have access to Ethereum L1 node -Base node needs a URL to a Full Ethereum Node to validate blocks it receives. You can run your own with [Ethereum node blueprint](https://aws-samples.github.io/aws-blockchain-node-runners/docs/Blueprints/Ethereum) or use [one of partners of Base](https://docs.base.org/tools/node-providers). +To configure Base blueprint you will need a URL to a Full Ethereum Node to validate blocks it receives. You can run your own with [Ethereum node blueprint](https://aws-samples.github.io/aws-blockchain-node-runners/docs/Blueprints/Ethereum) or use [one of partners of Base](https://docs.base.org/tools/node-providers). ### On your CloudShell: Clone this repository and install dependencies ```bash - git clone https://github.com/aws-samples/aws-blockchain-node-runners - cd aws-blockchain-node-runners - npm install +git clone https://github.com/aws-samples/aws-blockchain-node-runners +cd aws-blockchain-node-runners +npm install ``` ### From your CloudShell: Deploy required dependencies @@ -98,86 +111,73 @@ Base node needs a URL to a Full Ethereum Node to validate blocks it receives. Yo aws ec2 create-default-vpc ``` - > NOTE: - > You may see the following error if the default VPC already exists: `An error occurred (DefaultVpcAlreadyExists) when calling the CreateDefaultVpc operation: A Default VPC already exists for this account in this region.`. That means you can just continue with the following steps. +:::note NOTE +You may see the following error if the default VPC already exists: `An error occurred (DefaultVpcAlreadyExists) when calling the CreateDefaultVpc operation: A Default VPC already exists for this account in this region.`. That means you can just continue with the following steps. +::: 3. Configure your setup - Create your own copy of `.env` file and edit it to update with your AWS Account ID and Region: +- Create your own copy of `.env` file and open with text editor like `nano` change `AWS_ACCOUNT_ID` and `AWS_REGION`: ```bash -# Make sure you are in aws-blockchain-node-runners/lib/base cd lib/base -pwd cp ./sample-configs/.env-sample-full-sepolia .env nano .env ``` -4. Deploy common components such as IAM role +- For L1 node URL set `BASE_L1_EXECUTION_ENDPOINT` and `BASE_L1_CONSENSUS_ENDPOINT` properties of `.env` file. You can use one of [the providers recommended by Base](https://docs.base.org/tools/node-providers) or you can start your own Ethereum node [with Node Runner Ethereum blueprint](https://aws-samples.github.io/aws-blockchain-node-runners/docs/Blueprints/Ethereum) (tested with single-node `geth-lighthouse` combination). For example, for Sepolia network: ```bash -pwd -# Make sure you are in aws-blockchain-node-runners/lib/base -npx cdk deploy base-common +BASE_L1_EXECUTION_ENDPOINT="https://ethereum-sepolia-rpc.publicnode.com" +BASE_L1_CONSENSUS_ENDPOINT="https://ethereum-sepolia-beacon-api.publicnode.com" ``` - > IMPORTANT: - > All AWS CDK v2 deployments use dedicated AWS resources to hold data during deployment. Therefore, your AWS account and Region must be [bootstrapped](https://docs.aws.amazon.com/cdk/v2/guide/bootstrapping.html) to create these resources before you can deploy. If you haven't already bootstrapped, issue the following command: - > ```bash - > cdk bootstrap aws://ACCOUNT-NUMBER/REGION - > ``` - -### Option 1: Deploy Single Node +4. Deploy common components such as IAM role -1. For L1 node you you can set your own URLs in `BASE_L1_EXECUTION_ENDPOINT` and `BASE_L1_CONSENSUS_ENDPOINT` properties of `.env` file. It can be one of [the providers recommended by Base](https://docs.base.org/tools/node-providers) or you can run your own Ethereum node [with Node Runner Ethereum blueprint](https://aws-samples.github.io/aws-blockchain-node-runners/docs/Blueprints/Ethereum) (tested with single-node geth-lighthouse combination). For example: +```bash +npx cdk deploy base-common +``` +:::note NOTE +All AWS CDK v2 deployments use dedicated AWS resources to hold data during deployment. Therefore, your AWS account and Region must be [bootstrapped](https://docs.aws.amazon.com/cdk/v2/guide/bootstrapping.html) to create these resources before you can deploy. If you haven't already bootstrapped, issue the following command: ```bash -#For Sepolia: -BASE_L1_EXECUTION_ENDPOINT="https://ethereum-sepolia-rpc.publicnode.com" -BASE_L1_CONSENSUS_ENDPOINT="https://ethereum-sepolia-beacon-api.publicnode.com" +cdk bootstrap aws://ACCOUNT-NUMBER/REGION ``` +::: + +### Option 1: Deploy Single Node -2. Deploy Base RPC Node and wait for it to build binaries and sync. For Full node it takes about 10 minutes to build binaries from the source code and on Mainnet it might take a day to sync when using snapshots or about a week if syncing from block 0. You can use snapshots provided by the Base team by setting `BASE_RESTORE_FROM_SNAPSHOT="true"` in `.env` file. +1. Deploy Base RPC Node and wait for it to build binaries and sync. For Full node it takes about 10 minutes to build binaries from the source code and on Mainnet it might take a day to sync from snapshots or about a week if syncing from block 0. You can use snapshots provided by the Base team by setting `BASE_RESTORE_FROM_SNAPSHOT="true"` in `.env` file. ```bash pwd # Make sure you are in aws-blockchain-node-runners/lib/base npx cdk deploy base-single-node --json --outputs-file single-node-deploy.json ``` -After deployment you can watch the progress with CloudWatch dashboard (see [Monitoring](#monitoring)) or check the progress manually. For manual access, use SSM to connect into EC2 first and watch the log like this: +- After deployment you can watch the progress with CloudWatch dashboard (see [Monitoring](#monitoring)) or check the progress manually. For manual access, use SSM to connect into EC2 first and watch the log like this: ```bash export INSTANCE_ID=$(cat single-node-deploy.json | jq -r '..|.nodeinstanceid? | select(. != null)') echo "INSTANCE_ID=" $INSTANCE_ID export AWS_REGION=us-east-1 aws ssm start-session --target $INSTANCE_ID --region $AWS_REGION +``` +- Check the difference between the latest block timestamp and current time on the EC2 instance: + +```bash echo Latest synced block behind by: $((($(date +%s)-$( \ curl -d '{"id":0,"jsonrpc":"2.0","method":"optimism_syncStatus"}' \ -H "Content-Type: application/json" http://localhost:7545 | \ jq -r .result.unsafe_l2.timestamp))/60)) minutes ``` -3. Test Base RPC API - Use curl to query from within the node instance: +2. To test Base RPC API, use `curl` to query from within the node instance: ```bash -export INSTANCE_ID=$(cat single-node-deploy.json | jq -r '..|.nodeinstanceid? | select(. != null)') -echo "INSTANCE_ID=" $INSTANCE_ID -export AWS_REGION=us-east-1 -aws ssm start-session --target $INSTANCE_ID --region $AWS_REGION - curl -s -X POST -H "Content-Type: application/json" --data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' http://localhost:8545 ``` ### Option 2: Highly Available RPC Nodes -1. For L1 node you you can set your own URLs in `BASE_L1_EXECUTION_ENDPOINT` and `BASE_L1_CONSENSUS_ENDPOINT` properties of `.env` file. It can be one of [the providers recommended by Base](https://docs.base.org/tools/node-providers) or you can run your own Ethereum node [with Node Runner Ethereum blueprint](https://aws-samples.github.io/aws-blockchain-node-runners/docs/Blueprints/Ethereum) (tested with geth-lighthouse combination). For example: - -```bash -#For Sepolia: -BASE_L1_EXECUTION_ENDPOINT="https://ethereum-sepolia-rpc.publicnode.com" -BASE_L1_CONSENSUS_ENDPOINT="https://ethereum-sepolia-beacon-api.publicnode.com" -``` - -2. Deploy Base RPC Node and wait for it to sync. For Mainnet it might a day when using snapshots or about a week if syncing from block 0. You can use snapshots provided by the Base team by setting `BASE_RESTORE_FROM_SNAPSHOT="true"` in `.env` file. +1. Deploy Base RPC Node and wait for it to sync. For Mainnet it might a day when using snapshots or about a week if syncing from block 0. You can use snapshots provided by the Base team by setting `BASE_RESTORE_FROM_SNAPSHOT="true"` in `.env` file. ```bash pwd @@ -185,30 +185,34 @@ pwd npx cdk deploy base-ha-nodes --json --outputs-file ha-nodes-deploy.json ``` -2. Give the new RPC **full** nodes about 5 hours to initialize and then run the following query against the load balancer behind the RPC node created. +2. Give the RPC nodes about 5 hours to initialize and then run the following query against the load balancer behind the RPC node created. ```bash export RPC_ALB_URL=$(cat ha-nodes-deploy.json | jq -r '..|.alburl? | select(. != null)') echo RPC_ALB_URL=$RPC_ALB_URL ``` -Copy output from the last `echo` command with `RPC_ALB_URL=` and open [CloudShell tab with VPC environment](https://docs.aws.amazon.com/cloudshell/latest/userguide/creating-vpc-environment.html) to access internal IP address space. Paste `RPC_ALB_URL=` into the new CloudShell tab. Then query the API: +- Copy output from the last `echo` command with `RPC_ALB_URL=` and open [CloudShell tab with VPC environment](https://docs.aws.amazon.com/cloudshell/latest/userguide/creating-vpc-environment.html) to access internal IP address space. Paste `RPC_ALB_URL=` into the new CloudShell tab. Then query the API: ```bash -# IMPORTANT: Run from CloudShell VPC environment tab curl http://$RPC_ALB_URL:8545 -X POST -H "Content-Type: application/json" \ --data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' ``` -**NOTE:** By default and for security reasons the load balancer is available only from within the default VPC in the region where it is deployed. It is not available from the Internet and is not open for external connections. Before opening it up please make sure you protect your RPC APIs. +:::note NOTE +By default and for security reasons the load balancer is available only from within the default VPC in the region where it is deployed. It is not available from the Internet and is not open for external connections. Before opening it up please make sure you protect your RPC APIs. +::: -**NOTE:** We currently don't recommend running **archive** nodes in HA setup, because it takes way too long to get them synced. Use single-node setup instead. +:::warning WARNING +We currently don't recommend running **archive** nodes in HA setup, because it takes way too long to get them synced. Use single-node setup instead. +::: ### Monitoring -Every 5 minutes a script on the Base node publishes to CloudWatch service the metrics for current block for L1/L2 clients as well as blocks behind metric for L1 and minutes behind for L2. When the node is fully synced the blocks behind metric should get to 4 and minutes behind should get down to 0. To see the metrics for **single node only**: +Every 5 minutes a script on the deployed node publishes to CloudWatch service the metrics for current block for L1/L2 clients as well as blocks behind metric for L1 and minutes behind for L2. When the node is fully synced the blocks behind metric should get to 4 and minutes behind should get down to 0. -- Navigate to CloudWatch service (make sure you are in the region you have specified for AWS_REGION) -- Open Dashboards and select `base-single-node--` from the list of dashboards. +- To see the metrics for **single node only**: + - Navigate to CloudWatch service (make sure you are in the region you have specified for AWS_REGION) + - Open Dashboards and select `base-single-node--` from the list of dashboards. Metrics for **ha nodes** configuration is not yet implemented (contributions are welcome!) @@ -238,7 +242,9 @@ npx cdk destroy base-common 1. How to check the logs of the clients running on my Base node? - **Note:** In this tutorial we chose not to use SSH and use Session Manager instead. That allows you to log all sessions in AWS CloudTrail to see who logged into the server and when. If you receive an error saying `SessionManagerPlugin is not found`, [install Session Manager plugin for AWS CLI](https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-working-with-install-plugin.html) +:::note NOTE +In blueprints use Session Manager instead. That allows to log all sessions in AWS CloudTrail to see who logged into the server and when. If you get an error saying `SessionManagerPlugin is not found`, [install Session Manager plugin for AWS CLI](https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-working-with-install-plugin.html) +::: ```bash pwd @@ -248,6 +254,8 @@ export INSTANCE_ID=$(cat single-node-deploy.json | jq -r '..|.nodeinstanceid? | echo "INSTANCE_ID=" $INSTANCE_ID export AWS_REGION=us-east-1 aws ssm start-session --target $INSTANCE_ID --region $AWS_REGION +``` +```bash sudo su bcuser # Execution client logs: docker logs --tail 50 node_execution_1 -f @@ -259,11 +267,12 @@ docker logs --tail 50 node_node_1 -f ```bash pwd # Make sure you are in aws-blockchain-node-runners/lib/base - export INSTANCE_ID=$(cat single-node-deploy.json | jq -r '..|.nodeinstanceid? | select(. != null)') echo "INSTANCE_ID=" $INSTANCE_ID export AWS_REGION=us-east-1 aws ssm start-session --target $INSTANCE_ID --region $AWS_REGION +``` +```bash sudo cat /var/log/cloud-init-output.log ``` @@ -274,6 +283,8 @@ export INSTANCE_ID=$(cat single-node-deploy.json | jq -r '..|.nodeinstanceid? | echo "INSTANCE_ID=" $INSTANCE_ID export AWS_REGION=us-east-1 aws ssm start-session --target $INSTANCE_ID --region $AWS_REGION +``` +```bash sudo su bcuser /usr/local/bin/docker-compose -f /home/bcuser/node/docker-compose.yml down && \ /usr/local/bin/docker-compose -f /home/bcuser/node/docker-compose.yml up -d diff --git a/lib/base/doc/assets/Architecture-HA-nodes.png b/lib/base/doc/assets/Architecture-HA-nodes.png new file mode 100644 index 0000000000000000000000000000000000000000..c5999fff1696acaff57f13c68670475d6ed69d8f GIT binary patch literal 151628 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
S4BGOGZyBPx% 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 lM|P*t_`HV#kkUG%&7j2I!g0Rharo@Dty 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�!@FlE!x!E8`;E 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 literal 0 HcmV?d00001 diff --git a/lib/base/doc/assets/Architecture-SingleNode.drawio b/lib/base/doc/assets/Architecture-SingleNode.drawio deleted file mode 100644 index e8b44230..00000000 --- a/lib/base/doc/assets/Architecture-SingleNode.drawio +++ /dev/null @@ -1,77 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/lib/base/doc/assets/Architecture.drawio b/lib/base/doc/assets/Architecture.drawio new file mode 100644 index 00000000..c51343a8 --- /dev/null +++ b/lib/base/doc/assets/Architecture.drawio @@ -0,0 +1,194 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 69c8344ed1498dffc95b8386854d39f60611859e Mon Sep 17 00:00:00 2001 From: Varsha Narmat Date: Wed, 4 Sep 2024 11:22:29 -0400 Subject: [PATCH 74/75] base premerge review --- package-lock.json | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 449ab8fb..fd1c89fe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1741,12 +1741,14 @@ "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -1967,7 +1969,8 @@ "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true }, "node_modules/constructs": { "version": "10.3.0", @@ -3397,6 +3400,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -3735,6 +3739,7 @@ "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, "bin": { "semver": "bin/semver.js" } From 5cdc38ca17753b737a70608e750b7dc78271aa6f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 4 Sep 2024 15:28:11 +0000 Subject: [PATCH 75/75] Bump micromatch from 4.0.7 to 4.0.8 in /website Bumps [micromatch](https://github.com/micromatch/micromatch) from 4.0.7 to 4.0.8. - [Release notes](https://github.com/micromatch/micromatch/releases) - [Changelog](https://github.com/micromatch/micromatch/blob/master/CHANGELOG.md) - [Commits](https://github.com/micromatch/micromatch/compare/4.0.7...4.0.8) --- updated-dependencies: - dependency-name: micromatch dependency-type: indirect ... Signed-off-by: dependabot[bot] --- website/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/website/package-lock.json b/website/package-lock.json index 7bc3b654..121e5de1 100644 --- a/website/package-lock.json +++ b/website/package-lock.json @@ -10207,9 +10207,9 @@ ] }, "node_modules/micromatch": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", - "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1"

AKpZRgDWo5NF`KuXh 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 Df|X)J literal 0 HcmV?d00001 diff --git a/lib/base/doc/assets/Architecture-SingleNode.drawio b/lib/base/doc/assets/Architecture-SingleNode.drawio index f6fb4275..e8b44230 100644 --- a/lib/base/doc/assets/Architecture-SingleNode.drawio +++ b/lib/base/doc/assets/Architecture-SingleNode.drawio @@ -1,4 +1,4 @@ - + @@ -7,7 +7,7 @@ - + @@ -55,7 +55,7 @@ - + diff --git a/lib/base/lib/single-node-stack.ts b/lib/base/lib/single-node-stack.ts index 0292d9f0..07854438 100644 --- a/lib/base/lib/single-node-stack.ts +++ b/lib/base/lib/single-node-stack.ts @@ -116,7 +116,7 @@ export class BaseSingleNodeStack extends cdk.Stack { }) new cw.CfnDashboard(this, 'base-cw-dashboard', { - dashboardName: STACK_NAME, + dashboardName: `${STACK_NAME}-${node.instanceId}`, dashboardBody: dashboardString, }); diff --git a/lib/base/sample-configs/.env-sample-rpc b/lib/base/sample-configs/.env-sample-rpc index 2edc9750..a4b86c35 100644 --- a/lib/base/sample-configs/.env-sample-rpc +++ b/lib/base/sample-configs/.env-sample-rpc @@ -19,4 +19,4 @@ BASE_DATA_VOL_SIZE="3000" # Current required data size to keep both BASE_DATA_VOL_IOPS="5000" # Max IOPS for EBS volumes (not applicable for "instance-store") BASE_DATA_VOL_THROUGHPUT="700" # Max throughput for EBS gp3 volumes (not applicable for "io1" | "io2" | "instance-store") BASE_RESTORE_FROM_SNAPSHOT="false" # Download snapshot to speed up statup time -BASE_L1_ENDPOINT="none" # Leave as "none" if you use Amazon Managed Blockchain (AMB) Access Ethereum node or set your own L1 URL \ No newline at end of file +BASE_L1_ENDPOINT="none" # Set your own URL to Ethereum L1 node: https://docs.base.org/tools/node-providers \ No newline at end of file From 7dd57be446dc1fdf7e6732fef1226c1f7d56bcd5 Mon Sep 17 00:00:00 2001 From: Nikolay Vlasov Date: Fri, 2 Feb 2024 17:39:05 +1100 Subject: [PATCH 36/75] Base updated documentation --- lib/base/README.md | 2 +- .../doc/assets/Architecture-SingleNode-v3.jpg | Bin 48320 -> 0 bytes 2 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 lib/base/doc/assets/Architecture-SingleNode-v3.jpg diff --git a/lib/base/README.md b/lib/base/README.md index 48c4b453..9041e23b 100644 --- a/lib/base/README.md +++ b/lib/base/README.md @@ -6,7 +6,7 @@ ### Single node setup -![Single Node Deployment](./doc/assets/Architecture-SingleNode-v3.jpg) +![Single Node Deployment](./doc/assets/Architecture-SingleNode-v3.png) 1. A Base node deployed in the [Default VPC](https://docs.aws.amazon.com/vpc/latest/userguide/default-vpc.html) continuously synchronizes with the rest of nodes on Base blockchain network through [Internet Gateway](https://docs.aws.amazon.com/vpc/latest/userguide/VPC_Internet_Gateway.html). 2. The Base node is used by dApps or development tools internally from within the Default VPC. JSON RPC API is not exposed to the Internet directly to protect nodes from unauthorized access. diff --git a/lib/base/doc/assets/Architecture-SingleNode-v3.jpg b/lib/base/doc/assets/Architecture-SingleNode-v3.jpg deleted file mode 100644 index 99e05c260cd9207eb7e32f058e667a57c1b5daa5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 48320 zcmeFZ2UJwe(kMD6BuGvIf=ZICWF#pF12ZHUa+b^tIZ9MN$r*;^5QQNPFd#5P6j5>* zL^2FPiIN78EC}B5`_4O`XRUkw|K7LWdT*`!PS2vd_U_tU)m7cSyQ*vRbMogG01X%j z0s<~v001r!e}JD07bZcE9$7tx=m0^Qs()4V0Ep@GJpjPf&BqI(rhLoT#PrtHFMmDp ztIpci+v8XHZxXTH!|`9O0{|n!{|4v3x=m(h?`=y&*dhLLdl88fnPnit432-ne7|6u zzhL=a@KYZTA0p1VW$00=-0p!}=<#A{-5F8}~!4gi2lNq^t7K>z^t;Q+vO{NMMuUjqQtVE{ng z;NSQDmWhY8m-U~`T_)xi9UK6F-4XzR(gXmY{{#S#oBdfw%>E5;w}@3YhF8*OQknlM)}Kp&+9m-u>T9Ki>mr zNiRrWX1jEO6L69C!X?@ZKRW>IL~&odbb*-uuigSku3jM}yL{>5uWA?#0C0uKHpw+A zD$+}r$*vI9bMeyUDA7x-jserBB{R_ zAu8q4g0V*DElMJ$1o4Cq(OV}EU*-HG zzq+Q4QCw2rid=qFVO-X0M=jqv)lh3kZE2G zt6Rj|T2<8G(-+1-UZlQ&O#4*RxxBity24$}v&A3_Hm;PeA;ORwGQ3UsVK@fq zK6FMC)Q0w+D3dT2HIAvtBl~_Wi#mrath(v`_4s|Ul2rT+si=uYlpP$`6Kpl?NWGSl zY@BH17GgMy99freG>H|~Y_v+`u;T)W=%NihB+M0l9OJr^eM6gHwoG|q z5`~V|u8x!BZVX#EC3E|`=SnU0YHqi7zQR8RClw0)c$y;(iiFjOz)la*{kkaA$EXCQ zbp4*buxUqD+q?Xi+h#%z1xPm;U=YI(_LI2~WBfy#X@6hMj_>0KuL-h;(PYyOY!G(W z9f)=kPY?yYLK z;Z~+0y;x^dRwZ=Q@xGCs*NqZ(MJ|s)v8asbCB^zXw`F5|dij;ABuN6ilBj#7YM-+m7 z)vNz=-80=IT}dyjL6d(Ca!Z&|njhqO{r%Q&sK)PEic0%1f;oRP8~QiqZCijtI#S?M z<AiyO3!bSFYUnTZ6v|fax9y6eji`(*F^-`hSMmKURYOOs@ZfmXzB)XL7>L zexHWRj_O@yuSms$qlf1rL$zaOoHjS~Y=uqZ>r_%#iul!%T_ss|UJ>3bd@f+yY-{}q zPz_Ku{rg?oKWeBXplfDjW+zToSOFERS_qE@C>O;qa%;^~ zG6};MFQ}B>t{gRN|E8zM2!k_go6;(J`Z2+8%+N*;z6^oYqS&gJ*VFs8`Qh^CE7Q=U zNPQ??@yk2GgHS} zzkAm1YjgX0Dds!t$G9xl8^IMeXaIc4nVA&r>>$2ex;yFxgXFp#KX&W$wEKLu%$~C9 z^wH~g;UQiqZ~Sqdml02y0T}I7;L|;-0^HcDD{83aoI;?0L2?V{bJhiebcSD#HGTp- za^}bBtl!bKA++0r6tA?h2BDs5c+%Nvqj{3~_Qdh3ZZT#Mpx{n}GT zo6S%iZiDikxG`l=6E>?SA&n=MmT!O$404`?a}L17aX6eB`Uid6sU_0QVAZhPvk3pV zNYAMnl>Ge1m5LQ{zbr(Zb$&Je;Ft9u>HWZ1o(`^35LaF>Yp zL^%R%wv%cAfP248?*DHRmGH^!cR0bHfPJGO&#Ta@OlLHo9TJbo?W?~ zfWkinuoiv!?-;z|9iIEnGjg%QsvKi&Oa7HYA$XCx+ql)psjZ7HL!9Uqh)yD!>i)xv z>ai0S6a)fwiiUW7j*v}U#hS7!#fuFvn423?=*Yk^H~7kj*@=XPXOjM+cITaUv!m+^ zw0;7btA>99&aaO(xl%DQWdJi|CoKGGJsT=BMn3EH*m_LNe0qz3-0rU(Yj75$`PcyDIMq<*N1Xz49qx zZFpZ3aP#g*TLn+U<)JYhn=1+^_fdNv0lLjldwzuhXWXc|?hcm-)#^8u7TcQ4(5FJ$ zsp`f2=rOk9t?;6mBT1>kWY#?ignfCg{d)xpzl5rRyLz7#;w9U*i2(a>Hf-@??^1B< z{YbE9c^t)^{B;i|v#`|6mVvC-h|Q@X;RUu6;95VUdF(q0>{SX#*y% zJnahRG_A|9FGu8g#Knrq*W8;${vr{?8rUk)qG|PaN7(-s<)Qi0wpA}jwOBBJ_(ehL zrgu>@J?VQLcs9k3T^u$o&%Z%xMN!OsV$FTv595M-daiEhp=qSxQ;n|0tW4SpCtz7T z(46N3;^@*}BIM}HBs_x#=5eGh8eM17;adKVlhunie_|q^nDJFKyVyYFssIlO_Mw~D z4X0T*6)5kiot=G_v$(RBz;vy3@=}1uwNG8!lvbIRg=wUU6Dt`gvNdf<&p+&T6|J({ zi)6;SuB7yQf96g9gmR9R*e&2+xOe`pq57M$)(dHW*pJ}@wzrZ*o|1C^uKA58DZuRf zFEP38jD0U!R}TCqxA*=g+%b9SX#{bE=r*?-{fDLhNa;Ugsqp`R85hjY7_3~hz$i7+ zEa*poutVZ?OY?o!#dpl@dj{KFq6T_(;KK#!Xasl9Co>&U1)9uyw{oKe?1#N3^~xei z5Wi~ktY7^=5x#F-PamFY)-vH7jJK%B0sy9^2D76@7w#>g-0S*G!Rg*P1RKB7>(P)PngAYANdyfFD}7Z(mHB;nO+*kX~2mjx9hYkGTm^2;~f z-p{jxZNqh}nOpLn+FNPlkC!K)GkG{%mAXbMF{%mm`l&}axLPtb^_tpnf=Gg_*#qkj z8iLT?5?=h*G?C6KwnO8CcZp{=t;^=Q;_)NSVofnn?mVG^!_x-iX$|+AJ9CRkuX046 zz1oW`3mawS?N!?uhJl`W-?rreK|mleF)>_R!?j4o&(^DhBJ$~mBk0m$j4*E+k80a5 z7pYaC^n%@(w)V~O!mJ8mtUx9<7v>5Q8XbQ=HpV5k&0;@0RsHF6;mNcP8+#V^OKLHJ zMJ;lC6Qcd&J1@GcZ?=e?Z}o~036n7)i+7PRK=s=^e&om-dg$3|UHuTC@Orma4-?n1 zaX#EJ#gOH-k$3T?vYHP9YF`GHAc%1ZBbEfIDjS;T33{h%=gK-oPhF>Oz5t$$Fsy!$ zEj%@I(w&wm^Vt|s)iwk}K%T>2hZWeS{8^j1C}mS`O^3P7*i6rV(LQNf*p9^NY?pk$ zYGiJ)bBH?I{|UH#`ND^1)A;AO(Y^lBm7f4xm+j^GrNn%>^^Mqf9AgjORaptN*z8U}sn?bVF0F<+^mi z59)MJ^0C77s&4>*2>+$nXHo98g4(lc!m9StSNW)LlXIl6cM@CxOou+dq)68zz+(tY~8Nrh*h2iUoCKcm8 zfzMb7?iivHCVb)=2Qn5llMoQ}JGw4C?A@B^Gp&W6|AjP1|^dw(B= z@)up>ct(NwwH2epn}IP$$b5o1iB#Pulo7R#*jxY8QKJ@yqBD`?XIjPmX)*d@22x~7 z8nssCxR8#Nj}2h6@-Se4TT`J$M`4s{6OZAo37_7al7)kxjaMhyo+E#U(u z$K-}Wen$l2&`fhsoW-qPn9!v_e`pkrKz2pei%={)V(x@|E7muLaea%A+LQ@LG^nTq zlsn+UjfD{yCXpO4rlOX2cK_7GkixX#@|%X6^!_$AT61$JhCckC-TdPH>~mgm~NQ*=%GE27}Elj(ZZnuHFb z8oVZzgD-umvTbcB!M)Vm3>8KAfy`Y5pu&n9lxm=$4$V2wM_c>9QT z{N}7377UqpTc#6NDJi?#GvOOe`qX$peG`J#6V_h>O|@f8UNbu=WZv4uGhZubX2;D}jwyGZx(`_X#1R-?QU#PXOH%?*6C4qMLCBdQ90~5HhMpUsDCZ9LJah*-^YnkrZ zx2$p#iF!T=G<{#j2Ss7#j@b~RQ_pp^!?4x};cA6i7Q}l%0Ff@0Cn%R$FIaSrg+5>P zD{C!IA+_{*+4Z|{iEQ054d~x{&)WFxzxRyRAhh~zcy=w7BI;B$AYV^ zf?1@B7o@Ma8R{#QRa&p+D~!`?&WtY*?>Fu>Cw+vh#)Ev;o&1DDI9 zXlFS$Ay^J)r_(OAT+2ht^T%cEgPvyEvV>N%EFHZi(9`@mh-6P~znI94TKVRw#Q0g( zoE!MP{CHgUjTGmsp{bozdwgYT!S}q!WpAdl#27sgrkPq+@G%Qdh(*E->k`;=pk6PA zG1AgkQ2OYqdNgZ{vVNZ=q!5t{=IlvqZLgJ&rH{~79Os+{Clt{olr!GRIMC{Vx9%XK z_8Z|}d;MJ2w0o0wmPkNZAO^mbydYoFkhdpOTZqNaLz7> zB{$^E>bdYh8n7NDtHm;JGl413Gp`;%)SdO$ThW_2jTODAyA;6*S}IA=92C)qAGJ12 z?Er3C-W1Oyy2Spu{x@JM3C&zPq@SB?a&GebR2{F}J-2J6U#E)w3Aobs6QJ@OAk^;- zxGR9=XQuohf`APhB^MjOfM1+J>2^>h!YLl#cyx7y%3;cn@y$86fdNUa7ei<)uuxc~ zu*-zWk$-7t$!h4R-(h6nUQqFN<6YB&4QU07@4JS?1){RE=D}({&t$&hH(HwT8;X}M z041yIQTGK)Ri}6+zlw-#*9gWp)K%FSI4@R@6{%f?dBoOcr%i{H(SE6DwmY_Zse`Z< zBz@EEOxAsJyYKF5jzXLB4~yW|Pd1f89hyg% zc_-eDctLjFx|fLxag4mK9qKq$o6WXKIkF@d_Qx9zy}HK))>8zVwWoTS)tfIeb^Y?&63b2#OMeX*E45@FO3*k?h^(S`F9+7UUtgc zGeRDF1ncQ!LZ^<=tY0Q*0GA%)Wp$hbCkvllxOog^QHsU3xIi8N5`Rd_n}d_7;+}VoR9u-SVOUeq%)nt`yh%Y9$Pg`Eumd- zhqSfHw$cabGB)YY6N6;DgAkFfXV|X`_jOq7TE#0?87s`6iJ$hJD~)YjefyyJd4N@q z6UH)qXw?(uKK!b*KX(HK0zr;=&44HekTTq%h^0IX7Zp|K5RlQ<%1Sb?Q?6H#3S7U2 z)>h*|sY6GFLAtj_9ngB5p!8})x`RosNkZ)1LU!c!l|!E`xZlZ4-n84;^z@=@$bu<5 zbzN1z;*Ry2)W9=grP2*<*<#II<+xf-`Tn=!3M_0}y{x_YKLP0G(4Cdm2oZt9TgHBv zm9-27!oxyM-x}YBy>-oQUng-f9y-GUaW09{+J2#))Am-E_$v8l7jwmGbiIzfoiUAu zV3w8PJAXDv6&HVG7e{|r{C28(!a6&26#farmzu`iPuw$4+S$+Ed6+9hkL=|e z6p@>bMOm(_hd>yUH+7$?A0{n7jWBtY>|^KN%q=S>Jqzc=G`|crBl;jsXUHQo#Rboq z!0e|KP17kk(Roi=3}EGdn3kjmT<^KqabFNg4g;?a{Qc}bBZpX|?CGpL$#3LTW1aXb~-Uthl&Bm=a;` z<%&C!D`e{SFP!9;(D6Jd^_vBA*A8O0IG_+6C<}{6(phA^&5HBiXyJgvZ6$BX-Z zr~2<`6CY2-HfbgSE+he*nc__)otC|wmcyU&^uD<6Zdg86k$Gcn@2lyXn<*k1Z&=K# zWY_VMG7IBH{9s+gpDC6r&@Yx_C0k(0Rj%iS@MA{9eFBrhfQ&&8{%JDh8eS+U)1y2Y zv&{*>J~S{J!5AM&%o}k*=TxLioT|5=ZI!^0w^0|zE*Q9L582L)gqz2g1klXo6m&P1 zkTje{=Z=hKNq!#1*ii#*8Ple(^cW!Ws<5jTsZ8mXgx>m;LcDhmPf*Ut+ z@!|hg*qpn&-RQ?_%#8Q(s$RLcp2Tzw=;(^toY+=!hDSe?+^eymrA17sMXAw#Buf{u zrE?ELrudDchWc)6-e;D9-?Yv4o2EU2tdM4R+|3T-uShj2@*g{B$puwd@jOkTRY5Wfe10L%H8b{XVF2dsfeJlcF9|qxyFT%IkiZH9x0g({Qyt>`1M~niKk~ z?_RFRc#l=R5&%VqUbje~dvllBL8GROB9YaVv)VvePou0mfCEikIOsX*c~=VQpA?6e zPYp;W1OBE72FB%8izm;x7sD!m&HST*S~+y`PN}i7!^m1yYpAlgZ*SE-l@h~x!>#dN zEFImHbFK-M-NGPKs-E?zCrEE?cPfoZONy)FU4K~JjB=6#(c&kbJrXx*FMB^kK`h&2 zWwTUbm2E6wNL|h7lr?*yUP`)glRG9Pj+4o*hHZ=@Nlf9@)>gEe8)A`ki7JJ|1>NB! zTy_x%m#9I`yTQcMx8#SxsmLYAY~)kQm!U2brGt`L!H-*DEqBsCffR zA3Galf%mF65*8>A&Pf;#3&%$p$87W zFZ-6DD~~|(OQ*zwx?@ap956e5=u&+*`GB(E)@El#6IGtPm#&JGtHE3YiMnW=I)3Km zL=mhG|K-iQpjajN6fQkQw>8<$KHWaA<<(hFR2`p=jWZLL_Aa;i{tc=j&Ir;Utn2|E*F1XHU&p4de1b>#Vr`na;L6Kmmz&d zL7bf2n>|qtzQ9`WsxACm`yFz{Rdi2wzm=Y1x0x_vIX!?{b`41PvYb6XGL^ykzA4R9 z-G2L{y`_d(5w+*5<07?%JB*_ojyl`_VYL7Nm_z>cuoDydFHRzC*hcHcI5mC3EdV8C=dZ2ezt1oJiz6uf zUrs{FG!S6`fC@1*{PJ(sF>f2aAz=*m!>~c|DTv-$m7_)H<_O(id+S?(E6ZR0B=*p= z^5j`Z&0Ly}Hw38=LPo2aoCOmT*F!wFi`qRJ;L@54n4&@-&FCe_AR*&OVu)*-L&yjl zby2K)wwe8Cky(s+dRm@Gs@-_i30*`p^()1+y7*wj&*^mT>kwUCi#$^St>fnDO|?%q z)r?tXLgH^|GEbB)O%uM%Kz8PzV|w+`Wd|I2Q8!RzbNzI>_9!+}rMWa{2V0n&94A-)Fo12j;mhdi47G44`&`2{%9S<4Z za%CDkRmpd!t_&1SD90QsvN2m#@$sH*&799U4#+*fs&fG=IA3~T%bU>%J@f(q?vF$f zg-*#aJXn9N>OJTATKqh#QIQr?tsFCVPdF&+HZt^jwCg==i5TceJyPvNDXydj64N@7 z_@z&jU0H$vTSa~LT~~I)FR;an=&D3?8ZV|q=&YwWLB>j7awEz^{~)paZgaEJ@T*EQ zR9vJf+~%5kqndfNh9?&8s0KC%>mxbExgKm^KLERyZ0uI@3TxEJBOvU_a5Hc;$vgtV zjzc>%8hS;Q91^`xK zoKa@lVM4oHFS4^VH<)e@-7!zgqY~b0ZH)(tNVXZWUM&bqRx=l0+}!L97u{A%E7+`|Q2Ttab)FM7m;YWkn# z++ZjzxpDMm!oBKl>zsDPMSaf2+#jrjgXN>s0f*XZp*2SG8=1sWTXtD4|Jav2wEp%O zf9xH85n&7`tXIG+2*rKCj{8*GuXwG%#bQ5jY4WM$bc|%v1$FGk-4E%VN}g8m1wpzP zxxhJ;LnanGHtYJXJ@F=@N~fG52-UhM&;wGIit&g157QjM_#XHI;Ba|cojs& z+03p?mZqb$bJf=MO-i^VSr%OT;m1vtYHMj`SSQqIZe?f15WcF{^U(Ho{qQjT713TA zETs@D$86NhU^sA|*45)jifa+fSGX{}fD;l`hhQdg;6C)J%q%5LZ!d=U%+K#|AKKOO zmdW{@9)Gs^QJ$U((l0*gPii zL6{0L$cW}V{^aqY&mtwA8W%9HrsTR4{1^aWV)|{#W`Vimi+17yBh#}EQNdgs`7Kms zye|M`7t4m^CaXiArJddiCWjhlV7?a;&)sET_r!1AZB5r|e7$!;=R60#DN^4`qdd zir$o`>u(GFX{c@b7$0d^8wsK2jHM@6tGCkIDoym>zWLKo`+uCAVQLQVzH8N(_2W`g zrFk^4NH=Ll-DANbiQ}2Z79V_Ct1d%J!!*?n4x&XMl4)w*6D9R;UvLaIvawwc5dSFX zp8%LMUi?F{Bq>mfX+O0uw5>j!dQ@2B5($@)^%2fG*tmq)$)^;>bCqDu@!{sW&BXUy z&(>mmf>FbWKDMV!3!8iW-xPCKN4XSY!y!f|$v-Hbbr#*Ft!TvbT^TcSIDN5OB*}6D z?`?{MXpiH$#JJINI&Q%??%%XEX?fh}G)CsqDqBMCwpPwB;M+ATnc*3vz9;;atCQTN z&!x_BWLb_?(>Envet3YwRTUE#SC<@XlI>hyIc^#qXLt>KioTVUS;6BYv>8?BoauE( z@eq2~pMRo_AaJk5u%x>Dip$3>x}m-a&kS3r#Dtt&eN3vSnQX;Z?Yz38JHc-m7PpLQ zQiC9_l2g8R<M5@gF^r< zljHk4!)2MG@Opp0&V4M?><6#wj~usIv7`B4^WxD%YlFHT#ellRix z*L+Z$cV9SEt=ME}D==Pp8wyJ{eKi1DbWp7J6NQ-Ki=p%4{bKT`#G}kCNa<*LhJg4I z7(&5~`l?^cxy|IDNMSl-n(gUOR5}2LB#MhG(u?2x(B*n9sB6qC%y8uh*Z+cAhi?neJ73)J}Z7_ro{#z%Ne7n&1203jitc+ciMg zShKHbD`O>f&ald)GHkX9Z51AEz)NKlc){~ zMJ-08GB2henao)puEU+kDZb6ZQZhHF_+^AVM=XRjXuqk@hzlH7>pGq-l$u8Q6F^(u zO6@W|Rv-sY=>I5{ox;QG+Vt|+J{jS$Dc?WG0UfD)A(9G~!QB`YEOLBSkzr5hl$_i= z_6SMeZlPJrTK!?kOwTMJ{5ewQ-LzF^pvUOYhtxXP%+6V3n{eeQ_rBmm=KaUH>!TqZ z89rSdqR18MY(lTJ{MT&Iif2N>hjUv_VEsufc(T?tOZIkq-P*NB$}OgFU{T{vdH~8Q z`|#;ir75dN(-8lt^U+l3i23F=Z_A8ORrxYfu6X1`vTHkta&}X*RD}(w% z(>6R=E+d2n@VJT;R{w3y><#e&*pGE_`R43P5&*@}_D?0@NLB^|Bm>-(o9GnyCOcLV zg%TLESmvD$tnYjQU}hwWl!UkFaOejei*+DZNJBw)F*OWj`bKePr`M9Mxfh#x zPoKCGh+&8K_WRv}o@TXxX@HCHaJqQ(hDEbcJI_SREik1G+J|f&mIFmjs z{t0;H&JeV*5V396aDJ^9&~;*8uh3e2KKgl&TC?tJ{r+cs*17ec&9Jq&Egp+=Ut(KJ zpE*3wI@NoQQ}`!O5#Q9_z2kE`XiofsOQPlmx9|o<;K&O>B0REP5msPaY^Z$Hu~fze zN41?JOL^xkV;@~8ChZmA0_UwiJ8q9^g=Msfu5+0F_0o&jmo3dCeiKSwuCi^@MR~46 z*hao$DlNlk9|}GBymUTya{mw5i@#(hfaYnofIQ%1x-(v8j8!!#tamO`TfW$FGNu9e z=L_*K)IW3ba**4Eeq!I1=!3F!VO;~LFn&Gx`@?EQETpsS7shQ#w^_rgPUq0CKFLzQ zhm#;_M=eEv^!%eBMv(ktmJpwj$O3Oo*E|5kpBD2EJzY~(tdM@p`#*;?zt->Dy- z#u;BLDRR#H&{wkQ_Wag9+WYvv{#pFsk_;uj-tz~`$L(LL`*%oeSfai}|5Tmm6S2QZ zh7Mx~zbxw=Nrd5nzUT`yZ2WPf>{-{>U25>egU~dc$(eJdW<%oGVe8~L zIYt)v{oBRujmKY$TZ-rZt;OwXE>bsCy`-<)fCo#*z-z?$Xj_Q*#=gy4ga#{wsi-(O z&fU5?gNv#apA=+`fExolj@BetXO*||SR6)%2(l7=D+}`;`4hJ>i!PGt+Yb{jVr%nGb>HOQ@~o|N-D91GK;XA9nvfs;lmvA zOTc`LX>49+*+@7*Mzn4<(4}%m!7abhT&!I`#|KH@b7XuxIIAGUZzgvVZPNH1>wVM7 zRO3s3Elcr)NC^Q{Rhk8poC0S&G%HuUj6~v{vcHJ|3aUfel7M;w^ z{b`T7E`!bUP8%2;)zd$B@9J2IX-J`bf3#;k9awodpa`T;XlLK{=um)P=HfuXZuxaG zFNG&sJZxpix~ZOe2ab_Zzib${7|;>hQ{0|KA;}l3Ph54s)tyHaO*5izx`Q%&%;Bf{ z`uCRE_w!ImonXBt@+=oBARk@Jm)%x3@hHMDjr5BVL%SSI z5Z9iGE_5=J3Vq#@P3HNY;4mOdE64*1?zax=5n&W;u-FshD!hlhB%*0qAsy$x$3GRF z4hqas_m(J}9+YrK{&8&?E1Xd_XrhKWXqM`k#0Xf=jo?!}+B>Jr&=8Bs=W{wQkEd7|y=S9vUIzj+>}=b3^l*`~=uz5E;SUC^V&;M!tn; z$L5pL(c+3dQ&Z(R?$eINK3xA>)9@rb={ID%bwl-a*9cS4?Ye0Y4bmez&yrV>`< zl~G%WRwDSizc6Q!^qC&qwe^oXF`n}vzN*#*XVZO#cJBm`8=(>M6W|LoBT3xavU|QR zT0V4#)+J^22T#}iWGj&4r(tRJRZH{BFjp}6-EMLcUL-2PZU4UF+(vp}W+ysh?xqsy z{%5!DaMP)U?WHIf`yhKa(ra4x?gs1|3uX- zhr@@TPeQgMqSwh(r@_uv|8BNyi;sDOLG+}q@b|iu%3EbVC5ABGB*%60ZKOi<)3IA) z$zL&JSa2?G&FDx!|AwOQu12^(qQ-N$PHpvZ4M?wJM?>h{*Hza^(u45ePof-Sp?=eF zY4sFZL_W|ANyS_SZR3(pO__ZDig+d{o|KSf8n3AU(k(;tHA{RiZbTZCFcZJs*MD+N zOM`$s<`!$GhnzZ{laZJEYKr#}kH>#`VgLY|3Mj$I>-=)n+-)RRKy?Y$qW_q#)juLG zk@`gUoJ;NGfu2JDYW=aR`^Fg`0IF(XAiqGtFCF)^41g_wZ1dRup=2?B%a(i4C9Zts3=#d9RNS6$rrygc|E z;~4lqjjF3ou|8V=ZoQZ4Fm2Ii3H?z_F-&>z#UkiO|jC^irkHbob_@6FBHR#$>2@l!I105ILYiEqpNVR`>2r$TNN7E#x9VV1 zlnhmIH~0Msc%ONGYwFwPPk`Fz_fY@-J@Jn7q}If-oy6sbfUCZ;#9cWt{jXWl(H`au zS(YiB^=9d>hPduXRvc@dMsscQc}K+2u2PA}VNLU5g->gtAd*eMf2m5Yv{O-AC+)14cdo8_aNp5Jp%r5r<~Fpj zbgFNdd${>-$PcREqkq%PFqN0zQ+@YQ)ves%v%NtH=DD8T?N@Dj5^R6`! za1_lnSZA*e^x%Ld7V=rO-^A_JfmV!#oN;iyz#D5O@n`C4M@huQBRL%7xqrRedojPliAp`}G# zf=AHu!exdz!-j8;jcV3DPswC9k0KMGUT@8Uvo);^|O1QzTGbB)KI%v z(dL^Sh25YVIrAGkpetuq=yIm)aeuIm<}UaNc*HTa?>j<1%iH!bL23@4RylF4ch_$-ScuS`UXtpw zh-9cE;bin^Z9)Z!1emT=m2faG;tu|c{aiOPhXMQW&q44n8KWV5wi0zSm4bf5K*+S9 zY}gDt#=3@CC{o}wt9-<)*6a4xsqxcmA zS{7#&r-X_=foYF;(xigm8hKpYo*+B#BRrQEz?ssU&Qp6FVcssi-G_E-imhW5OOwh` zF4-S8cU*In>|%RV2BUXYs;iyN*#r^4@qyIP_HigIpb3umZ(onp!K_Oyqs^*7X`Feq z`v&IG`UuLx2D>7uR34gM?X9a=o!6hx7Lkc|-w_|%ElRb! z*Q3-N@*bNXS6hX6<)rbTgi($>t~I)!pggx~C&VEXd|5RIEuPHR++xlS7$5W4g!pA~ zjA=4ud1b$%6{t=^`n>BaTIVvEwUt;o4J?F<^lsu;#E~v9U=cD?963I*XbHn-tP{-i zRH*((8Fh`~*FJ`*-C=>4nZkqQxVS*cw01U>k@W*3WE;xDXfL^ouI&ycQXS=> zhxMFGy}(bkKgOj4*jHGlT?j(axpn)*j|5p&VvQ22%tOK zsyNbB^sv0Bfe(7FZqA@?b+<{`k=L0I4Ms)K6Qbc`e$*;G1Eh zh%Z5_c>L9CkxaaSD%>D5@UfPgU0MUt!RF5w5f`PjQF&k8HWGlup}G zRT@Z|)gU>!jH?O#VNh{R+2QuhcR(Rel8Q}xx$-}Lk8LL0`WKIg%e?IN`x4qQIdx9| z@aV%_mbQq4wjU*54LD>lWd@33MLH-&RYW~|m_reQX-v?D#sWT?x?%&_K))IO9AFnQ z4MM}jiC>)JaGfCJvZ#>-E$c&b^wts8EugA0U9QlWM!sxN1#6JX^WC0IshOvoTxR{I zY42ZZqXJH$1fFd#ejzN>I_>^Isf{y5)z|>bbAt38lUNqqX|#UflDV8;u%OHpA4{Ku zAyH~|L43VxdR>z(5SF7FP5!#i96ZJ{qDi0J&;S}WxxG~Ssfej=$P)k%#69Z}%%Hee zRuydOw@_|yovK~wo^CEYoyAX2OM&0z4RE8>x`b&rY08;jn1!}P!RAfJBSxX7vm>X< zc-Uug0_$i%GK}G*LO&~P`rDia>d_{AsJYv+5*g15*HBJ1NpHW!pRF*A;Bhf>b1puv zsb)>1vyjRGW<_oaH;%09v73QPnTVLjqBx7XZ2FJ zcef1%?G?Ok^%Uj{BC%wLrs@03DaF%T5;}GB4QayUO3B2R4Ua&a{=V+pt;_=>`pUPO z_4p2>F%_?j#|8NESk16yF^#Ial7dFzZhQW=kn!{)#gv(`vUu}ax^>0GgT@RwBey#? zcZ*SQ;mm`2J^LmrrEcG{?9xP5+Y&#IN&DgjdXOt{A*~Xohe%HTgTBG1zYz4MoN)vE zv4NivlFz$TPrg>PMO&Ua7j2!^_aw`IqSS(>H}l3!Hm5HZW@j_{!TG#8>X3MTVHQsr zxi;b2tppXN!Q|_%9Y7yXL88IB8(A*5#_jBYNZn-a8qU%qq@~f7rc5f%g#*Pm%S@wk zQqvi9)A|OT5skE#zTf%pYus59dflajVuPzEA-NA7RDI?x*$>2DkMV`btYv6cf|uKy z_7xW!_q&jsZj0$PwKFGNdS+tdW8=N|*OTaP0RT_j&FBU@U=-<{+wPAl^vq2;c9p0I7>`#w?_{lIPtVj>w1@<6E+jh>T)*`NDqv9ZtJi(CB<+|Lspe?W>EE z|MChXUNC-S3jE%6GokufV7Ab~UfctAn==a}#dWYb?iS!8@5BGX8WAxjZD1aC+eQBBZ5zS(=h#m@>PE&HjQw{zYE`-CKcBm*8lnf^j43IC!x{W>6 zQw+fTng|Z7y+j_uPrz~Shvr%jJ-kexvATD5>NAmvQ?ijz6b~a_pGksp?|4nOwLE$J zAjg^2q@>FyQFoC`p;rL3xfvJVnehR%$!5>1>93oE5MB~%r$kMdn=GZ?=0TJWHmIn+ zs@Qj|J>&f{*2*}3z&(msD~vnV(m4bTe&#(A`_ZNr$BuG$k4?LqC3F8g$tE9DSK-0H zUJ8MwGDXYX51PqiI`wBZgroF0ml`+|n`cv@l8GubN}Q4^}BOKFBtxDk78{?ufd zm%4Nl6(vk|YUy@`XY-#0g{Xmx{1DH4ZF&#LV*^3`R7mpxPdY{7Ww*Df_dP(R=!>mRsv|pQe6V{XS+PIg00-`isfADuSh-N%~PkDP`#-&@#hjDz5Rg)=+^zgk_gUj{LINP{o51@ywQ{6MALW1p$~ zOkX8%N}47(cI#)JM)PD9srEaYQ%NBW;TzJo8Su)60KayA@bZ zDF7xN4H@a>K^*x54-<-p2{{pMdhLt%_Q&pjG_d9CeA|cu0l&2}R zm_1*FxH=jksgkItYAigNEEb^pk_||RF(fU;F)xHd+*Pf;461uXGEII}i$c`^CkR`P z3ti1}bM}%%0m zUF$LJdj4%LO|7CJB`)us?$<9i^+&|7VCz#($&3M1nXo_pau!Dz+rX?`Z+^IK;D(9_ zcnE;e{yC#BGR7;EZCSlfJMECLbC;4hEN^^%(P*1yD0%)T0Q{iCiD(_IJ2r+e{RB{= z&z=`u;B24vG_ohCycS?U=q?W?%wW2~@t_1Z?rcjti)cDc+}L|_PFX36DV;qUV?@r%>qx1kbC!9%fp57m^JTU zDnh*{_KnPa{d~6iwj*rS2L+3)%>{o~+!b@2eW_WRGx*(DY5(vDhv_R{N)#!N8&HSU zS4W(4I(I0d#=~K;+)UbRLNdpzS_GkUr*yNC_OKpAf*jyiDyfyPnN$`WOJVf?P#DxT+N4A=qOM;yx-zJq7zh?D zT*z-~W9H6Md*n

AKpZRgDWo5NF`KuXh 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 Df|X)J literal 0 HcmV?d00001 diff --git a/lib/base/doc/assets/Architecture-SingleNode.drawio b/lib/base/doc/assets/Architecture-SingleNode.drawio index f6fb4275..e8b44230 100644 --- a/lib/base/doc/assets/Architecture-SingleNode.drawio +++ b/lib/base/doc/assets/Architecture-SingleNode.drawio @@ -1,4 +1,4 @@ - + @@ -7,7 +7,7 @@ - + @@ -55,7 +55,7 @@ - + diff --git a/lib/base/lib/single-node-stack.ts b/lib/base/lib/single-node-stack.ts index 0292d9f0..07854438 100644 --- a/lib/base/lib/single-node-stack.ts +++ b/lib/base/lib/single-node-stack.ts @@ -116,7 +116,7 @@ export class BaseSingleNodeStack extends cdk.Stack { }) new cw.CfnDashboard(this, 'base-cw-dashboard', { - dashboardName: STACK_NAME, + dashboardName: `${STACK_NAME}-${node.instanceId}`, dashboardBody: dashboardString, }); diff --git a/lib/base/sample-configs/.env-sample-rpc b/lib/base/sample-configs/.env-sample-rpc index 2edc9750..a4b86c35 100644 --- a/lib/base/sample-configs/.env-sample-rpc +++ b/lib/base/sample-configs/.env-sample-rpc @@ -19,4 +19,4 @@ BASE_DATA_VOL_SIZE="3000" # Current required data size to keep both BASE_DATA_VOL_IOPS="5000" # Max IOPS for EBS volumes (not applicable for "instance-store") BASE_DATA_VOL_THROUGHPUT="700" # Max throughput for EBS gp3 volumes (not applicable for "io1" | "io2" | "instance-store") BASE_RESTORE_FROM_SNAPSHOT="false" # Download snapshot to speed up statup time -BASE_L1_ENDPOINT="none" # Leave as "none" if you use Amazon Managed Blockchain (AMB) Access Ethereum node or set your own L1 URL \ No newline at end of file +BASE_L1_ENDPOINT="none" # Set your own URL to Ethereum L1 node: https://docs.base.org/tools/node-providers \ No newline at end of file From 4a5127c36e2d1a37d77a8a4c3454c4ff4e281489 Mon Sep 17 00:00:00 2001 From: Nikolay Vlasov Date: Fri, 2 Feb 2024 17:39:05 +1100 Subject: [PATCH 06/75] Base updated documentation --- lib/base/README.md | 2 +- .../doc/assets/Architecture-SingleNode-v3.jpg | Bin 48320 -> 0 bytes 2 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 lib/base/doc/assets/Architecture-SingleNode-v3.jpg diff --git a/lib/base/README.md b/lib/base/README.md index 48c4b453..9041e23b 100644 --- a/lib/base/README.md +++ b/lib/base/README.md @@ -6,7 +6,7 @@ ### Single node setup -![Single Node Deployment](./doc/assets/Architecture-SingleNode-v3.jpg) +![Single Node Deployment](./doc/assets/Architecture-SingleNode-v3.png) 1. A Base node deployed in the [Default VPC](https://docs.aws.amazon.com/vpc/latest/userguide/default-vpc.html) continuously synchronizes with the rest of nodes on Base blockchain network through [Internet Gateway](https://docs.aws.amazon.com/vpc/latest/userguide/VPC_Internet_Gateway.html). 2. The Base node is used by dApps or development tools internally from within the Default VPC. JSON RPC API is not exposed to the Internet directly to protect nodes from unauthorized access. diff --git a/lib/base/doc/assets/Architecture-SingleNode-v3.jpg b/lib/base/doc/assets/Architecture-SingleNode-v3.jpg deleted file mode 100644 index 99e05c260cd9207eb7e32f058e667a57c1b5daa5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 48320 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