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
23 changes: 23 additions & 0 deletions docs/ci-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,29 @@ cast wallet address $(cat ~/.agentkeys/heima-deployer-test.key)

The script defaults to derivation path `m/44'/60'/0'/0/0` (standard Ethereum BIP-44); pass `--index N` for a different address index. Idempotent — re-running with the same mnemonic prints `skip already-matches`; re-running with a different mnemonic refuses to overwrite (the existing key may own live deployed contracts).

The script also has standalone utility modes (run `--help` for the full reference):

```bash
# Derive + print the private key to stdout WITHOUT writing anything — safe for
# scanning --index candidates when a derived address doesn't match the key file:
echo "$MNEMONIC" | bash scripts/heima-deployer-from-mnemonic.sh --stdin --print-key --index 3

# EVM address → Heima Substrate twin (the account you send HEI to when funding
# an EVM address; Frontier HashedAddressMapping: blake2_256("evm:" || addr)).
# python3-stdlib only, no mnemonic needed, nothing written:
bash scripts/heima-deployer-from-mnemonic.sh --evm-to-substrate 0xdE644936D5B7d5d42032fd08bbA42Fbbfd6663Bc
# → JSON with raw 32-byte hex + SS58 prefix 31 (mainnet) / 131 (Paseo) / 42 (generic)

# Substrate address → decode to the raw account + SS58 re-encodings. The EVM
# source address CANNOT be computed back (one-way hash) — but you can verify a
# candidate pair (exit 0 = the SS58 IS that EVM address's twin):
bash scripts/heima-deployer-from-mnemonic.sh \
--substrate-to-evm 47hNCTi9Jrs86atvDj9AhY67X2vQEDzzHAvzapKvUpxXz6EX \
--verify-evm 0xdE644936D5B7d5d42032fd08bbA42Fbbfd6663Bc
```

The mnemonic flow itself also prints the derived address's Substrate twin (`twin:` line), so funding info is at hand immediately after deriving.

### 2b. Rotating the test deployer key (e.g. after CI exposure)

A rotated `TEST_HEIMA_DEPLOYER_KEY` is a **new wallet → new operator omni**, with no
Expand Down
2 changes: 1 addition & 1 deletion docs/spec/deployed-contracts.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Two distinct EVM accounts deploy AgentKeys contracts. They are **different keys*
| **Local / prod deploy** | `0xdE644936D5B7d5d42032fd08bbA42Fbbfd6663Bc` | `$HEIMA_DEPLOYER_KEY_FILE` (default `~/.agentkeys/heima-deployer.key`, never committed) | [`scripts/operator-workstation.env`](../../scripts/operator-workstation.env) `HEIMA_DEPLOYER_ADDR_HEIMA` |
| **Test / CI deploy** | `0x051eFD0C16943c38a6E95D14fb5Cda78735b475e` | `~/.agentkeys/heima-deployer-test.key` (operator-provided; wired into GitHub Actions secrets via [`scripts/ci-set-github-secrets.sh`](../../scripts/ci-set-github-secrets.sh)) | [`scripts/operator-workstation.test.env`](../../scripts/operator-workstation.test.env) `HEIMA_DEPLOYER_ADDR_HEIMA` |

- The prod deployer's Substrate twin (SS58 prefix 31) is `47NGSq6JE5ZSnymGNa4nFVjWbsuhTfoSKN2jtpk28mUyC1M3` — fund the EVM side via the twin, see [`scripts/evm-to-substrate-address.mjs`](../../scripts/evm-to-substrate-address.mjs).
- The prod deployer's Substrate twin (SS58 prefix 31) is `47hNCTi9Jrs86atvDj9AhY67X2vQEDzzHAvzapKvUpxXz6EX` — fund the EVM side by sending HEI to the twin (Substrate-side `balances.transferKeepAlive`). Compute the twin for any EVM address with `bash scripts/heima-deployer-from-mnemonic.sh --evm-to-substrate <0xADDR>` (python3-stdlib only; replaces the former `evm-to-substrate-address.mjs`, which required never-installed `@polkadot` deps). The reverse direction is a one-way hash (`blake2_256("evm:" ‖ addr)`) — verify a candidate pair with `--substrate-to-evm <SS58> --verify-evm <0xADDR>`. *(Correction 2026-06-11: this line previously listed `47NGSq6JE5ZSnymGNa4nFVjWbsuhTfoSKN2jtpk28mUyC1M3` as the twin — that account is NOT the hashed twin of `0xdE6449…63Bc` (cross-checked with both the `@polkadot` libs and an independent python implementation; likely it was the mnemonic's sr25519 address, the exact mix-up the old `.mjs` header warned about). HEI sent there does not credit the deployer's EVM balance.)*
- Heima Paseo testnet uses its own deployer `0xeBdE9E5F8c0495e87a871BF4f17Fb85e1bFE827F` (`HEIMA_PASEO_DEPLOYER_ADDR`) — currently unused (chain halted, see below).

