Generate Ethereum Address

Ethereum

Overview:

Generate Ethereum address from private key using plain old Python3, Elliptic Curve multiplication from SageMath and of course Keccak hash function.

  import sha3
  def keccak(bin):
    h = sha3.keccak_256()
    h.update(bin);
    return h.hexdigest()

  p = 2**256 - 2**32 - 2**9 - 2**8 - 2**7 - 2**6 - 2**4 - 1
  F = FiniteField(p)
  E = EllipticCurve(F, [0, 7])
  G = E([0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798, 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8])
  k = 0xf8f8a2f43c8376ccb0871305060d7b27b0554d2cc72bccf41b2705608452f315
  P = k * G
  p = '%x' % P[0] + '%x' % P[1]
  hexdigest = keccak(bytes.fromhex(p))
  s_addr = hexdigest[-40:]
  print('standard address: ' + '0x' + s_addr)
  hexdigest = keccak(bytes(s_addr, 'UTF8'))
  checksum = hexdigest[:40]
  c_addr =''.join([p[0] if int(p[1], 16) < 8 else p[0].upper() for p in zip(s_addr, checksum)])
  print('checksum address: ' + '0x' + c_addr)
standard address: 0x001d3f1ef827552ae1114027bd3ecf1f086ba0f9
checksum address: 0x001d3F1ef827552Ae1114027BD3ECF1f086bA0F9

1. Hash function

Let's begin with our little helper function to calculate Keccak-256 hash:

  import sha3
  def keccak(bin):
    h = sha3.keccak_256()
    h.update(bin);
    return h.hexdigest()

2. Elliptic Curve

Then we have standard Secp256k1 Elliptic Curve's parameters, see generate Bitcoin address blog post for low-level implementation details.

  p = 2**256 - 2**32 - 2**9 - 2**8 - 2**7 - 2**6 - 2**4 - 1
  F = FiniteField(p)
  E = EllipticCurve(F, [0, 7])
  G = E([0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798, 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8])
  E
Elliptic Curve defined by y^2 = x^3 + 7 over Finite Field of size 115792089237316195423570985008687907853269984665640564039457584007908834671663

3. Private and public keys

Do scalar multiplication between private key k and generator G to find the public key P which is just another point on elliptic curve.

  k = 0xf8f8a2f43c8376ccb0871305060d7b27b0554d2cc72bccf41b2705608452f315
  P = k * G
  P
(49790390825249384486033144355916864607616083520101638681403973749255924539515 : 59574132161899900045862086493921015780032175291755807399284007721050341297360 : 1)

4. Standard address

Address generation is done in 4 steps:

  1. calculate the payload which is the concatenation of hex values of each x, y coordinate of our public key

  2. calculate Keccak256 hash of the payload

  3. take last 20 bytes (40 chars)

  4. concatenate 0x prefix

  p = '%x' % P[0] + '%x' % P[1]
  print('payload: ' + p)
  hexdigest = keccak(bytes.fromhex(p))
  s_addr = hexdigest[-40:]
  print('standard address: ' + '0x' + s_addr)
payload: 6e145ccef1033dea239875dd00dfb4fee6e3348b84985c92f103444683bae07b83b5c38e5e2b0c8529d7fa3f64d46daa1ece2d9ac14cab9477d042c84c32ccd0
standard address: 0x001d3f1ef827552ae1114027bd3ecf1f086ba0f9

5. Checksum address

Checksum address generation in 4 steps as well:

  1. calculate Keccak256 hash of standard address generated above (w/o the 0x prefix)

  2. take only first 20 bytes (40 chars)

  3. if the i-th nibble (character) in checksum is greater than 8 then upcase the i-th character in address, else leave unchanged, see more details in EIP-55

  4. concatenate 0x prefix again

  hexdigest = keccak(bytes(s_addr, 'UTF8'))
  checksum = hexdigest[:40]
  print('checksum: ' + checksum)
  print('address : ' + s_addr)
  c_addr =''.join([p[0] if int(p[1], 16) < 8 else p[0].upper() for p in zip(s_addr, checksum)])
  print('result  : ' + c_addr)
  print('checksum address: ' + '0x' + c_addr)
checksum: 23a69c1653e4ebbb619b0b2cb8a9bad49892a8b9
address : 001d3f1ef827552ae1114027bd3ecf1f086ba0f9
result  : 001d3F1ef827552Ae1114027BD3ECF1f086bA0F9
checksum address: 0x001d3F1ef827552Ae1114027BD3ECF1f086bA0F9

Note

  hexdigest = keccak(bytes.fromhex(p))
  hexdigest = keccak(bytes(s_addr, 'UTF8'))

It is imporant to understand the two lines above which might be confusing:

  1. the first calculates the hash of the hex value p

  2. the second calculates the hash of a plain text s_addr.

See the difference:

  bytes.fromhex('1c')
  bytes('1c', 'UTF8')
b'\x1c'
b'1c'
comments powered by Disqus