Skip to content
Open
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
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions ansible/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
19 changes: 19 additions & 0 deletions ansible/roles/quorum_list_server/defaults/main.yml
Original file line number Diff line number Diff line change
@@ -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: ""
47 changes: 47 additions & 0 deletions ansible/roles/quorum_list_server/tasks/main.yml
Original file line number Diff line number Diff line change
@@ -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
24 changes: 24 additions & 0 deletions ansible/roles/quorum_list_server/templates/config.toml.j2
Original file line number Diff line number Diff line change
@@ -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 %}
79 changes: 79 additions & 0 deletions docs/quorum-list-server.md
Original file line number Diff line number Diff line change
@@ -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.<network>.networks.dash.org`
- A Route53 CNAME for `quorums.<network>.networks.dash.org`

For devnets, `<network>` 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://<seed-1-private-ip>: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.<network>.networks.dash.org/health
curl -fsS https://quorums.<network>.networks.dash.org/quorums
```
2 changes: 2 additions & 0 deletions terraform/aws/.terraform.lock.hcl

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 14 additions & 0 deletions terraform/aws/ansible_inventory_output.tf
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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.*,
Expand All @@ -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))
Expand Down
37 changes: 37 additions & 0 deletions terraform/aws/instances.tf
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
82 changes: 81 additions & 1 deletion terraform/aws/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
Expand Down Expand Up @@ -381,4 +462,3 @@ resource "aws_eip" "vpn" {
DashNetwork = terraform.workspace
}
}

Loading