---
Expand Down
57 changes: 0 additions & 57 deletions scripts/evm-to-substrate-address.mjs

This file was deleted.

181 changes: 179 additions & 2 deletions scripts/heima-deployer-from-mnemonic.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
# bash scripts/heima-deployer-from-mnemonic.sh --mnemonic-file /path/to/mnemonic.txt
# AGENTKEYS_DEPLOYER_MNEMONIC="word1 word2 …" bash scripts/heima-deployer-from-mnemonic.sh
# echo "$MNEMONIC" | bash scripts/heima-deployer-from-mnemonic.sh --stdin
# bash scripts/heima-deployer-from-mnemonic.sh --evm-to-substrate 0xdE6449…63Bc
# bash scripts/heima-deployer-from-mnemonic.sh --substrate-to-evm 47NGSq…C1M3 --verify-evm 0xdE6449…63Bc
#
# Flags:
# --test derive the TEST deployer (out path gets -test suffix)
Expand All @@ -21,8 +23,32 @@
# --path "m/44'/60'/…" full derivation path (overrides --index)
# --mnemonic-file <path> read mnemonic from this file (more secure than CLI)
# --stdin read mnemonic from stdin
# --print-key print the derived private key to stdout and exit —
# nothing is written to disk, no key-file comparison
# (address + path go to stderr so stdout stays pipeable)
# --evm-to-substrate <0xADDR>
# print the Heima Substrate TWIN of an EVM address and
# exit (no mnemonic needed, nothing written). Heima's
# Frontier HashedAddressMapping<BlakeTwo256> maps
# account32 = blake2_256("evm:" || eth_addr_bytes);
# output is JSON: raw 32-byte hex + SS58 prefix 31
# (Heima mainnet) / 131 (Heima Paseo) / 42 (generic).
# Fund a Heima EVM address by sending HEI to this twin
# via a Substrate-side balances.transferKeepAlive.
# --substrate-to-evm <SS58|0xHEX32>
# decode a Substrate address (SS58 checksum-verified,
# or raw 0x 32-byte hex) to its account + SS58
# re-encodings, and exit. NOTE: the EVM source address
# CANNOT be computed from the account — the mapping is
# a one-way hash with no inverse. Pass --verify-evm to
# check a candidate EVM address instead.
# --verify-evm <0xADDR> with --substrate-to-evm: exit 0 iff the Substrate
# account IS the Heima twin of this EVM address
# --help print this header
#
# The mnemonic flow also prints the derived address's Substrate twin (prefix 31)
# when python3 is available — that twin is the account you fund for EVM gas.
#
# Output path defaults (matches setup-heima.sh's HEIMA_DEPLOYER_KEY_FILE
# resolution: ${HEIMA_DEPLOYER_KEY_FILE:-$HOME/.agentkeys/${AGENTKEYS_CHAIN}-deployer.key}):
# prod → ~/.agentkeys/${AGENTKEYS_CHAIN:-heima}-deployer.key
Expand All @@ -43,6 +69,10 @@ INDEX="0"
DERIV_PATH=""
MNEMONIC_FILE=""
FROM_STDIN=0
PRINT_KEY=0
EVM_TO_SUB=""
SUB_TO_EVM=""
VERIFY_EVM=""

