oxen-core/utils/generate-quorum-matrix.py
Jason Rhinelander 300667e5f2
Add support for N=11 and N=12 quorum comms (#1178)
Previously we supported N=7 through N=10 and N=13 through N=20; with
pulse we need N=11 and N=12, so fill in the gaps so we now have a matrix
for everything from N=7 to N=20.

Connections are generated from utils/generate-quorum-matrix.py using
that same criteria as the existing connection matrices (that is, finding
a set of connections to ensure that every node is connected to every
other node twice with direct or two-hop connections).  The missing added
connection details are as follows:

┌────┬───────────────────────────────┬─────────────┬────────────────────┐
│ i  │ Connections (1 = out, x = in) │ ≤2 paths    │ Connectivity:      │
├────┼───────────────────────────────┼─────────────┼────────────────────┤
│ 0  │ -100xx1x001                   │ -3433343332 │ 6 (= 3 out + 3 in) │
│ 1  │ x-10x0011x0                   │ 3-333344322 │ 6 (= 3 out + 3 in) │
│ 2  │ 0x-11x01000                   │ 43-33232322 │ 5 (= 3 out + 2 in) │
│ 3  │ 00x-110010x                   │ 333-2322223 │ 5 (= 3 out + 2 in) │
│ 4  │ 11xx-010000                   │ 3332-424222 │ 5 (= 3 out + 2 in) │
│ 5  │ 101x0-100xx                   │ 33234-33323 │ 6 (= 3 out + 3 in) │
│ 6  │ x000xx-1010                   │ 443223-2222 │ 5 (= 2 out + 3 in) │
│ 7  │ 1xx000x-100                   │ 3422432-232 │ 5 (= 2 out + 3 in) │
│ 8  │ 0x0x000x-11                   │ 33322322-22 │ 5 (= 2 out + 3 in) │
│ 9  │ 010001x0x-0                   │ 322222232-2 │ 4 (= 2 out + 2 in) │
│ 10 │ x0010100x0-                   │ 2223232222- │ 4 (= 2 out + 2 in) │
└────┴───────────────────────────────┴─────────────┴────────────────────┘

┌────┬───────────────────────────────┬──────────────┬────────────────────┐
│ i  │ Connections (1 = out, x = in) │ ≤2 paths     │ Connectivity:      │
├────┼───────────────────────────────┼──────────────┼────────────────────┤
│ 0  │ -10xx0x00010                  │ -23332332222 │ 5 (= 2 out + 3 in) │
│ 1  │ x-110x001000                  │ 2-3422232222 │ 5 (= 3 out + 2 in) │
│ 2  │ 0x-10100001x                  │ 33-222233222 │ 5 (= 3 out + 2 in) │
│ 3  │ 1xx-1001x000                  │ 342-23323233 │ 6 (= 3 out + 3 in) │
│ 4  │ 100x-010010x                  │ 3222-2323332 │ 5 (= 3 out + 2 in) │
│ 5  │ 01x00-110x00                  │ 22232-323232 │ 5 (= 3 out + 2 in) │
│ 6  │ 1000xx-101x0                  │ 322333-32333 │ 6 (= 3 out + 3 in) │
│ 7  │ 000x0xx-1010                  │ 3332223-2322 │ 5 (= 2 out + 3 in) │
│ 8  │ 0x01000x-101                  │ 22333322-222 │ 5 (= 3 out + 2 in) │
│ 9  │ 0000x1x0x-01                  │ 222232332-23 │ 5 (= 2 out + 3 in) │
│ 10 │ x0x0001x00-1                  │ 2223333222-2 │ 5 (= 2 out + 3 in) │
│ 11 │ 00101000xxx-                  │ 22232232232- │ 5 (= 2 out + 3 in) │
└────┴───────────────────────────────┴──────────────┴────────────────────┘
2020-06-22 11:10:52 +10:00

173 lines
6.1 KiB
Python
Executable file

#!/usr/bin/python3
# This script generates the quorum interconnection matrices for different quorum sizes stored in
# src/quorumnet/conn_matrix.h and used to establish a quorum p2p mesh that establishes a set of
# connections with good partial connectivity using a minimal number of outgoing connections.
#
# It works by looking at the number of different paths of 2 hops or less (1 hop meaning a direct
# connection, 2 meaning a connection that passes through 1 intermediate node) and looks for
# connections between least-connected nodes that increases the most two-hop minimum paths. It then
# establishes this "best" connection, then restarts, continuing until it has achieved a minimum
# 2-hop connectivity from every node to every other node.
#
# This isn't any guarantee that this procedure generates the absolute best connectivity mesh, but it
# appears to do fairly well.
# If you want to see every path that gets added and the before/after two-hop-connectivity graph
# after each addition, set this to true:
TRACE = False
TRACE = True
# N sizes to calculate for. The default calculates for all possible quorums that are capable of
# achieving supermajority for blink and obligations quorums (10, 7 required) and checkpoint quorums
# (20, 13 required)
N = range(7, 21)
# This defines how much 2-path connectivity we require for different values of N
def min_connections(n):
return 4 if n <= 10 else 2
# Some stuff you might need: apt install python3-numpy python3-terminaltables
import numpy as np
from terminaltables import SingleTable
nodes = None
conns = None
def count_paths_within_two(i, j):
if i > j:
i, j = j, i
paths = 0
neighbours = []
for r in range(0, j):
if conns[r, j] or conns[j, r] > 0:
neighbours.append(r)
for c in range(j+1, nodes):
if conns[j, c] or conns[c, j] > 0:
neighbours.append(c)
for n in neighbours:
if n == i or conns[n, i] > 0 or conns[i, n] > 0:
paths += 1
return paths
def within_two_matrix():
z = np.zeros([nodes, nodes], dtype=int)
for i in range(nodes):
for j in range(nodes):
if i == j:
continue
z[i, j] = count_paths_within_two(i, j)
return z
def print_conns():
table_data = [["i", "Connections (1 = out, x = in)", "≤2 paths", "Connectivity:"], ["\n".join(str(x) for x in range(nodes)), "", "", ""]]
for r in range(nodes):
if table_data[1][1]:
table_data[1][1] += "\n"
table_data[1][1] += "".join('-' if r == c else 'x' if conns[c,r] else str(conns[r,c]) for c in range(nodes))
z = within_two_matrix()
for r in range(nodes):
if table_data[1][2]:
table_data[1][2] += "\n"
table_data[1][2] += "".join('-' if r == c else str(z[r,c]) for c in range(nodes))
for i in range(nodes):
myouts, myins = 0, 0
for j in range(nodes):
if conns[i, j]:
myouts += 1
elif conns[j, i]:
myins += 1
if table_data[1][3]:
table_data[1][3] += "\n"
table_data[1][3] += "{} (= {} out + {} in)".format(myouts + myins, myouts, myins)
print(SingleTable(table_data).table)
def min_within_two():
m = None
for i in range(nodes):
for j in range(i+1, nodes):
c = count_paths_within_two(i, j)
if m is None or c < m:
m = c
return m
def count_not_within_two(min_paths):
c = 0
for i in range(nodes):
for j in range(i+1, nodes):
if count_paths_within_two(i, j) <= min_paths:
c += 1
return c
def highlight(s, hl, code="\033[32m"):
return "{}{}\033[0m".format(code, s) if hl else "{}".format(s)
cpp = ""
for n in N:
nodes = n
conns = np.zeros([nodes, nodes], dtype=int)
target = min_connections(nodes)
min_paths = 0
last_min_paths = 0
while min_paths < target:
best = (nodes + nodes, count_not_within_two(min_paths))
best_ij = (0, 0)
for i in range(nodes):
outgoing_conns = sum(conns[i,j] for j in range(nodes)) + 1
for j in range(nodes):
incoming_conns = sum(conns[k,j] for k in range(nodes)) + 1
if i == j or conns[i, j] or conns[j, i]:
continue
conns[i, j] = 1
c = (outgoing_conns + incoming_conns, count_not_within_two(min_paths))
if c < best:
best_ij = (i, j)
best = c
best_conns = outgoing_conns
conns[i, j] = 0
if TRACE:
before = within_two_matrix()
conns[best_ij[0], best_ij[1]] = 1
if TRACE:
print("Chose connection [{},{}]".format(*best_ij))
after = within_two_matrix()
for r in range(nodes):
print("".join(
highlight('-' if r == c else '#' if conns[c,r] and conns[r,c] else 'x' if conns[c,r] else conns[r,c], (r,c)==best_ij) for c in range(nodes)
), end='')
print(" : " if r == nodes // 2 else " ", end='')
print("".join(highlight('-' if r == c else before[r,c], after[r,c] > before[r,c], "\033[33m") for c in range(nodes)), end='')
print(" => " if r == nodes // 2 else " ", end='')
print("".join(highlight('-' if r == c else after[r,c], after[r,c] > before[r,c]) for c in range(nodes)))
min_paths = min_within_two()
if min_paths > last_min_paths:
print("\n\n\n\n====================================================\nConstructed {}-min-two-hop-paths (N={})\n====================================================\n".format(
min_paths, nodes))
print_conns()
last_min_paths = min_paths
print("\n\n\n\n")
cpp += "template<> constexpr std::array<bool, {N}*{N}> quorum_conn_matrix<{N}>{{{{\n".format(N=n);
for r in range(nodes):
cpp += " " + ",".join(str(conns[r,c]) for c in range(nodes)) + ",\n"
cpp += "}};\n\n"
print("C++ code for quorumnet/conn_matrix.h:\n\n\n")
print(cpp)