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])
k = 0xf8f8a2f43c8376ccb0871305060d7b27b0554d2cc72bccf41b2705608452f315
P = k * G
p = '%x' % P[0] + '%x' % P[1]
hexdigest = keccak(bytes.fromhex(p))
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)])

### 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])
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)

Address generation is done in 4 steps:

1. calculate the payload which is the concatenation of hex values of each xy 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]
hexdigest = keccak(bytes.fromhex(p))

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

checksum = hexdigest[:40]
print('checksum: ' + checksum)
c_addr =''.join([p[0] if int(p[1], 16) < 8 else p[0].upper() for p in zip(s_addr, checksum)])

And this is it, the entire process in a single image below.

icostan