Skip to content

Commit 77b5754

Browse files
committed
feat(polygon): add Polygon PoS blueprint with Erigon
Add Polygon PoS Full Node blueprint under lib/polygon/ using Erigon (0xpolygon/erigon:v3.4.0) as a single-container all-in-one client. Stacks: - polygon-common: IAM role with SSM and CloudWatch policies - polygon-single-node: single EC2 instance (reuses SingleNodeConstruct) - polygon-ha-nodes: ALB + ASG multi-AZ HA (reuses HANodesConstruct) Why Erigon over Heimdall+Bor: - Heimdall v1->v2 migration breaks P2P for v1 clients on both networks - Heimdall v2 Docker image has init bugs (reported: 0xPolygon/heimdall-v2#568) - Erigon connects to Polygon official Heimdall API endpoint instead - Built-in OtterSync for torrent-based snapshot sync Includes: - Config with mainnet and amoy sample .env files - Security group: P2P (30303), torrent (42069) public; RPC (8545) VPC-only - User-data: Docker install, EBS volume detection, Erigon startup, CloudWatch cron - README with deployment guide and Well-Architected checklist - Website docs page - Jest tests (3/3 passing), cdk-nag compliant - package.json for CI integration - Removed polygon from CI test exclusion list Deployment verified on Graviton m7g.xlarge in us-east-1. Closes #7 Refs #233
1 parent cd31b00 commit 77b5754

23 files changed

Lines changed: 1393 additions & 1 deletion

lib/polygon/.gitignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
*.js
2+
!jest.config.js
3+
*.d.ts
4+
node_modules
5+
6+
# CDK asset staging directory
7+
.cdk.staging
8+
cdk.out

lib/polygon/.npmignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
*.ts
2+
!*.d.ts
3+
4+
# CDK asset staging directory
5+
.cdk.staging
6+
cdk.out

lib/polygon/README.md

Lines changed: 276 additions & 0 deletions
Large diffs are not rendered by default.

lib/polygon/app.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
#!/usr/bin/env node
2+
import 'dotenv/config'
3+
import "source-map-support/register";
4+
import * as cdk from "aws-cdk-lib";
5+
import * as nag from "cdk-nag";
6+
import * as config from "./lib/config/node-config";
7+
8+
import { PolygonSingleNodeStack } from "./lib/single-node-stack";
9+
import { PolygonHaNodesStack } from "./lib/ha-nodes-stack";
10+
import { PolygonCommonStack } from "./lib/common-stack";
11+
12+
const app = new cdk.App();
13+
cdk.Tags.of(app).add("Project", "AWSPolygon");
14+
15+
new PolygonCommonStack(app, "polygon-common", {
16+
stackName: `polygon-nodes-common`,
17+
env: { account: config.baseConfig.accountId, region: config.baseConfig.region },
18+
});
19+
20+
new PolygonSingleNodeStack(app, "polygon-single-node", {
21+
stackName: `polygon-single-node-${config.baseConfig.network}`,
22+
env: { account: config.baseConfig.accountId, region: config.baseConfig.region },
23+
network: config.baseConfig.network,
24+
erigonImage: config.baseConfig.erigonImage,
25+
heimdallApiUrl: config.baseConfig.heimdallApiUrl,
26+
instanceType: config.singleNodeConfig.instanceType,
27+
instanceCpuType: config.singleNodeConfig.instanceCpuType,
28+
dataVolume: config.singleNodeConfig.dataVolumes[0],
29+
});
30+
31+
new PolygonHaNodesStack(app, "polygon-ha-nodes", {
32+
stackName: `polygon-ha-nodes-${config.baseConfig.network}`,
33+
env: { account: config.baseConfig.accountId, region: config.baseConfig.region },
34+
network: config.baseConfig.network,
35+
erigonImage: config.baseConfig.erigonImage,
36+
heimdallApiUrl: config.baseConfig.heimdallApiUrl,
37+
instanceType: config.haNodeConfig.instanceType,
38+
instanceCpuType: config.haNodeConfig.instanceCpuType,
39+
numberOfNodes: config.haNodeConfig.numberOfNodes,
40+
albHealthCheckGracePeriodMin: config.haNodeConfig.albHealthCheckGracePeriodMin,
41+
heartBeatDelayMin: config.haNodeConfig.heartBeatDelayMin,
42+
dataVolume: config.haNodeConfig.dataVolumes[0],
43+
});
44+
45+
cdk.Aspects.of(app).add(
46+
new nag.AwsSolutionsChecks({
47+
verbose: false,
48+
reports: true,
49+
logIgnores: false,
50+
})
51+
);

lib/polygon/cdk.json

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"app": "npx ts-node --prefer-ts-exts app.ts",
3+
"watch": {
4+
"include": ["**"],
5+
"exclude": ["README.md", "cdk*.json", "**/*.d.ts", "**/*.js", "tsconfig.json", "package*.json", "yarn.lock", "node_modules", "test"]
6+
},
7+
"context": {
8+
"@aws-cdk/aws-lambda:recognizeLayerVersion": true,
9+
"@aws-cdk/core:checkSecretUsage": true,
10+
"@aws-cdk/core:target-partitions": [
11+
"aws",
12+
"aws-cn"
13+
],
14+
"@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true,
15+
"@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true,
16+
"@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true,
17+
"@aws-cdk/aws-iam:minimizePolicies": true,
18+
"@aws-cdk/core:validateSnapshotRemovalPolicy": true,
19+
"@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true,
20+
"@aws-cdk/aws-s3:createDefaultLoggingPolicy": true,
21+
"@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true,
22+
"@aws-cdk/aws-apigateway:disableCloudWatchRole": true,
23+
"@aws-cdk/core:enablePartitionLiterals": true,
24+
"@aws-cdk/aws-events:eventsTargetQueueSameAccount": true,
25+
"@aws-cdk/aws-iam:standardizedServicePrincipals": true,
26+
"@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true,
27+
"@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true,
28+
"@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true,
29+
"@aws-cdk/aws-route53-patters:useCertificate": true,
30+
"@aws-cdk/customresources:installLatestAwsSdkDefault": false
31+
}
32+
}