while [ $# -gt 0 ]; do
case "$1" in
Expand All @@ -52,8 +82,12 @@ while [ $# -gt 0 ]; do
--index) INDEX="$2"; shift 2 ;;
--path) DERIV_PATH="$2"; shift 2 ;;
--mnemonic-file) MNEMONIC_FILE="$2"; shift 2 ;;
--stdin) FROM_STDIN=1; shift ;;
-h|--help) sed -n '2,35p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
--stdin) FROM_STDIN=1; shift ;;
--print-key) PRINT_KEY=1; shift ;;
--evm-to-substrate) EVM_TO_SUB="$2"; shift 2 ;;
--substrate-to-evm) SUB_TO_EVM="$2"; shift 2 ;;
--verify-evm) VERIFY_EVM="$2"; shift 2 ;;
-h|--help) awk 'NR==1{next} /^#/{sub(/^# ?/,""); print; next} {exit}' "$0"; exit 0 ;;
*) echo "unknown arg: $1 (try --help)" >&2; exit 2 ;;
esac
done
Expand All @@ -71,6 +105,134 @@ if [ -z "$DERIV_PATH" ]; then
DERIV_PATH="m/44'/60'/0'/0/${INDEX}"
fi

# --- SS58 / twin-account helpers (python3 stdlib only, no external deps) ---
# Heima (Frontier HashedAddressMapping<BlakeTwo256>) maps EVM -> Substrate
# one-way: account32 = blake2_256("evm:" || eth_address_bytes). The reverse
# has no inverse (hash preimage), so `decode` + `verify` are the honest
# equivalents of "substrate-to-evm".
ss58_py() {
python3 - "$@" <<'PYEOF'
import sys, json, hashlib

ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"

def b58encode(raw):
n = int.from_bytes(raw, "big")
out = ""
while n:
n, r = divmod(n, 58)
out = ALPHABET[r] + out
pad = len(raw) - len(raw.lstrip(b"\x00"))
return ALPHABET[0] * pad + out

def b58decode(s):
n = 0
for c in s:
i = ALPHABET.find(c)
if i < 0:
raise ValueError(f"invalid base58 char {c!r}")
n = n * 58 + i
raw = n.to_bytes((n.bit_length() + 7) // 8, "big")
pad = len(s) - len(s.lstrip(ALPHABET[0]))
return b"\x00" * pad + raw

def prefix_bytes(p):
if p < 64:
return bytes([p])
return bytes([((p & 0xFC) >> 2) | 0x40, (p >> 8) | ((p & 0x03) << 6)])

def ss58_encode(account32, p):
payload = prefix_bytes(p) + account32
ck = hashlib.blake2b(b"SS58PRE" + payload, digest_size=64).digest()[:2]
return b58encode(payload + ck)

def ss58_decode(addr):
raw = b58decode(addr)
if len(raw) < 35:
raise ValueError("address too short for a 32-byte account")
body, ck = raw[:-2], raw[-2:]
if hashlib.blake2b(b"SS58PRE" + body, digest_size=64).digest()[:2] != ck:
raise ValueError("SS58 checksum mismatch -- not a valid address")
if body[0] < 64:
p, account = body[0], body[1:]
else:
p = ((body[0] & 0x3F) << 2) | (body[1] >> 6) | ((body[1] & 0x3F) << 8)
account = body[2:]
if len(account) != 32:
raise ValueError(f"decoded account is {len(account)} bytes, expected 32")
return p, account

def twin_of(evm_addr):
return hashlib.blake2b(b"evm:" + bytes.fromhex(evm_addr[2:]), digest_size=32).digest()

def parse_account(arg):
if arg.lower().startswith("0x"):
account = bytes.fromhex(arg[2:])
if len(account) != 32:
raise ValueError(f"hex account is {len(account)} bytes, expected 32")
return None, account
return ss58_decode(arg)

def account_json(account32, extra):
return json.dumps({**extra,
"substrate_account_hex": "0x" + account32.hex(),
"ss58_heima_mainnet": ss58_encode(account32, 31),
"ss58_heima_paseo": ss58_encode(account32, 131),
"ss58_generic": ss58_encode(account32, 42),
}, indent=2)

try:
cmd = sys.argv[1]
if cmd == "twin":
print(account_json(twin_of(sys.argv[2]), {"evm_address": sys.argv[2]}))
elif cmd == "twin-ss58":
print(ss58_encode(twin_of(sys.argv[2]), 31))
elif cmd == "decode":
p, account = parse_account(sys.argv[2])
print(account_json(account, {"input": sys.argv[2], "ss58_prefix": p}))
elif cmd == "verify":
_, account = parse_account(sys.argv[2])
print("true" if account == twin_of(sys.argv[3]) else "false")
else:
sys.exit(f"unknown subcommand: {cmd}")
except ValueError as e:
sys.exit(f"error: {e}")
PYEOF
}

is_evm_addr() { [[ "$1" =~ ^0x[0-9a-fA-F]{40}$ ]]; }

if [ -n "$EVM_TO_SUB" ] || [ -n "$SUB_TO_EVM" ]; then
command -v python3 >/dev/null || { echo "python3 required for address conversion" >&2; exit 1; }
fi

if [ -n "$EVM_TO_SUB" ]; then
is_evm_addr "$EVM_TO_SUB" || { echo "--evm-to-substrate: not a 0x 40-hex EVM address: $EVM_TO_SUB" >&2; exit 1; }
echo "[heima twin] blake2_256(\"evm:\" || $EVM_TO_SUB) — fund the EVM side by sending HEI to this Substrate account:" >&2
ss58_py twin "$EVM_TO_SUB"
exit 0
fi

if [ -n "$SUB_TO_EVM" ]; then
ss58_py decode "$SUB_TO_EVM"
if [ -n "$VERIFY_EVM" ]; then
is_evm_addr "$VERIFY_EVM" || { echo "--verify-evm: not a 0x 40-hex EVM address: $VERIFY_EVM" >&2; exit 1; }
if [ "$(ss58_py verify "$SUB_TO_EVM" "$VERIFY_EVM")" = "true" ]; then
echo "verify: MATCH — $SUB_TO_EVM IS the Heima twin of $VERIFY_EVM" >&2
exit 0
fi
echo "verify: NO MATCH — $SUB_TO_EVM is NOT the twin of $VERIFY_EVM" >&2
exit 1
fi
cat >&2 <<'NOTE'
note: the EVM source address CANNOT be computed from a Substrate account —
Heima's mapping is one-way (account = blake2_256("evm:" || eth_addr), a hash
with no inverse). If you have a candidate EVM address, re-run with
--verify-evm 0x… to check whether this account is its twin.
NOTE
exit 0
fi

