Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions lib/polygon/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
*.js
!jest.config.js
*.d.ts
node_modules

# CDK asset staging directory
.cdk.staging
cdk.out
6 changes: 6 additions & 0 deletions lib/polygon/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
*.ts
!*.d.ts

# CDK asset staging directory
.cdk.staging
cdk.out
276 changes: 276 additions & 0 deletions lib/polygon/README.md

Large diffs are not rendered by default.

51 changes: 51 additions & 0 deletions lib/polygon/app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#!/usr/bin/env node
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/node-config";

import { PolygonSingleNodeStack } from "./lib/single-node-stack";
import { PolygonHaNodesStack } from "./lib/ha-nodes-stack";
import { PolygonCommonStack } from "./lib/common-stack";

const app = new cdk.App();
cdk.Tags.of(app).add("Project", "AWSPolygon");

new PolygonCommonStack(app, "polygon-common", {
stackName: `polygon-nodes-common`,
env: { account: config.baseConfig.accountId, region: config.baseConfig.region },
});

new PolygonSingleNodeStack(app, "polygon-single-node", {
stackName: `polygon-single-node-${config.baseConfig.network}`,
env: { account: config.baseConfig.accountId, region: config.baseConfig.region },
network: config.baseConfig.network,
erigonImage: config.baseConfig.erigonImage,
heimdallApiUrl: config.baseConfig.heimdallApiUrl,
instanceType: config.singleNodeConfig.instanceType,
instanceCpuType: config.singleNodeConfig.instanceCpuType,
dataVolume: config.singleNodeConfig.dataVolumes[0],
});

new PolygonHaNodesStack(app, "polygon-ha-nodes", {
stackName: `polygon-ha-nodes-${config.baseConfig.network}`,
env: { account: config.baseConfig.accountId, region: config.baseConfig.region },
network: config.baseConfig.network,
erigonImage: config.baseConfig.erigonImage,
heimdallApiUrl: config.baseConfig.heimdallApiUrl,
instanceType: config.haNodeConfig.instanceType,
instanceCpuType: config.haNodeConfig.instanceCpuType,
numberOfNodes: config.haNodeConfig.numberOfNodes,
albHealthCheckGracePeriodMin: config.haNodeConfig.albHealthCheckGracePeriodMin,
heartBeatDelayMin: config.haNodeConfig.heartBeatDelayMin,
dataVolume: config.haNodeConfig.dataVolumes[0],
});

cdk.Aspects.of(app).add(
new nag.AwsSolutionsChecks({
verbose: false,
reports: true,
logIgnores: false,
})
);
32 changes: 32 additions & 0 deletions lib/polygon/cdk.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"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
}
}
Empty file added lib/polygon/doc/assets/.gitkeep
Empty file.
8 changes: 8 additions & 0 deletions lib/polygon/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module.exports = {
testEnvironment: 'node',
roots: ['<rootDir>/test'],
testMatch: ['**/*.test.ts'],
transform: {
'^.+\\.tsx?$': 'ts-jest'
},
};
147 changes: 147 additions & 0 deletions lib/polygon/lib/assets/user-data/node.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
#!/bin/bash
set -euo pipefail

# Variables injected by CDK via Fn::Sub
REGION="${_REGION_}"
ASSETS_S3_PATH="${_ASSETS_S3_PATH_}"
POLYGON_NETWORK="${_POLYGON_NETWORK_}"
POLYGON_ERIGON_IMAGE="${_POLYGON_ERIGON_IMAGE_}"
POLYGON_HEIMDALL_API_URL="${_POLYGON_HEIMDALL_API_URL_}"
STACK_NAME="${_STACK_NAME_}"
DATA_VOLUME_TYPE="${_DATA_VOLUME_TYPE_}"
DATA_VOLUME_SIZE="${_DATA_VOLUME_SIZE_}"

# Map network name to Erigon chain name
case "$POLYGON_NETWORK" in
mainnet) POLYGON_CHAIN_NAME="bor-mainnet" ;;
amoy) POLYGON_CHAIN_NAME="amoy" ;;
*) POLYGON_CHAIN_NAME="bor-mainnet" ;;
esac

echo "========== Polygon Node Setup Starting =========="
echo "Network: $POLYGON_NETWORK (chain: $POLYGON_CHAIN_NAME)"
echo "Erigon Image: $POLYGON_ERIGON_IMAGE"

# Install dependencies
yum update -y
yum install -y jq aws-cfn-bootstrap amazon-cloudwatch-agent cronie

# Install Docker from official repo (includes docker-compose-plugin)
yum install -y yum-utils
yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
yum install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

# Start Docker
systemctl enable docker
systemctl start docker