lib/polygon/doc/assets/.gitkeep

Whitespace-only changes.

lib/polygon/jest.config.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
module.exports = {
2+
testEnvironment: 'node',
3+
roots: ['<rootDir>/test'],
4+
testMatch: ['**/*.test.ts'],
5+
transform: {
6+
'^.+\\.tsx?$': 'ts-jest'
7+
},
8+
};
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
#!/bin/bash
2+
set -euo pipefail
3+
4+
# Variables injected by CDK via Fn::Sub
5+
REGION="${_REGION_}"
6+
ASSETS_S3_PATH="${_ASSETS_S3_PATH_}"
7+
POLYGON_NETWORK="${_POLYGON_NETWORK_}"
8+
POLYGON_ERIGON_IMAGE="${_POLYGON_ERIGON_IMAGE_}"
9+
POLYGON_HEIMDALL_API_URL="${_POLYGON_HEIMDALL_API_URL_}"
10+
STACK_NAME="${_STACK_NAME_}"
11+
DATA_VOLUME_TYPE="${_DATA_VOLUME_TYPE_}"
12+
DATA_VOLUME_SIZE="${_DATA_VOLUME_SIZE_}"
13+
14+
# Map network name to Erigon chain name
15+
case "$POLYGON_NETWORK" in
16+
mainnet) POLYGON_CHAIN_NAME="bor-mainnet" ;;
17+
amoy) POLYGON_CHAIN_NAME="amoy" ;;
18+
*) POLYGON_CHAIN_NAME="bor-mainnet" ;;
19+
esac
20+
21+
echo "========== Polygon Node Setup Starting =========="
22+
echo "Network: $POLYGON_NETWORK (chain: $POLYGON_CHAIN_NAME)"
23+
echo "Erigon Image: $POLYGON_ERIGON_IMAGE"
24+
25+
# Install dependencies
26+
yum update -y
27+
yum install -y docker jq aws-cfn-bootstrap amazon-cloudwatch-agent cronie
28+
29+
# Start Docker
30+
systemctl enable docker
31+
systemctl start docker
32+
33+
# Install docker-compose
34+
ARCH=$(uname -m)
35+
curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-linux-$ARCH" -o /usr/local/bin/docker-compose
36+
chmod +x /usr/local/bin/docker-compose
37+
38+
# Format and mount data volume
39+
# Note: CloudFormation VolumeAttachment may take several minutes after instance launch.
40+
# We poll for up to 10 minutes for the device to appear.
41+
DATA_DIR="/data"
42+
mkdir -p "$DATA_DIR"
43+
if [ "$DATA_VOLUME_TYPE" != "instance-store" ]; then
44+
echo "Waiting for EBS data volume to be attached..."
45+
WAIT_SECONDS=0
46+
MAX_WAIT=600
47+
DEVICE=""
48+
while [ $WAIT_SECONDS -lt $MAX_WAIT ]; do
49+
# Look for unformatted nvme device larger than 100GB (skip root volume)
50+
DEVICE=$(lsblk -lnb | awk '{if ($7 == "" && $4 > 100000000000) {print "/dev/"$1}}' | grep nvme | sort | head -1)
51+
if [ -n "$DEVICE" ]; then
52+
echo "Found data volume: $DEVICE after $WAIT_SECONDS seconds"
53+
break
54+
fi
55+
# Also check traditional device names
56+
for dev in /dev/sdf /dev/xvdf; do
57+
if [ -e "$dev" ]; then DEVICE="$dev"; break 2; fi
58+
done
59+
sleep 10
60+
WAIT_SECONDS=$((WAIT_SECONDS + 10))
61+
echo "Waiting for data volume... ($WAIT_SECONDS seconds/$MAX_WAIT seconds)"
62+
done
63+
64+
if [ -n "$DEVICE" ]; then
65+
echo "Using device: $DEVICE"
66+
if ! blkid "$DEVICE" 2>/dev/null; then
67+
mkfs.xfs "$DEVICE"
68+
fi
69+
mount "$DEVICE" "$DATA_DIR"
70+
VOLUME_UUID=$(blkid -s UUID -o value "$DEVICE")
71+
echo "UUID=$VOLUME_UUID $DATA_DIR xfs defaults,nofail 0 2" >> /etc/fstab
72+
else
73+
echo "WARNING: No data volume found after $MAX_WAIT seconds. Using root volume for data."
74+
fi
75+
fi
76+
77+
# Create data directory
78+
mkdir -p "$DATA_DIR/erigon"
79+
chmod -R 777 "$DATA_DIR/erigon"
80+
81+
# Create docker-compose file
82+
cat > /home/ec2-user/docker-compose.yml << COMPOSEOF
83+
services:
84+
erigon:
85+
image: $POLYGON_ERIGON_IMAGE
86+
container_name: erigon
87+
restart: always
88+
command:
89+
- --chain=$POLYGON_CHAIN_NAME
90+
- --bor.heimdall=$POLYGON_HEIMDALL_API_URL
91+
- --datadir=/var/lib/erigon/data
92+
- --http
93+
- --http.api=eth,debug,net,trace,web3,erigon,txpool,bor
94+
- --http.addr=0.0.0.0
95+
- --http.vhosts=*
96+
- --torrent.download.rate=512mb
97+
- --metrics
98+
- --metrics.addr=0.0.0.0
99+
- --maxpeers=100
100+
ports:
101+
- "8545:8545"
102+
- "30303:30303/tcp"
103+
- "30303:30303/udp"
104+
- "42069:42069/tcp"
105+
- "42069:42069/udp"
106+
volumes:
107+
- $DATA_DIR/erigon:/var/lib/erigon/data
108+
COMPOSEOF
109+
110+
# Start services
111+
cd /home/ec2-user
112+
/usr/local/bin/docker-compose up -d
113+
114+
# Setup sync checker cron (publish metrics to CloudWatch every 5 min)
115+
cat > /home/ec2-user/sync-checker.sh << 'CHECKEREOF'
116+
#!/bin/bash
117+
INSTANCE_ID=$(curl -s http://169.254.169.254/latest/meta-data/instance-id)
118+
119+
# Check if Erigon is syncing
120+
SYNCING=$(curl -s -X POST -H "Content-Type: application/json" \
121+
--data '{"method":"eth_syncing","params":[],"id":1,"jsonrpc":"2.0"}' \
122+
http://localhost:8545 2>/dev/null | jq -r '.result')
123+
124+
BLOCK_HEX=$(curl -s -X POST -H "Content-Type: application/json" \
125+
--data '{"method":"eth_blockNumber","params":[],"id":1,"jsonrpc":"2.0"}' \
126+
http://localhost:8545 2>/dev/null | jq -r '.result // "0x0"')
127+
BLOCK=$((BLOCK_HEX))
128+
129+
IS_SYNCING=1
130+
if [ "$SYNCING" = "false" ]; then IS_SYNCING=0; fi
131+
132+
aws cloudwatch put-metric-data --namespace "Polygon/Node" --dimensions InstanceId="$INSTANCE_ID" \
133+
--metric-data "[{\"MetricName\":\"ErigonBlockHeight\",\"Value\":$BLOCK,\"Unit\":\"Count\"},{\"MetricName\":\"ErigonSyncing\",\"Value\":$IS_SYNCING,\"Unit\":\"Count\"}]" 2>/dev/null || true
134+
CHECKEREOF
135+
136+
chmod +x /home/ec2-user/sync-checker.sh
137+
echo "*/5 * * * * /home/ec2-user/sync-checker.sh" | crontab -
138+
139+
# Note: cfn-signal is not used because CreationPolicy is disabled
140+
# (avoids circular dependency with VolumeAttachment).
141+
# Node health is monitored via CloudWatch metrics instead.
142+
143+
echo "========== Polygon Node Setup Complete =========="

lib/polygon/lib/common-stack.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import * as cdk from "aws-cdk-lib";
2+
import * as cdkConstructs from "constructs";
3+
import * as iam from "aws-cdk-lib/aws-iam";
4+
import * as s3Assets from "aws-cdk-lib/aws-s3-assets";
5+
import * as path from "path";
6+
import * as nag from "cdk-nag";
7+
8+
export class PolygonCommonStack extends cdk.Stack {
9+
constructor(scope: cdkConstructs.Construct, id: string, props?: cdk.StackProps) {
10+
super(scope, id, props);
11+
12+
const region = cdk.Stack.of(this).region;
13+
const asset = new s3Assets.Asset(this, "assets", {
14+
path: path.join(__dirname, "assets"),
15+
});
16+
17+
const instanceRole = new iam.Role(this, `node-role`, {
18+
assumedBy: new iam.ServicePrincipal("ec2.amazonaws.com"),
19+
managedPolicies: [
20+
iam.ManagedPolicy.fromAwsManagedPolicyName("AmazonSSMManagedInstanceCore"),
21+
iam.ManagedPolicy.fromAwsManagedPolicyName("CloudWatchAgentServerPolicy"),
22+
],
23+
});
24+
25+
instanceRole.addToPolicy(
26+
new iam.PolicyStatement({
27+
resources: ["*"],
28+
actions: ["cloudformation:SignalResource"],
29+
})
30+
);
31+
32+
new cdk.CfnOutput(this, "Instance Role ARN", {
33+
value: instanceRole.roleArn,
34+
exportName: "PolygonNodeInstanceRoleArn",
35+
});
36+
37+
nag.NagSuppressions.addResourceSuppressions(
38+
this,
39+
[
40+
{
41+
id: "AwsSolutions-IAM4",
42+
reason: "AmazonSSMManagedInstanceCore and CloudWatchAgentServerPolicy are restrictive enough",
43+
},
44+
{
45+
id: "AwsSolutions-IAM5",
46+
reason: "Can't target specific stack: https://github.com/aws/aws-cdk/issues/22657",
47+
},
48+
],
49+
true
50+
);
51+
}
52+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import * as configTypes from "../../../constructs/config.interface";
2+
3+
export type PolygonNetwork = "mainnet" | "amoy";
4+
5+
export interface PolygonDataVolumeConfig extends configTypes.DataVolumeConfig {}
6+
7+
export interface PolygonBaseConfig extends configTypes.BaseConfig {
8+
network: PolygonNetwork;
9+
erigonImage: string;
10+
heimdallApiUrl: string;
11+
}
12+
13+
export interface PolygonSingleNodeConfig extends configTypes.SingleNodeConfig {}
14+
15+
export interface PolygonHaNodeConfig extends configTypes.HaNodesConfig {}

0 commit comments

Comments
 (0)