command -v cast >/dev/null || {
echo "cast not found — install Foundry: curl -L https://foundry.paradigm.xyz | bash && foundryup" >&2
exit 1
Expand Down Expand Up @@ -116,12 +278,26 @@ if [[ ! "$PRIV" =~ ^0x[0-9a-fA-F]{64}$ ]]; then
fi

ADDR=$(cast wallet address "$PRIV")
TWIN_SS58=""
if command -v python3 >/dev/null; then
TWIN_SS58=$(ss58_py twin-ss58 "$ADDR" 2>/dev/null || true)
fi
twin_line() { if [ -n "$TWIN_SS58" ]; then echo "${1:-}twin: $TWIN_SS58 (Substrate account to fund, SS58 prefix 31)"; fi; }

if [ "$PRINT_KEY" = "1" ]; then
echo "address: $ADDR" >&2
echo "path: $DERIV_PATH" >&2
twin_line >&2
printf '%s\n' "$PRIV"
exit 0
fi

if [ -f "$OUT" ]; then
EXISTING=$(tr -d '\r\n[:space:]' < "$OUT")
if [ "$EXISTING" = "$PRIV" ]; then
echo "skip already-matches ($OUT)"
echo "address: $ADDR"
twin_line
exit 0
fi
EXIST_ADDR=$(cast wallet address "$EXISTING" 2>/dev/null || echo "<unparseable>")
Expand Down Expand Up @@ -153,6 +329,7 @@ echo " chain: $CHAIN"
echo " path: $DERIV_PATH"
echo " out: $OUT"
echo " address: $ADDR"
twin_line " "
echo
echo "Next: setup-heima.sh will pick this up automatically — for test:"
echo " HEIMA_DEPLOYER_KEY_FILE=$OUT MAINNET_CONFIRM=1 \\"
Expand Down
Loading