# Format and mount data volume
# Note: CloudFormation VolumeAttachment may take several minutes after instance launch.
# We poll for up to 10 minutes for the device to appear.
DATA_DIR="/data"
mkdir -p "$DATA_DIR"
if [ "$DATA_VOLUME_TYPE" != "instance-store" ]; then
echo "Waiting for EBS data volume to be attached..."
WAIT_SECONDS=0
MAX_WAIT=600
DEVICE=""
while [ $WAIT_SECONDS -lt $MAX_WAIT ]; do
# Look for unformatted nvme device larger than 100GB (skip root volume)
DEVICE=$(lsblk -lnb | awk '{if ($7 == "" && $4 > 100000000000) {print "/dev/"$1}}' | grep nvme | sort | head -1)
if [ -n "$DEVICE" ]; then
echo "Found data volume: $DEVICE after $WAIT_SECONDS seconds"
break
fi
# Also check traditional device names
for dev in /dev/sdf /dev/xvdf; do
if [ -e "$dev" ]; then DEVICE="$dev"; break 2; fi
done
sleep 10
WAIT_SECONDS=$((WAIT_SECONDS + 10))
echo "Waiting for data volume... ($WAIT_SECONDS seconds/$MAX_WAIT seconds)"
done

if [ -n "$DEVICE" ]; then
echo "Using device: $DEVICE"
if ! blkid "$DEVICE" 2>/dev/null; then
mkfs.xfs "$DEVICE"
fi
mount "$DEVICE" "$DATA_DIR"
VOLUME_UUID=$(blkid -s UUID -o value "$DEVICE")
echo "UUID=$VOLUME_UUID $DATA_DIR xfs defaults,nofail 0 2" >> /etc/fstab
else
echo "WARNING: No data volume found after $MAX_WAIT seconds. Using root volume for data."
fi
fi

# Create data directory with restricted permissions
# Erigon runs as UID 1000 inside the container
mkdir -p "$DATA_DIR/erigon"
chown -R 1000:1000 "$DATA_DIR/erigon"
chmod -R 750 "$DATA_DIR/erigon"

# Create docker-compose file
cat > /home/ec2-user/docker-compose.yml << COMPOSEOF
services:
erigon:
image: $POLYGON_ERIGON_IMAGE
container_name: erigon
restart: always
command:
- --chain=$POLYGON_CHAIN_NAME
- --bor.heimdall=$POLYGON_HEIMDALL_API_URL
- --datadir=/var/lib/erigon/data
- --http
- --http.api=eth,debug,net,trace,web3,erigon,txpool,bor
- --http.addr=0.0.0.0
- --http.vhosts=localhost,127.0.0.1
- --torrent.download.rate=512mb
- --metrics
- --metrics.addr=0.0.0.0
- --maxpeers=100
ports:
- "8545:8545"
- "30303:30303/tcp"
- "30303:30303/udp"
- "42069:42069/tcp"
- "42069:42069/udp"
volumes:
- $DATA_DIR/erigon:/var/lib/erigon/data
COMPOSEOF

# Start services
cd /home/ec2-user
docker compose up -d

# Setup sync checker cron (publish metrics to CloudWatch every 5 min)
# Uses IMDSv2 token-based authentication
cat > /home/ec2-user/sync-checker.sh << 'CHECKEREOF'
#!/bin/bash
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 -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/instance-id)

# Check if Erigon is syncing
SYNCING=$(curl -s -X POST -H "Content-Type: application/json" \
--data '{"method":"eth_syncing","params":[],"id":1,"jsonrpc":"2.0"}' \
http://localhost:8545 2>/dev/null | jq -r '.result')

BLOCK_HEX=$(curl -s -X POST -H "Content-Type: application/json" \
--data '{"method":"eth_blockNumber","params":[],"id":1,"jsonrpc":"2.0"}' \
http://localhost:8545 2>/dev/null | jq -r '.result // "0x0"')
BLOCK=$((BLOCK_HEX))

IS_SYNCING=1
if [ "$SYNCING" = "false" ]; then IS_SYNCING=0; fi

aws cloudwatch put-metric-data --namespace "Polygon/Node" --dimensions InstanceId="$INSTANCE_ID" \
--metric-data "[{\"MetricName\":\"ErigonBlockHeight\",\"Value\":$BLOCK,\"Unit\":\"Count\"},{\"MetricName\":\"ErigonSyncing\",\"Value\":$IS_SYNCING,\"Unit\":\"Count\"}]" 2>/dev/null || true
CHECKEREOF

chmod +x /home/ec2-user/sync-checker.sh
echo "*/5 * * * * /home/ec2-user/sync-checker.sh" | crontab -

# Note: cfn-signal is not used because CreationPolicy is disabled
# (avoids circular dependency with VolumeAttachment).
# Node health is monitored via CloudWatch metrics instead.

echo "========== Polygon Node Setup Complete =========="
52 changes: 52 additions & 0 deletions lib/polygon/lib/common-stack.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import * as cdk from "aws-cdk-lib";
import * as cdkConstructs from "constructs";
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 nag from "cdk-nag";

export class PolygonCommonStack extends cdk.Stack {
constructor(scope: cdkConstructs.Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);

const region = cdk.Stack.of(this).region;
const asset = new s3Assets.Asset(this, "assets", {
path: path.join(__dirname, "assets"),
});

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({
resources: ["*"],
actions: ["cloudformation:SignalResource"],
})
);

new cdk.CfnOutput(this, "Instance Role ARN", {
value: instanceRole.roleArn,
exportName: "PolygonNodeInstanceRoleArn",
});

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
);
}
}
15 changes: 15 additions & 0 deletions lib/polygon/lib/config/node-config.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import * as configTypes from "../../../constructs/config.interface";

