aere-node/validator/AereCandidacy.java

499 lines
26 KiB
Java

/*
* AereCandidacy.java - the signing helper of the Aere Network validator kit (chain 2800).
*
* It runs on the CANDIDATE's machine, against the lib/ directory of the candidate's OWN build of the
* Aere fork (run-node.sh --no-start builds it from the published patch series), with the JDK's
* single-file source launcher:
*
* java -cp '<build>/build/install/besu/lib/*' AereCandidacy.java <command> [options]
*
* It uses the classes of that build and nothing else: the registry v3 forms (PqRegistryV3, patch 0008),
* the Falcon-512 and SLH-DSA-SHA2-128s schemes the node signs with, Besu's secp256k1 and Besu's own
* KeyPairUtil (the code path that creates data/key when a node starts for the first time).
*
* WHAT IT NEVER DOES: print, log, copy or transmit any private key, or any part of one. It reads the
* key files, signs, verifies its own signatures before writing anything, and writes PUBLIC data only.
* The shell scripts next to it check that again on their own output (validator-candidacy.sh).
*
* Commands:
* params --params <admission-params.json> prints "chainId bindHeight count index"
* node-key --data-path <dir> creates <dir>/key with Besu's KeyPairUtil if
* (and only if) it does not exist
* ids --node-key-file <key> --falcon-key <f> --slh-key <s> prints the public identity (JSON)
* candidacy --params <p> --row <row.json> --falcon-key <f> --slh-key <s> --node-key-file <key>
* --enode-host <host> --p2p-port <n> --issued <YYYY-MM-DD> --clock-measured-at <unix>
* --clock-head <n> --clock-samples <n> --clock-dmin <s> --clock-dmax <s> --out <file>
* clock-report --chain-id <c> --node-key-file <key> --enode-host <host> --p2p-port <n>
* --clock-measured-at <unix> --clock-head <n> --clock-samples <n> --clock-dmin <s>
* --clock-dmax <s> --out <file>
*
* The two signed messages (also written out in validator/README.md, "The candidacy file", so that
* anyone can verify a candidacy without this file):
*
* candidacy = keccak256("AERE-CANDIDACY-1" || u64 chainId || u64 bindHeight || u32 count || u32 index
* || u32 len(issued) || issued (ASCII, YYYY-MM-DD) || id (20 bytes) || rowHash (32 bytes))
* clock = keccak256("AERE-CLOCK-1" || u64 chainId || u64 measuredAt || u64 headNumber || u32 samples
* || i32 dMin || i32 dMax)
*
* signed by: candidacy -> the node key (secp256k1, 65 bytes r||s||v), the Falcon-512 key and the
* SLH-DSA-SHA2-128s key of the row; clock -> the node key. Integers are big-endian; i32 is two's
* complement. rowHash is the registry v3 row hash (AIP-22 section 3.2, PqRegistryV3.rowHash). The node
* key is bound through its ADDRESS: the secp256k1 signatures must recover to the row's ecdsa address,
* which is also its transport address, and the enode's public key must hash to that same address.
*
* SPDX-License-Identifier: Apache-2.0
*/
import org.hyperledger.besu.consensus.common.bft.FalconSealScheme;
import org.hyperledger.besu.consensus.common.bft.PqRegistryV3;
import org.hyperledger.besu.consensus.common.bft.SealScheme;
import org.hyperledger.besu.consensus.common.bft.SealSchemes;
import org.hyperledger.besu.crypto.Hash;
import org.hyperledger.besu.crypto.KeyPair;
import org.hyperledger.besu.crypto.KeyPairUtil;
import org.hyperledger.besu.crypto.SECPPublicKey;
import org.hyperledger.besu.crypto.SECPSignature;
import org.hyperledger.besu.crypto.SignatureAlgorithm;
import org.hyperledger.besu.crypto.SignatureAlgorithmFactory;
import org.hyperledger.besu.ethereum.core.Util;
import java.io.IOException;
import java.io.StringReader;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.Properties;
import java.util.Set;
import java.util.TreeMap;
import java.util.regex.Pattern;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.apache.tuweni.bytes.Bytes;
import org.apache.tuweni.bytes.Bytes32;
import org.bouncycastle.pqc.crypto.falcon.FalconParameters;
import org.bouncycastle.pqc.crypto.falcon.FalconPrivateKeyParameters;
public class AereCandidacy {
static final String CANDIDACY_DOMAIN = "AERE-CANDIDACY-1";
static final String CLOCK_DOMAIN = "AERE-CLOCK-1";
static final String CANDIDACY_FORMAT = "aere-validator-candidacy/1";
static final String CLOCK_FORMAT = "aere-validator-clock/1";
static final String PARAMS_FORMAT = "aere-admission-params/1";
static final ObjectMapper M = new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT);
static final SignatureAlgorithm EC = SignatureAlgorithmFactory.getInstance();
static final Pattern HOST = Pattern.compile("^([A-Za-z0-9]([A-Za-z0-9.-]{0,251}[A-Za-z0-9])?|\\[[0-9A-Fa-f:.]{2,45}\\])$");
static final Pattern DATE = Pattern.compile("^20[0-9]{2}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$");
public static void main(final String[] args) throws IOException {
if (args.length == 0) {
die(2, "usage: AereCandidacy.java params|node-key|ids|candidacy|clock-report [options] (see the header)");
}
final Map<String, String> o = new HashMap<>();
for (int i = 1; i < args.length; i++) {
if (!args[i].startsWith("--") || i + 1 >= args.length) {
die(2, "options are --name value pairs; bad argument: " + args[i]);
}
o.put(args[i].substring(2), args[++i]);
}
try {
switch (args[0]) {
case "params" -> {
final long[] p = params(o);
System.out.println(p[0] + " " + p[1] + " " + p[2] + " " + p[3]);
}
case "node-key" -> nodeKey(o);
case "ids" -> ids(o);
case "candidacy" -> candidacy(o);
case "clock-report" -> clockReport(o);
default -> die(2, "unknown command " + args[0]);
}
} catch (final IllegalArgumentException | IllegalStateException e) {
// messages here never carry key material: every one of them is written below by hand
die(1, "REFUSED: " + e.getMessage());
}
}
// ---- the admission parameters the Foundation sends (public) ----------------------------------
static long[] params(final Map<String, String> o) throws IOException {
final JsonNode p = M.readTree(Files.readString(Path.of(need(o, "params")), StandardCharsets.UTF_8));
if (p == null || !p.isObject()) {
throw new IllegalArgumentException("the admission parameters are not a JSON object");
}
final Set<String> allowed = Set.of("format", "chainId", "bindHeight", "count", "index", "note");
for (final Iterator<String> it = p.fieldNames(); it.hasNext(); ) {
final String f = it.next();
if (!allowed.contains(f)) {
throw new IllegalArgumentException("the admission parameters carry an unknown field: " + f);
}
}
if (!PARAMS_FORMAT.equals(p.path("format").asText())) {
throw new IllegalArgumentException("the admission parameters are not of format " + PARAMS_FORMAT);
}
final long chain = integral(p, "chainId");
final long bind = integral(p, "bindHeight");
final long count = integral(p, "count");
final long index = integral(p, "index");
if (chain <= 0 || bind <= 0 || count < 1 || count > 64 || index < 0 || index >= count) {
throw new IllegalArgumentException("the admission parameters are out of range (chainId, bindHeight > 0; 1 <= count <= 64; 0 <= index < count)");
}
return new long[] {chain, bind, count, index};
}
// ---- the node key: Besu's own path, never a second implementation -----------------------------
static void nodeKey(final Map<String, String> o) {
final Path dir = Path.of(need(o, "data-path"));
final Path key = dir.resolve("key");
if (Files.exists(key)) {
throw new IllegalStateException(key + " already exists; nothing was created (the node key is never replaced)");
}
if (!Files.isDirectory(dir)) {
throw new IllegalStateException(dir + " is not a directory");
}
final KeyPair kp = KeyPairUtil.loadKeyPair(dir);
final ObjectNode out = M.createObjectNode();
out.put("created", key.toString());
out.put("address", hx(Util.publicKeyToAddress(kp.getPublicKey()).getBytes().toArrayUnsafe()));
out.put("nodePublicKey", kp.getPublicKey().getEncodedBytes().toHexString());
print(out);
}
// ---- the public identity ------------------------------------------------------------------------
static void ids(final Map<String, String> o) throws IOException {
final KeyPair kp = nodeKeyPair(need(o, "node-key-file"));
final Falcon f = falcon(need(o, "falcon-key"), -1);
final Slh s = slh(need(o, "slh-key"), -1);
if (f.index != s.index) {
throw new IllegalArgumentException("the Falcon key file is for index " + f.index + ", the SLH-DSA key file for index " + s.index);
}
final ObjectNode out = M.createObjectNode();
out.put("address", hx(Util.publicKeyToAddress(kp.getPublicKey()).getBytes().toArrayUnsafe()));
out.put("nodePublicKey", kp.getPublicKey().getEncodedBytes().toHexString());
out.put("index", f.index);
out.put("id", hx(PqRegistryV3.idOf(f.std)));
out.put("falcon512PublicKeySha256", Hash.sha256(Bytes.wrap(f.std)).toHexString());
out.put("slhDsaSha2128sPublicKey", hx(s.pk));
print(out);
}
// ---- the candidacy --------------------------------------------------------------------------------
static void candidacy(final Map<String, String> o) throws IOException {
final long[] p = params(o);
final long chain = p[0], bind = p[1];
final int count = (int) p[2], index = (int) p[3];
final String issued = need(o, "issued");
if (!DATE.matcher(issued).matches()) {
throw new IllegalArgumentException("--issued must be a UTC date YYYY-MM-DD");
}
final KeyPair kp = nodeKeyPair(need(o, "node-key-file"));
final byte[] addr = Util.publicKeyToAddress(kp.getPublicKey()).getBytes().toArrayUnsafe();
final byte[] nodePub = kp.getPublicKey().getEncodedBytes().toArrayUnsafe();
final Falcon f = falcon(need(o, "falcon-key"), index);
final Slh s = slh(need(o, "slh-key"), index);
final String enode = enode(nodePub, o);
final Path out = Path.of(need(o, "out"));
if (Files.exists(out)) {
throw new IllegalStateException(out + " exists; a candidacy file is never overwritten");
}
// the row, as PqRegistryV3Tool wrote it: it must be THIS node's keys, bound to THESE parameters
final JsonNode rf = M.readTree(Files.readString(Path.of(need(o, "row")), StandardCharsets.UTF_8));
if (rf == null || rf.path("index").asInt(-1) != index || !rf.has("row")) {
throw new IllegalArgumentException("the row file is not a PqRegistryV3Tool row for index " + index);
}
final JsonNode row = rf.get("row");
final byte[] id = hex(text(row, "id"));
if (!Arrays.equals(id, PqRegistryV3.idOf(f.std))) {
throw new IllegalArgumentException("the row's id is not the derivation of this Falcon key (a new id must be)");
}
if (!Arrays.equals(hex(text(row.path("keys"), "falcon-512")), f.std)
|| !Arrays.equals(hex(text(row.path("keys"), "slh-dsa-sha2-128s")), s.pk)) {
throw new IllegalArgumentException("the row carries other public keys than the key files given");
}
if (row.has("rotation")) {
throw new IllegalArgumentException("the row carries a rotation; a new validator's row never does");
}
final byte[] transport = hex(text(row.path("transport"), "secp256k1Address"));
final byte[] ecAddr = hex(text(row.path("ecdsa"), "addr"));
if (!Arrays.equals(transport, addr) || !Arrays.equals(ecAddr, addr)) {
throw new IllegalArgumentException("the row's transport and ecdsa addresses must both be the address of the NODE key"
+ " (the consensus key is the node key; a validator whose node key differs receives no consensus message)");
}
final byte[] falconPop = hex(text(row.path("pop"), "falcon-512"));
final byte[] slhPop = hex(text(row.path("pop"), "slh-dsa-sha2-128s"));
final TreeMap<Integer, PqRegistryV3.Key> keys = new TreeMap<>();
keys.put(SealSchemes.FALCON_512.wireId() & 0xff, new PqRegistryV3.Key(SealSchemes.FALCON_512, f.std, falconPop));
keys.put(SealSchemes.SLH_DSA_128S.wireId() & 0xff, new PqRegistryV3.Key(SealSchemes.SLH_DSA_128S, s.pk, slhPop));
final Bytes32 rowHash =
PqRegistryV3.rowHash(chain, bind, count, new PqRegistryV3.Row(index, id, keys, transport, Map.of(), null, null));
// the row's own proofs, checked again here before this node signs anything about it
if (!SealSchemes.FALCON_512.verifyRaw(f.raw896,
PqRegistryV3.popMessage(chain, bind, count, index, id, SealSchemes.FALCON_512.wireId(), f.std).toArrayUnsafe(), falconPop)
|| !SealSchemes.SLH_DSA_128S.verifyRaw(s.pk,
PqRegistryV3.popMessage(chain, bind, count, index, id, SealSchemes.SLH_DSA_128S.wireId(), s.pk).toArrayUnsafe(), slhPop)) {
throw new IllegalStateException("a proof of possession in the row does not verify for these parameters");
}
final SECPSignature claim = EC.decodeSignature(Bytes.wrap(hex(text(row.path("ecdsa"), "claim"))));
final Optional<SECPPublicKey> claimer = EC.recoverPublicKeyFromSignature(PqRegistryV3.claimDigest(chain, rowHash), claim);
if (claimer.isEmpty() || !Arrays.equals(Util.publicKeyToAddress(claimer.get()).getBytes().toArrayUnsafe(), addr)) {
throw new IllegalStateException("the row's ecdsa claim does not recover to the node key's address");
}
// the candidacy proof: three signatures over a message bound to the chain, the epoch, the date and the row
final Bytes32 msg = candidacyMessage(chain, bind, count, index, issued, id, rowHash);
final SECPSignature es = EC.sign(msg, kp);
final byte[] fs = ((FalconSealScheme) SealSchemes.FALCON_512).signWithParams(f.sk, msg.toArrayUnsafe())
.orElseThrow(() -> new IllegalStateException("the Falcon key did not sign"));
final byte[] ss = SealSchemes.SLH_DSA_128S.sign(s.sk, msg.toArrayUnsafe())
.orElseThrow(() -> new IllegalStateException("the SLH-DSA key did not sign"));
selfCheck(msg, es, nodePub, f, fs, s, ss);
final ObjectNode clock = clock(o, chain, nodePub, kp);
final ObjectNode c = M.createObjectNode();
c.put("format", CANDIDACY_FORMAT);
c.put("chainId", chain);
c.put("bindHeight", bind);
c.put("count", count);
c.put("index", index);
c.put("issued", issued);
c.put("enode", enode);
c.put("address", hx(addr));
c.put("id", hx(id));
c.set("row", row);
c.put("rowHash", rowHash.toHexString());
final ObjectNode pr = c.putObject("proof");
pr.put("domain", CANDIDACY_DOMAIN);
pr.put("message", msg.toHexString());
pr.put("secp256k1", es.encodedBytes().toHexString());
pr.put("falcon-512", hx(fs));
pr.put("slh-dsa-sha2-128s", hx(ss));
c.set("clock", clock);
Files.writeString(out, M.writeValueAsString(c) + "\n", StandardCharsets.UTF_8, StandardOpenOption.CREATE_NEW);
final ObjectNode sum = M.createObjectNode();
sum.put("written", out.toString());
sum.put("enode", enode);
sum.put("address", hx(addr));
sum.put("id", hx(id));
sum.put("rowHash", rowHash.toHexString());
print(sum);
}
static void clockReport(final Map<String, String> o) throws IOException {
final long chain = Long.parseLong(need(o, "chain-id"));
final KeyPair kp = nodeKeyPair(need(o, "node-key-file"));
final byte[] nodePub = kp.getPublicKey().getEncodedBytes().toArrayUnsafe();
final Path out = Path.of(need(o, "out"));
if (Files.exists(out)) {
throw new IllegalStateException(out + " exists; a clock report is never overwritten");
}
final ObjectNode c = M.createObjectNode();
c.put("format", CLOCK_FORMAT);
c.put("chainId", chain);
c.put("enode", enode(nodePub, o));
c.put("address", hx(Util.publicKeyToAddress(kp.getPublicKey()).getBytes().toArrayUnsafe()));
c.set("clock", clock(o, chain, nodePub, kp));
Files.writeString(out, M.writeValueAsString(c) + "\n", StandardCharsets.UTF_8, StandardOpenOption.CREATE_NEW);
final ObjectNode sum = M.createObjectNode();
sum.put("written", out.toString());
sum.set("clock", c.get("clock"));
print(sum);
}
// ---- messages -------------------------------------------------------------------------------------
static Bytes32 candidacyMessage(final long chain, final long bind, final int count, final int index, final String issued,
final byte[] id, final Bytes32 rowHash) {
final byte[] d = CANDIDACY_DOMAIN.getBytes(StandardCharsets.US_ASCII);
final byte[] is = issued.getBytes(StandardCharsets.US_ASCII);
final ByteBuffer b = ByteBuffer.allocate(d.length + 8 + 8 + 4 + 4 + 4 + is.length + 20 + 32);
b.put(d).putLong(chain).putLong(bind).putInt(count).putInt(index).putInt(is.length).put(is).put(id)
.put(rowHash.toArrayUnsafe());
return Hash.keccak256(Bytes.wrap(b.array()));
}
static Bytes32 clockMessage(final long chain, final long measuredAt, final long head, final int samples, final int dMin,
final int dMax) {
final byte[] d = CLOCK_DOMAIN.getBytes(StandardCharsets.US_ASCII);
final ByteBuffer b = ByteBuffer.allocate(d.length + 8 + 8 + 8 + 4 + 4 + 4);
b.put(d).putLong(chain).putLong(measuredAt).putLong(head).putInt(samples).putInt(dMin).putInt(dMax);
return Hash.keccak256(Bytes.wrap(b.array()));
}
static ObjectNode clock(final Map<String, String> o, final long chain, final byte[] nodePub, final KeyPair kp) {
final long at = Long.parseLong(need(o, "clock-measured-at"));
final long head = Long.parseLong(need(o, "clock-head"));
final int samples = Integer.parseInt(need(o, "clock-samples"));
final int dMin = Integer.parseInt(need(o, "clock-dmin"));
final int dMax = Integer.parseInt(need(o, "clock-dmax"));
if (samples < 1 || dMin > dMax || head < 1 || at < 1) {
throw new IllegalArgumentException("the clock measurement is malformed");
}
final Bytes32 cm = clockMessage(chain, at, head, samples, dMin, dMax);
final SECPSignature cs = EC.sign(cm, kp);
final Optional<SECPPublicKey> back = EC.recoverPublicKeyFromSignature(cm, cs);
if (back.isEmpty() || !Arrays.equals(back.get().getEncodedBytes().toArrayUnsafe(), nodePub)) {
throw new IllegalStateException("the clock signature does not recover to the node key; nothing written");
}
final ObjectNode c = M.createObjectNode();
c.put("domain", CLOCK_DOMAIN);
c.put("measuredAt", at);
c.put("headNumber", head);
c.put("samples", samples);
c.put("dMin", dMin);
c.put("dMax", dMax);
c.put("secp256k1", cs.encodedBytes().toHexString());
return c;
}
static void selfCheck(final Bytes32 msg, final SECPSignature es, final byte[] nodePub, final Falcon f, final byte[] fs,
final Slh s, final byte[] ss) {
final Optional<SECPPublicKey> back = EC.recoverPublicKeyFromSignature(msg, es);
if (back.isEmpty() || !Arrays.equals(back.get().getEncodedBytes().toArrayUnsafe(), nodePub)) {
throw new IllegalStateException("the secp256k1 signature does not recover to the node key; nothing written");
}
if (!SealSchemes.FALCON_512.verifyRaw(f.raw896, msg.toArrayUnsafe(), fs)) {
throw new IllegalStateException("the Falcon-512 signature does not verify; nothing written");
}
if (!SealSchemes.SLH_DSA_128S.verifyRaw(s.pk, msg.toArrayUnsafe(), ss)) {
throw new IllegalStateException("the SLH-DSA signature does not verify; nothing written");
}
}
// ---- key files (read, never printed) -----------------------------------------------------------
record Falcon(int index, FalconPrivateKeyParameters sk, byte[] raw896, byte[] std) {}
record Slh(int index, SealScheme.PrivateHandle sk, byte[] pk) {}
static Falcon falcon(final String file, final int wantIndex) throws IOException {
final Properties fp = new Properties();
fp.load(new StringReader(Files.readString(Path.of(file), StandardCharsets.UTF_8)));
final int index = Integer.parseInt(prop(fp, "index", "the Falcon key file"));
if (wantIndex >= 0 && index != wantIndex) {
throw new IllegalArgumentException("the Falcon key file is for index " + index + ", the admission parameters say " + wantIndex);
}
final byte[] h = hex(prop(fp, "pk", "the Falcon key file"));
if (h.length != PqRegistryV3.FALCON_512_PK_LENGTH - 1) {
throw new IllegalArgumentException("the Falcon key file's public key is not 896 bytes");
}
final FalconPrivateKeyParameters sk = new FalconPrivateKeyParameters(FalconParameters.falcon_512,
hex(prop(fp, "f", "the Falcon key file")), hex(prop(fp, "g", "the Falcon key file")), hex(prop(fp, "F", "the Falcon key file")), h);
final byte[] std = new byte[PqRegistryV3.FALCON_512_PK_LENGTH];
std[0] = PqRegistryV3.FALCON_512_PK_HEADER;
System.arraycopy(h, 0, std, 1, h.length);
// the private half must match the public half: sign a probe and verify it under the public key
final byte[] probe = Hash.keccak256(Bytes.wrap("AERE-KIT-PROBE".getBytes(StandardCharsets.US_ASCII))).toArrayUnsafe();
final byte[] sig = ((FalconSealScheme) SealSchemes.FALCON_512).signWithParams(sk, probe)
.orElseThrow(() -> new IllegalArgumentException("the Falcon key does not sign"));
if (!SealSchemes.FALCON_512.verifyRaw(h, probe, sig)) {
throw new IllegalArgumentException("the Falcon key file's private and public halves do not match");
}
return new Falcon(index, sk, h, std);
}
static Slh slh(final String file, final int wantIndex) throws IOException {
final Properties sp = new Properties();
sp.load(new StringReader(Files.readString(Path.of(file), StandardCharsets.UTF_8)));
final int index = Integer.parseInt(prop(sp, "index", "the SLH-DSA key file"));
if (wantIndex >= 0 && index != wantIndex) {
throw new IllegalArgumentException("the SLH-DSA key file is for index " + index + ", the admission parameters say " + wantIndex);
}
if (!"slh-dsa-sha2-128s".equals(sp.getProperty("alg", "").trim())) {
throw new IllegalArgumentException("the SLH-DSA key file does not name alg=slh-dsa-sha2-128s");
}
final SealScheme.PrivateHandle sk = SealSchemes.SLH_DSA_128S.parsePrivateKey(hex(prop(sp, "sk", "the SLH-DSA key file")))
.orElseThrow(() -> new IllegalArgumentException("the SLH-DSA private key does not parse"));
final byte[] pk = hex(prop(sp, "pk", "the SLH-DSA key file"));
final byte[] probe = Hash.keccak256(Bytes.wrap("AERE-KIT-PROBE".getBytes(StandardCharsets.US_ASCII))).toArrayUnsafe();
final byte[] sig = SealSchemes.SLH_DSA_128S.sign(sk, probe).orElseThrow(() -> new IllegalArgumentException("the SLH-DSA key does not sign"));
if (!SealSchemes.SLH_DSA_128S.verifyRaw(pk, probe, sig)) {
throw new IllegalArgumentException("the SLH-DSA key file's private and public halves do not match");
}
return new Slh(index, sk, pk);
}
static KeyPair nodeKeyPair(final String file) throws IOException {
final String t = Files.readString(Path.of(file), StandardCharsets.UTF_8).trim();
if (!t.matches("^(0x)?[0-9a-fA-F]{64}$")) {
throw new IllegalArgumentException("the node key file does not hold a 32-byte hex key");
}
return EC.createKeyPair(EC.createPrivateKey(Bytes32.fromHexString(t)));
}
static String enode(final byte[] nodePub, final Map<String, String> o) {
final String host = need(o, "enode-host");
final int port = Integer.parseInt(need(o, "p2p-port"));
if (!HOST.matcher(host).matches() || port < 1 || port > 65535) {
throw new IllegalArgumentException("--enode-host must be a host name or an IP address and --p2p-port a port number");
}
return "enode://" + hx(nodePub).substring(2) + "@" + host + ":" + port;
}
// ---- small helpers -----------------------------------------------------------------------------
static String prop(final Properties p, final String k, final String what) {
final String v = p.getProperty(k);
if (v == null || v.isBlank()) {
throw new IllegalArgumentException(what + " has no '" + k + "' entry");
}
return v.trim();
}
static long integral(final JsonNode p, final String k) {
final JsonNode v = p.get(k);
if (v == null || !v.isIntegralNumber()) {
throw new IllegalArgumentException("the admission parameters need the integer field " + k);
}
return v.asLong();
}
static String text(final JsonNode n, final String k) {
final JsonNode v = n == null ? null : n.get(k);
if (v == null || !v.isTextual()) {
throw new IllegalArgumentException("the row has no text field " + k);
}
return v.asText();
}
static String need(final Map<String, String> o, final String k) {
final String v = o.get(k);
if (v == null || v.isBlank()) {
throw new IllegalArgumentException("missing --" + k);
}
return v;
}
static byte[] hex(final String s) {
return Bytes.fromHexStringLenient(s.trim()).toArrayUnsafe();
}
static String hx(final byte[] b) {
final StringBuilder sb = new StringBuilder("0x");
for (final byte x : b) {
sb.append(String.format(Locale.ROOT, "%02x", x & 0xff));
}
return sb.toString();
}
static void print(final ObjectNode n) {
try {
System.out.println(M.writeValueAsString(n));
} catch (final IOException e) {
die(2, "cannot write output");
}
}
static void die(final int code, final String msg) {
System.err.println(msg);
System.exit(code);
}
}