From 37f3ca64d3e05ef8ec8b34d353c0ed4b1d1125d3 Mon Sep 17 00:00:00 2001 From: infraclaw-dash <283232465+infraclaw-dash@users.noreply.github.com> Date: Fri, 29 May 2026 18:19:40 +0000 Subject: [PATCH] feat: add standalone quorum list server deploy --- README.md | 3 + ansible/deploy.yml | 7 ++ .../quorum_list_server/defaults/main.yml | 19 +++++ .../roles/quorum_list_server/tasks/main.yml | 47 +++++++++++ .../templates/config.toml.j2 | 24 ++++++ docs/quorum-list-server.md | 79 ++++++++++++++++++ terraform/aws/.terraform.lock.hcl | 2 + terraform/aws/ansible_inventory_output.tf | 14 ++++ terraform/aws/instances.tf | 37 +++++++++ terraform/aws/main.tf | 82 ++++++++++++++++++- terraform/aws/security_groups.tf | 19 +++++ .../templates/inventory/ansible_inventory.tpl | 3 + terraform/aws/variables.tf | 20 +++++ 13 files changed, 355 insertions(+), 1 deletion(-) create mode 100644 ansible/roles/quorum_list_server/defaults/main.yml create mode 100644 ansible/roles/quorum_list_server/tasks/main.yml create mode 100644 ansible/roles/quorum_list_server/templates/config.toml.j2 create mode 100644 docs/quorum-list-server.md diff --git a/README.md b/README.md index 4213297c6..0175c1209 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,9 @@ Ansible configuration is stored in the `*.yml` file. The file contains the majority of playbook options. The rest are defined in [ansible roles](https://github.com/dashpay/dash-network-deploy/tree/master/ansible/roles). +Standalone quorum API hosts can be enabled with `quorum_list_server_count`. +See [Standalone Quorum List Server](docs/quorum-list-server.md) for details. + Configure your credentials in the `.env` file. ### Using git diff --git a/ansible/deploy.yml b/ansible/deploy.yml index 2fdc6b689..d73dccceb 100644 --- a/ansible/deploy.yml +++ b/ansible/deploy.yml @@ -256,6 +256,13 @@ core_container_name: core abci_logs_path: "{{ dashmate_logs_dir }}" +- name: Set up quorum list servers + hosts: quorum_list_servers + become: true + roles: + - role: quorum_list_server + when: quorum_list_server_enabled | bool + - name: Set up protx diff script hosts: masternodes become: true diff --git a/ansible/roles/quorum_list_server/defaults/main.yml b/ansible/roles/quorum_list_server/defaults/main.yml new file mode 100644 index 000000000..760b843af --- /dev/null +++ b/ansible/roles/quorum_list_server/defaults/main.yml @@ -0,0 +1,19 @@ +--- + +quorum_list_server_enabled: true +quorum_list_server_image: dashpay/quorum-list-server:latest +quorum_list_server_container_name: quorum-list-server +quorum_list_server_config_dir: /etc/quorum-list-server +quorum_list_server_config_path: "{{ quorum_list_server_config_dir }}/config.toml" +quorum_list_server_host: "0.0.0.0" +quorum_list_server_port: 8080 +quorum_list_server_previous_blocks_offset: 8 + +quorum_list_server_rpc_host: "{{ groups['seed_nodes'][0] }}" +quorum_list_server_rpc_url: "http://{{ hostvars[quorum_list_server_rpc_host].private_ip }}:{{ dashd_rpc_port }}" +quorum_list_server_rpc_user: "{{ dashd_rpc_user }}" +quorum_list_server_rpc_password: "{{ dashd_rpc_password }}" +quorum_list_server_network: "{{ 'devnet-porter' if dash_network == 'devnet' else dash_network }}" + +quorum_list_server_version_check_host: "" +quorum_list_server_address_host_override: "" diff --git a/ansible/roles/quorum_list_server/tasks/main.yml b/ansible/roles/quorum_list_server/tasks/main.yml new file mode 100644 index 000000000..af2e1b466 --- /dev/null +++ b/ansible/roles/quorum_list_server/tasks/main.yml @@ -0,0 +1,47 @@ +--- + +- name: Check quorum-list-server RPC target + ansible.builtin.assert: + that: + - quorum_list_server_rpc_host is defined + - quorum_list_server_rpc_host | length > 0 + - hostvars[quorum_list_server_rpc_host].private_ip is defined + fail_msg: "quorum_list_server_rpc_host must point at an inventory host with a private_ip" + +- name: Create quorum-list-server config dir + ansible.builtin.file: + path: "{{ quorum_list_server_config_dir }}" + state: directory + owner: root + group: root + mode: "0755" + +- name: Write quorum-list-server config + ansible.builtin.template: + src: config.toml.j2 + dest: "{{ quorum_list_server_config_path }}" + owner: root + group: root + mode: "0644" + register: quorum_list_server_config + +- name: Start quorum-list-server + community.docker.docker_container: + name: "{{ quorum_list_server_container_name }}" + image: "{{ quorum_list_server_image }}" + state: started + restart_policy: always + pull: true + recreate: "{{ quorum_list_server_config.changed }}" + network_mode: host + volumes: + - "{{ quorum_list_server_config_path }}:/app/config.toml:ro" + +- name: Wait for quorum-list-server health endpoint + ansible.builtin.uri: + url: "http://127.0.0.1:{{ quorum_list_server_port }}/health" + status_code: 200 + register: quorum_list_server_health + until: quorum_list_server_health.status == 200 + retries: 30 + delay: 2 diff --git a/ansible/roles/quorum_list_server/templates/config.toml.j2 b/ansible/roles/quorum_list_server/templates/config.toml.j2 new file mode 100644 index 000000000..61fe7eea4 --- /dev/null +++ b/ansible/roles/quorum_list_server/templates/config.toml.j2 @@ -0,0 +1,24 @@ +[server] +port = {{ quorum_list_server_port | int }} +host = "{{ quorum_list_server_host }}" + +[rpc] +url = "{{ quorum_list_server_rpc_url }}" +username = "{{ quorum_list_server_rpc_user }}" +password = "{{ quorum_list_server_rpc_password }}" + +[quorum] +previous_blocks_offset = {{ quorum_list_server_previous_blocks_offset | int }} + +[network] +network = "{{ quorum_list_server_network }}" + +{% if quorum_list_server_version_check_host | length > 0 or quorum_list_server_address_host_override | length > 0 %} +[docker] +{% if quorum_list_server_version_check_host | length > 0 %} +version_check_host = "{{ quorum_list_server_version_check_host }}" +{% endif %} +{% if quorum_list_server_address_host_override | length > 0 %} +address_host_override = "{{ quorum_list_server_address_host_override }}" +{% endif %} +{% endif %} diff --git a/docs/quorum-list-server.md b/docs/quorum-list-server.md new file mode 100644 index 000000000..f5c391218 --- /dev/null +++ b/docs/quorum-list-server.md @@ -0,0 +1,79 @@ +# Standalone Quorum List Server + +`dash-network-deploy` can provision a standalone `dashpay/quorum-list-server` +node for networks that need a public quorum API endpoint. + +The Paloma setup this is based on used a separate EC2 instance running the +`dashpay/quorum-list-server` Docker image, a private RPC connection to the +network seed node, and the public endpoint: + +```text +https://quorums.paloma.networks.dash.org +``` + +## Enable + +Set a non-zero `quorum_list_server_count` in the network Terraform vars: + +```hcl +quorum_list_server_count = 1 +``` + +Optional Terraform settings: + +```hcl +quorum_list_server_port = 8080 +quorum_list_server_instance_size = "micro" +quorum_list_server_root_disk_size = 20 +``` + +Terraform creates: + +- `quorum-list-server-N` EC2 host(s) +- An Ansible inventory group named `quorum_list_servers` +- A classic ELB with HTTP and HTTPS listeners +- An ACM certificate for `quorums..networks.dash.org` +- A Route53 CNAME for `quorums..networks.dash.org` + +For devnets, `` is the devnet suffix. For example, +`devnet-paloma` gets `quorums.paloma.networks.dash.org`. + +## Provisioning + +The deploy playbook applies the `quorum_list_server` Ansible role to hosts in +the `quorum_list_servers` inventory group. The role writes +`/etc/quorum-list-server/config.toml` and starts the Docker container with host +networking. + +By default, the server reads quorum data from the first seed node over the +private VPC RPC endpoint: + +```text +http://:20002 +``` + +The role uses the network's existing Dash Core RPC credentials from Ansible +vars. Do not commit those credentials into docs or examples. + +Optional Ansible vars: + +```yaml +quorum_list_server_image: dashpay/quorum-list-server:latest +quorum_list_server_port: 8080 +quorum_list_server_previous_blocks_offset: 8 +quorum_list_server_rpc_host: seed-1 +quorum_list_server_version_check_host: "" +quorum_list_server_address_host_override: "" +``` + +For devnets, the role passes `devnet-porter` to `quorum-list-server`, which +selects the devnet Platform LLMQ settings used by Paloma-style devnets. + +## Verify + +After deploy: + +```bash +curl -fsS https://quorums..networks.dash.org/health +curl -fsS https://quorums..networks.dash.org/quorums +``` diff --git a/terraform/aws/.terraform.lock.hcl b/terraform/aws/.terraform.lock.hcl index f43b743e0..43137e05d 100644 --- a/terraform/aws/.terraform.lock.hcl +++ b/terraform/aws/.terraform.lock.hcl @@ -6,6 +6,7 @@ provider "registry.terraform.io/hashicorp/aws" { constraints = "~> 5.21" hashes = [ "h1:N8sP6VZjHbtgmaCU6BKPox51UIypWXQRal7JMecEXQw=", + "h1:P9kgQZyi06yrLsAGnlOGAe01sOKsRsLQZOL295U38KM=", "zh:1ba1411e4f8c047950db94c236f146d4590790320c68320b4e56082d8746a507", "zh:3185e4a34cfcad35dcf11439290a4bd0ad52d462eca2ab5d4940488a2db72833", "zh:3c6b901f874b4d9a85301a653d0bd507b052992bd84fc81100f4e5f73b1adab7", @@ -28,6 +29,7 @@ provider "registry.terraform.io/hashicorp/random" { version = "3.5.1" constraints = "~> 3.5.1" hashes = [ + "h1:6FVyQ/aG6tawPam6B+oFjgdidKd83uG9n7dOSQ66HBA=", "h1:VSnd9ZIPyfKHOObuQCaKfnjIHRtR7qTw19Rz8tJxm+k=", "zh:04e3fbd610cb52c1017d282531364b9c53ef72b6bc533acb2a90671957324a64", "zh:119197103301ebaf7efb91df8f0b6e0dd31e6ff943d231af35ee1831c599188d", diff --git a/terraform/aws/ansible_inventory_output.tf b/terraform/aws/ansible_inventory_output.tf index 9e574596d..82be94da9 100644 --- a/terraform/aws/ansible_inventory_output.tf +++ b/terraform/aws/ansible_inventory_output.tf @@ -35,6 +35,18 @@ locals { ) ] + quorum_list_server_hosts = [ + for n in range(length(aws_instance.quorum_list_server)) : templatefile( + "${path.module}/templates/inventory/hostname.tpl", + { + index = n + 1 + name = element(aws_instance.quorum_list_server.*.tags.Hostname, n) + public_ip = element(aws_instance.quorum_list_server.*.public_ip, n) + private_ip = element(aws_instance.quorum_list_server.*.private_ip, n) + } + ) + ] + mixer_hosts = [ for n in range(length(aws_instance.mixer)) : templatefile( "${path.module}/templates/inventory/hostname.tpl", @@ -144,6 +156,7 @@ locals { local.web_hosts.*, local.logs_hosts.*, local.wallet_node_hosts.*, + local.quorum_list_server_hosts.*, local.mixer_hosts.*, local.seed_node_hosts.*, local.miner_hosts.*, @@ -155,6 +168,7 @@ locals { web_hosts = join("\n", concat(aws_instance.web.*.tags.Hostname)) logs_hosts = join("\n", concat(aws_instance.logs.*.tags.Hostname)) wallet_node_hosts = join("\n", concat(aws_instance.dashd_wallet.*.tags.Hostname)) + quorum_list_servers = join("\n", concat(aws_instance.quorum_list_server.*.tags.Hostname)) mixer_hosts = join("\n", concat(aws_instance.mixer.*.tags.Hostname)) miner_hosts = join("\n", concat(aws_instance.miner.*.tags.Hostname)) masternode_hosts = join("\n", concat(aws_instance.masternode_amd.*.tags.Hostname), concat(aws_instance.masternode_arm.*.tags.Hostname)) diff --git a/terraform/aws/instances.tf b/terraform/aws/instances.tf index cedfa50de..791e7c40d 100644 --- a/terraform/aws/instances.tf +++ b/terraform/aws/instances.tf @@ -159,6 +159,43 @@ resource "aws_instance" "miner" { } +resource "aws_instance" "quorum_list_server" { + count = var.quorum_list_server_count + + ami = var.main_host_arch == "arm64" ? data.aws_ami.ubuntu_arm.id : data.aws_ami.ubuntu_amd.id + instance_type = join(".", [var.main_host_arch == "arm64" ? "t4g" : "t3", var.quorum_list_server_instance_size]) + key_name = aws_key_pair.auth.id + iam_instance_profile = aws_iam_instance_profile.monitoring.name + + root_block_device { + volume_size = var.quorum_list_server_root_disk_size + volume_type = var.volume_type + } + + vpc_security_group_ids = [ + aws_security_group.default.id, + aws_security_group.quorum_list_server.id, + ] + + subnet_id = element(aws_subnet.public.*.id, count.index) + + volume_tags = { + Name = "dn-${terraform.workspace}-quorum-list-server-${count.index + 1}" + Hostname = "quorum-list-server-${count.index + 1}" + DashNetwork = terraform.workspace + } + + tags = { + Name = "dn-${terraform.workspace}-quorum-list-server-${count.index + 1}" + Hostname = "quorum-list-server-${count.index + 1}" + DashNetwork = terraform.workspace + } + + lifecycle { + ignore_changes = [ami] + } +} + # masternodes (amd) resource "aws_instance" "masternode_amd" { count = var.masternode_amd_count diff --git a/terraform/aws/main.tf b/terraform/aws/main.tf index 86b176a1b..96c4a9ee9 100644 --- a/terraform/aws/main.tf +++ b/terraform/aws/main.tf @@ -288,6 +288,87 @@ resource "aws_route53_record" "insight" { count = length(var.main_domain) > 1 ? 1 : 0 } +resource "aws_elb" "quorum_list_server" { + name = "${var.public_network_name}-quorums" + + subnets = aws_subnet.public.*.id + + count = var.quorum_list_server_count > 0 && length(var.main_domain) > 1 ? 1 : 0 + + security_groups = [ + aws_security_group.elb.id, + ] + + instances = aws_instance.quorum_list_server.*.id + + listener { + instance_port = var.quorum_list_server_port + instance_protocol = "http" + lb_port = 80 + lb_protocol = "http" + } + + listener { + instance_port = var.quorum_list_server_port + instance_protocol = "http" + lb_port = 443 + lb_protocol = "https" + ssl_certificate_id = aws_acm_certificate_validation.quorum_list_server[0].certificate_arn + } + + health_check { + healthy_threshold = 2 + interval = 30 + target = "HTTP:${var.quorum_list_server_port}/health" + timeout = 10 + unhealthy_threshold = 5 + } + + tags = { + Name = "dn-${terraform.workspace}-quorum-list-server" + DashNetwork = terraform.workspace + } +} + +resource "aws_acm_certificate" "quorum_list_server" { + count = var.quorum_list_server_count > 0 && length(var.main_domain) > 1 ? 1 : 0 + domain_name = "quorums.${var.public_network_name}.${var.main_domain}" + validation_method = "DNS" +} + +resource "aws_route53_record" "quorum_list_server_validation" { + for_each = var.quorum_list_server_count > 0 && length(var.main_domain) > 1 ? { + for dvo in aws_acm_certificate.quorum_list_server[0].domain_validation_options : dvo.domain_name => { + name = dvo.resource_record_name + record = dvo.resource_record_value + type = dvo.resource_record_type + } + } : {} + + zone_id = data.aws_route53_zone.main_domain[0].zone_id + allow_overwrite = true + name = each.value.name + records = [each.value.record] + ttl = 60 + type = each.value.type +} + +resource "aws_acm_certificate_validation" "quorum_list_server" { + count = var.quorum_list_server_count > 0 && length(var.main_domain) > 1 ? 1 : 0 + certificate_arn = aws_acm_certificate.quorum_list_server[0].arn + validation_record_fqdns = [for record in aws_route53_record.quorum_list_server_validation : record.fqdn] +} + +resource "aws_route53_record" "quorum_list_server" { + zone_id = data.aws_route53_zone.main_domain[count.index].zone_id + name = "quorums.${var.public_network_name}.${var.main_domain}" + type = "CNAME" + ttl = "300" + records = [aws_elb.quorum_list_server[count.index].dns_name] + + count = var.quorum_list_server_count > 0 && length(var.main_domain) > 1 ? 1 : 0 +} + resource "aws_route53_record" "logs" { zone_id = data.aws_route53_zone.main_domain[count.index].zone_id name = "logs.${var.public_network_name}.${var.main_domain}" @@ -381,4 +462,3 @@ resource "aws_eip" "vpn" { DashNetwork = terraform.workspace } } - diff --git a/terraform/aws/security_groups.tf b/terraform/aws/security_groups.tf index 0b2e4ac94..184c5f813 100644 --- a/terraform/aws/security_groups.tf +++ b/terraform/aws/security_groups.tf @@ -193,6 +193,25 @@ resource "aws_security_group" "http" { } } +resource "aws_security_group" "quorum_list_server" { + name = "${terraform.workspace}-quorum-list-server" + description = "standalone quorum list server" + vpc_id = aws_vpc.default.id + + ingress { + from_port = var.quorum_list_server_port + to_port = var.quorum_list_server_port + protocol = "tcp" + description = "Quorum list server API" + security_groups = [aws_security_group.elb.id] + } + + tags = { + Name = "dn-${terraform.workspace}-quorum-list-server" + DashNetwork = terraform.workspace + } +} + resource "aws_security_group" "logs" { name = "${terraform.workspace}-logs" description = "logs node" diff --git a/terraform/aws/templates/inventory/ansible_inventory.tpl b/terraform/aws/templates/inventory/ansible_inventory.tpl index 6fe21232d..08f50aaf8 100644 --- a/terraform/aws/templates/inventory/ansible_inventory.tpl +++ b/terraform/aws/templates/inventory/ansible_inventory.tpl @@ -9,6 +9,9 @@ ${logs_hosts} [wallet_nodes] ${wallet_node_hosts} +[quorum_list_servers] +${quorum_list_servers} + [mixer_nodes] ${mixer_hosts} diff --git a/terraform/aws/variables.tf b/terraform/aws/variables.tf index e130ae615..d597f8a1e 100644 --- a/terraform/aws/variables.tf +++ b/terraform/aws/variables.tf @@ -36,6 +36,16 @@ variable "insight_https_port" { default = 3002 } +variable "quorum_list_server_count" { + description = "Number of standalone quorum-list-server instances to create" + default = 0 +} + +variable "quorum_list_server_port" { + description = "HTTP port for the standalone quorum-list-server API" + default = 8080 +} + variable "ssh_port" { description = "SSH port" default = 22 @@ -216,6 +226,16 @@ variable "wallet_node_instance_size" { default = "micro" } +variable "quorum_list_server_instance_size" { + description = "Instance size of quorum-list-server nodes" + default = "micro" +} + +variable "quorum_list_server_root_disk_size" { + description = "Root disk size of quorum-list-server nodes" + default = 20 +} + variable "volume_type" { description = "Type of volume to use for block devices" default = "gp3"