export type PolygonNetwork = "mainnet" | "amoy";

export interface PolygonDataVolumeConfig extends configTypes.DataVolumeConfig {}

export interface PolygonBaseConfig extends configTypes.BaseConfig {
network: PolygonNetwork;
erigonImage: string;
heimdallApiUrl: string;
}

export interface PolygonSingleNodeConfig extends configTypes.SingleNodeConfig {}

export interface PolygonHaNodeConfig extends configTypes.HaNodesConfig {}
61 changes: 61 additions & 0 deletions lib/polygon/lib/config/node-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import * as ec2 from "aws-cdk-lib/aws-ec2";
import * as configTypes from "./node-config.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;
}
}

const network = <configTypes.PolygonNetwork>process.env.POLYGON_NETWORK || "mainnet";

const defaultHeimdallApiUrl = network === "amoy"
? "https://heimdall-api-amoy.polygon.technology"
: "https://heimdall-api.polygon.technology";

export const baseConfig: configTypes.PolygonBaseConfig = {
accountId: process.env.AWS_ACCOUNT_ID || "xxxxxxxxxxx",
region: process.env.AWS_REGION || "us-east-1",
network,
erigonImage: process.env.POLYGON_ERIGON_IMAGE || "0xpolygon/erigon:v3.4.0",
heimdallApiUrl: process.env.POLYGON_HEIMDALL_API_URL || defaultHeimdallApiUrl,
};

export const singleNodeConfig: configTypes.PolygonSingleNodeConfig = {
instanceType: new ec2.InstanceType(process.env.POLYGON_INSTANCE_TYPE || "m7g.4xlarge"),
instanceCpuType: process.env.POLYGON_CPU_TYPE?.toLowerCase() == "x86_64" ? ec2.AmazonLinuxCpuType.X86_64 : ec2.AmazonLinuxCpuType.ARM_64,
dataVolumes: [
{
sizeGiB: process.env.POLYGON_DATA_VOL_SIZE ? parseInt(process.env.POLYGON_DATA_VOL_SIZE) : 8000,
type: parseDataVolumeType(process.env.POLYGON_DATA_VOL_TYPE?.toLowerCase() || "gp3"),
iops: process.env.POLYGON_DATA_VOL_IOPS ? parseInt(process.env.POLYGON_DATA_VOL_IOPS) : 16000,
throughput: process.env.POLYGON_DATA_VOL_THROUGHPUT ? parseInt(process.env.POLYGON_DATA_VOL_THROUGHPUT) : 1000,
}
],
};

export const haNodeConfig: configTypes.PolygonHaNodeConfig = {
instanceType: new ec2.InstanceType(process.env.POLYGON_RPC_INSTANCE_TYPE || "m7g.4xlarge"),
instanceCpuType: process.env.POLYGON_RPC_CPU_TYPE?.toLowerCase() == "x86_64" ? ec2.AmazonLinuxCpuType.X86_64 : ec2.AmazonLinuxCpuType.ARM_64,
numberOfNodes: process.env.POLYGON_RPC_NUMBER_OF_NODES ? parseInt(process.env.POLYGON_RPC_NUMBER_OF_NODES) : 2,
albHealthCheckGracePeriodMin: process.env.POLYGON_RPC_ALB_HEALTHCHECK_GRACE_PERIOD_MIN ? parseInt(process.env.POLYGON_RPC_ALB_HEALTHCHECK_GRACE_PERIOD_MIN) : 10,
heartBeatDelayMin: process.env.POLYGON_RPC_HA_NODES_HEARTBEAT_DELAY_MIN ? parseInt(process.env.POLYGON_RPC_HA_NODES_HEARTBEAT_DELAY_MIN) : 60,
dataVolumes: [
{
sizeGiB: process.env.POLYGON_RPC_DATA_VOL_SIZE ? parseInt(process.env.POLYGON_RPC_DATA_VOL_SIZE) : 8000,
type: parseDataVolumeType(process.env.POLYGON_RPC_DATA_VOL_TYPE?.toLowerCase() || "gp3"),
iops: process.env.POLYGON_RPC_DATA_VOL_IOPS ? parseInt(process.env.POLYGON_RPC_DATA_VOL_IOPS) : 16000,
throughput: process.env.POLYGON_RPC_DATA_VOL_THROUGHPUT ? parseInt(process.env.POLYGON_RPC_DATA_VOL_THROUGHPUT) : 1000,
}
],
};
Loading
Loading