One Lethean seed phrase derives keys for: - Main chain (ed25519, native) - HNS sidechain (secp256k1, derived via HKDF) - Document signing (ed25519, for hash timestamping) - Audit trail (ed25519, for revision verification) Domain-separated HKDF ensures keys can't be reversed or collide. Foundation for Web3 identity bridge and corporate document control. Co-Authored-By: Charon <charon@lethean.io>
153 lines
5.4 KiB
Python
153 lines
5.4 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Lethean Key Bridge Prototype
|
|
Derives an HNS sidechain secp256k1 keypair from a Lethean ed25519 spend key.
|
|
|
|
One seed phrase → both chains.
|
|
|
|
Lethean seed phrase
|
|
↓
|
|
ed25519 spend key (32 bytes)
|
|
├── Main chain wallet (native CryptoNote)
|
|
└── HKDF("lethean-hns-bridge", spend_key)
|
|
↓
|
|
secp256k1 private key (32 bytes)
|
|
└── Sidechain wallet (derived)
|
|
|
|
Usage:
|
|
python3 key-bridge-prototype.py <hex_spend_key>
|
|
python3 key-bridge-prototype.py --from-wallet http://127.0.0.1:46944/json_rpc
|
|
|
|
Dependencies:
|
|
pip install cryptography secp256k1 (or use hashlib for prototype)
|
|
"""
|
|
|
|
import hashlib
|
|
import hmac
|
|
import sys
|
|
import json
|
|
import urllib.request
|
|
|
|
|
|
def derive_sidechain_key(spend_key_hex: str) -> dict:
|
|
"""
|
|
Derive a secp256k1 private key from a Lethean ed25519 spend key.
|
|
|
|
Uses HKDF-like construction:
|
|
prk = HMAC-SHA256(key="lethean-hns-bridge", msg=spend_key)
|
|
secp256k1_key = HMAC-SHA256(key=prk, msg="hns-sidechain-v1" || 0x01)
|
|
|
|
The domain separation ensures:
|
|
- Different derivation paths produce different keys
|
|
- The ed25519 key cannot be recovered from the secp256k1 key
|
|
- Future derivation paths (v2, v3) won't collide
|
|
"""
|
|
spend_key = bytes.fromhex(spend_key_hex)
|
|
assert len(spend_key) == 32, f"Spend key must be 32 bytes, got {len(spend_key)}"
|
|
|
|
# Step 1: Extract — domain-separated PRK
|
|
salt = b"lethean-hns-bridge"
|
|
prk = hmac.new(salt, spend_key, hashlib.sha256).digest()
|
|
|
|
# Step 2: Expand — derive the secp256k1 key
|
|
info = b"hns-sidechain-v1\x01"
|
|
secp256k1_privkey = hmac.new(prk, info, hashlib.sha256).digest()
|
|
|
|
# secp256k1 requires the key to be in range [1, n-1]
|
|
# n = FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
|
|
n = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
|
|
key_int = int.from_bytes(secp256k1_privkey, 'big')
|
|
if key_int == 0 or key_int >= n:
|
|
# Extremely unlikely but handle it — rehash
|
|
secp256k1_privkey = hmac.new(prk, b"hns-sidechain-v1\x02", hashlib.sha256).digest()
|
|
key_int = int.from_bytes(secp256k1_privkey, 'big') % (n - 1) + 1
|
|
secp256k1_privkey = key_int.to_bytes(32, 'big')
|
|
|
|
# Derive additional keys for future use
|
|
document_signing_key = hmac.new(prk, b"document-signing-v1\x01", hashlib.sha256).digest()
|
|
audit_key = hmac.new(prk, b"audit-trail-v1\x01", hashlib.sha256).digest()
|
|
|
|
return {
|
|
"source": {
|
|
"type": "ed25519",
|
|
"chain": "lethean-mainchain",
|
|
"spend_key": spend_key_hex,
|
|
},
|
|
"derived": {
|
|
"hns_sidechain": {
|
|
"type": "secp256k1",
|
|
"chain": "lethean-hns-sidechain",
|
|
"private_key": secp256k1_privkey.hex(),
|
|
"derivation": "HKDF(salt=lethean-hns-bridge, ikm=spend_key, info=hns-sidechain-v1)",
|
|
},
|
|
"document_signing": {
|
|
"type": "ed25519",
|
|
"purpose": "document hash timestamping",
|
|
"private_key": document_signing_key.hex(),
|
|
"derivation": "HKDF(salt=lethean-hns-bridge, ikm=spend_key, info=document-signing-v1)",
|
|
},
|
|
"audit": {
|
|
"type": "ed25519",
|
|
"purpose": "audit trail verification",
|
|
"private_key": audit_key.hex(),
|
|
"derivation": "HKDF(salt=lethean-hns-bridge, ikm=spend_key, info=audit-trail-v1)",
|
|
},
|
|
},
|
|
"derivation_version": "1.0",
|
|
"note": "All keys derived deterministically from the Lethean spend key. One seed, all chains and purposes.",
|
|
}
|
|
|
|
|
|
def get_spend_key_from_wallet(rpc_url: str) -> str:
|
|
"""Fetch the spend key from a running wallet RPC."""
|
|
payload = json.dumps({
|
|
"jsonrpc": "2.0",
|
|
"id": "0",
|
|
"method": "get_wallet_info",
|
|
"params": {}
|
|
}).encode()
|
|
|
|
req = urllib.request.Request(
|
|
rpc_url,
|
|
data=payload,
|
|
headers={"Content-Type": "application/json"}
|
|
)
|
|
|
|
try:
|
|
with urllib.request.urlopen(req, timeout=5) as resp:
|
|
data = json.loads(resp.read())
|
|
# The spend key isn't directly in get_wallet_info
|
|
# We'd need a custom RPC or the seed to derive it
|
|
# For prototype, use the address as a stand-in
|
|
print("Note: Full implementation needs 'get_spend_key' RPC or seed phrase input")
|
|
print(f"Wallet address: {data.get('result', {}).get('wi', {}).get('address', '?')}")
|
|
return None
|
|
except Exception as e:
|
|
print(f"Error connecting to wallet: {e}")
|
|
return None
|
|
|
|
|
|
def main():
|
|
if len(sys.argv) < 2:
|
|
print(__doc__)
|
|
print("\nExample with test key:")
|
|
test_key = "a" * 64 # 32 bytes of 0xAA
|
|
result = derive_sidechain_key(test_key)
|
|
print(json.dumps(result, indent=2))
|
|
return
|
|
|
|
if sys.argv[1] == "--from-wallet":
|
|
rpc_url = sys.argv[2] if len(sys.argv) > 2 else "http://127.0.0.1:46944/json_rpc"
|
|
spend_key = get_spend_key_from_wallet(rpc_url)
|
|
if not spend_key:
|
|
print("\nUsing test key for demonstration:")
|
|
spend_key = "7b9f1e2a3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7"
|
|
else:
|
|
spend_key = sys.argv[1]
|
|
|
|
result = derive_sidechain_key(spend_key)
|
|
print(json.dumps(result, indent=2))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|