Let's implement a cryptocurrency
Let’s implement bitcoin in Go. We’ll first take a look at the bitcoin whitepaper.
The whitepaper
It’s funny how blockchain
is never mentioned. The paper only debate “Timestamp server with signed hash linking blocks”.Did the term was actually coined (got it ?) at the time ?
Anyway,
Proof-of-work
To generate a Proof-of-work, we need to find a hash with n
zeroes prefix. This is done by modifying the nonce
field randomly and regenerating the hash.
Node responsabilities
- Listen to broadcasted blocks
- Listen to broadcasted transactions
- Try to sign a new block until done or new block is received
- Works on first received chain but keeps all other in case it becomes longer
Implementation
- Generate public/private key
- Connect to network, retrieve block chain and validate it.
- Listen to transactions and build block
- Try to sign new block with proof-of-work
Block
- header
- nonce
- transactions root
Transactions are hashed in a Merkle tree
with only the root included in the block’s hash.
Transactions
Transactions can have multiple input, and at most two outputs: one for the payment, and one returning the change if any back to sender.
Crypto money theorical validity
Attack on a crypto money is akin to a binomial random walk. So the attacker potential progress is a Poisson random variable.
The bigger the network, the longer the chain, the (exponentially) better it is. Cool.
Actual implementation
OK so, we’ve seen that bitcoin is an electronic cash system based on
- a recursively signed chain of transaction blocks
- an NP-hard problem
- a P2P network
Let’s write a block
First, we need a Block. The block contains:
- A Header with
- the previous block’s hash
- a nonce
- the root hash of the transactions’s Merkle tree
- The transaction’s hash Merkle tree
We can verify the validity of the chain only using block headers.
Let’s go with a simple implementation:
type Header struct {
Prev [sha256.Size]byte
Nonce [sha256.Size]byte
Root [sha256.Size]byte
}
type Block struct {
header Header
}
Now we need to hash this block:
func (b Block) Encode() ([]byte, error) {
var buf bytes.Buffer
err := binary.Write(&buf, binary.LittleEndian, b)
if err != nil {
return nil, err
}
return buf.Bytes(), nil
}
func (b Block) Hash() ([sha256.Size]byte, error) {
bin, err := b.Encode()
if err != nil {
return sha256.Sum256(nil), err
}
return sha256.Sum256(bin), nil
}
Let’s write a Blockchain
Now that we have a Block
that we can hash, we can have any chain and check its validity:
func TestValidate(t *testing.T) {
bc := NewBlockchain()
ok := Validate(bc)
if !ok {
t.Fatalf("expected empty blockchain to be valid")
}
b := &block.Block{
Header: block.Header{
Prev: [sha256.Size]byte{},
Nonce: [sha256.Size]byte{26, 40, 18, 41, 246, 84, 101, 88, 229, 243, 242, 66, 210, 31, 237, 17, 78, 78, 6, 29, 111, 30, 182, 10, 187, 101, 182, 53, 188, 21, 217, 201},
Root: [sha256.Size]byte{26, 40, 18, 41, 246, 84, 101, 88, 229, 243, 242, 66, 210, 31, 237, 17, 78, 78, 6, 29, 111, 30, 182, 10, 187, 101, 182, 53, 188, 21, 217, 201},
},
}
bc.Append(b)
ok = Validate(bc)
if !ok {
t.Fatalf("expected 1 block blockchain to be valid")
}
prev, err := b.Hash()
if err != nil {
t.Fatalf("cannot hash block: %s", err)
}
b = &block.Block{
Header: block.Header{
Prev: prev,
Nonce: [sha256.Size]byte{26, 40, 18, 41, 246, 84, 101, 88, 229, 243, 242, 66, 210, 31, 237, 17, 78, 78, 6, 29, 111, 30, 182, 10, 187, 101, 182, 53, 188, 21, 217, 201},
Root: [sha256.Size]byte{26, 40, 18, 41, 246, 84, 101, 88, 229, 243, 242, 66, 210, 31, 237, 17, 78, 78, 6, 29, 111, 30, 182, 10, 187, 101, 182, 53, 188, 21, 217, 201},
},
}
bc.Append(b)
ok = Validate(bc)
if !ok {
t.Fatalf("expected 2 block blockchain to be valid")
}
bc.Append(b)
ok = Validate(bc)
if ok {
t.Fatalf("expected invalid previous block")
}
}
First block is always valid, subsequent blocks are valid only if Header.Prev
is equal to the previous block’s hash.
Let’s write an NP-hard problem
As we can see in the test output, we have a valid chain with:
dcd381637bd02a30da82c1264ffd7de62d3216a680419406c9299c35492bac10
222ef267bae092ad8e8056faf60d8152b234d2a00f3962e15344fd0390c7bf7e
It’s time to introduce the proof-of-work requirement, and use the Nonce
field. We want the first n
bits to be zeroes. The whitepaper specify that the proof-of-work to be a moving average of number of blocks per hour. Let’s ignore that for now and set a constant.
const (
ProofOfWorkDifficulty int = 9
)
func Validate(b *Block) (bool, error) {
h, err := b.Hash()
if err != nil {
return false, err
}
var c int
for _, byt := range h {
i := bits.LeadingZeros8(byt)
c += i
if i != 8 {
break
}
}
fmt.Printf("%x ---> c=%d\n", h, c)
if c < ProofOfWorkDifficulty {
return false, nil
}
return true, nil
}
func GenerateValideHash(b *Block) error {
buf := make([]byte, 512)
for {
_, err := rand.Read(buf)
if err != nil {
return err
}
b.Header.Nonce = sha256.Sum256(buf)
ok, err := Validate(b)
if err != nil {
return err
}
if ok {
return nil
}
}
}
Now we can write a test for both:
func TestValidity(t *testing.T) {
b := &Block{}
b.Header.Prev = _testHash()
b.Header.Nonce = _testHash()
b.Header.Root = _testHash()
ok, err := Validate(b)
if err != nil {
t.Fatalf("unexpected error validating block: %s", err)
}
if ok {
t.Fatalf("expected invalid proof of work")
}
err = GenerateValideHash(b)
if err != nil {
t.Fatalf("unexpected error generating proof of work")
}
ok, err = Validate(b)
if err != nil {
t.Fatalf("unexpected error validating block: %s", err)
}
if !ok {
t.Fatalf("expected valid proof of work")
}
}
Looks like it works !
=== RUN TestValidity
1a281229f6546558e5f3f242d21fed114e4e061d6f1eb60abb65b635bc15d9c9 ---> c=3
8579f5c8321f5c0a4360f4bfa61574a0c3082c97110eac176e440b11836da0bd ---> c=0
f0afaa2abf43b355a4347ed7754953d876b4b4295ab13271de7e828fae06c45d ---> c=0
b905314589ed4b2d3ed5c5be015871545638caaed7d1e79209e9355ecb5e3830 ---> c=0
67ee0dc837e0b0c17abb787ee4cf82b8edb4b79619376e8cfd73cd924421752a ---> c=1
4ef6b8dcbc9cee3fc16bbeeb9092c93f98a5cd3b2f5efddeb80f8dc9f69d5e38 ---> c=1
488bac0110d9b59b4bb04e137973ee9ddfbdc3c9a05c5d03741b68a4283116f1 ---> c=1
037c23e3a7ff3277dfb86bf1556bff17505e3b0a336dc9538f05a202616e4f40 ---> c=6
07bb80ab8f3ab965a76b8b03ad80b7fb4974ada2ede47565d507691235244aa7 ---> c=5
9c0f4d021ffa1f4e70fbcee8a9e9e8376982189e61fff943a6fd9bb389739d89 ---> c=0
474afea048e3a05cd2b14369b41c36bf9a2d829767fbe26d0c2f15219b3443eb ---> c=1
1a58866610c1890efef373a8f6cf1b0c1d995f71c938fa160bfdfff18bceb32f ---> c=3
65bf06d7c5cfda4c79dab576668be5fba2ca730f667fc646bc30e9c46119b49f ---> c=1
94337954c2fa8854397ae6f32e30d45c83c71721ce3db5cb853fde369bada532 ---> c=0
5a526c26d573df1613d272ff9584c0c8827601bdd3bd6c5be3ba145abb8a550c ---> c=1
b03a9ca341765cc67c356e578e4073deecf3b99fc4f08f7859060a068dcebbce ---> c=0
8dcfd22a19ce82a5e716708f42054f083118c194303c580ea45662eee223b7c3 ---> c=0
cce9b68027640dcd19477543dfc0a503066500ba80e008611405973183d9eb98 ---> c=0
40bcbbc083140375e0102ca1beb60d68aef7d068308e59cda12a77d3df376c67 ---> c=1
2c23ff0fbade52752dc716a9baff8ef4fe2e790ccfcd054d59ee17b64c1f2336 ---> c=2
717cb78bb041db8de292d704cf65c74fb038bc65fee45ac40b073fb00c435eeb ---> c=1
d00b2ba98043995c1e35cb89ea1e47a6b9171c56438debc0b1e9cf783f81e75b ---> c=0
a14baa86dbfba827cdbf25b24c702a1af54ca07ad9497380f65e2f9713f333c1 ---> c=0
d3c0c1103766d965b5e19c7f790fab62ddff6d88e119974f04f17b3c420abcb1 ---> c=0
898927e462f50c89f804c129b68a73c90e4a2490835dfd480cd52229875cbb9f ---> c=0
d9918bae3afc59bb84ab3d98ec529be96b3e0691b30c6542d684fa13fc85774e ---> c=0
7835b233def4e6cd74ed636698fe61c9e93fe874786c33dcc05a2ffd397c8260 ---> c=1
29eff18ad71940d6bb3ed324d834ce07d29e8c2d471c33ac6b3cb9dff09bbe67 ---> c=2
7f0e97a8af728e27974902703b1dfd8412255c14ac78afaf72cef5dc52340a7b ---> c=1
a7b5d830073a62c74a4c9a9ad143f0009f69e91b3f7a2b168e2a61492f8fdac7 ---> c=0
b1589944ee336ddd2e0e00de76917bcff65fd3260277365de181fa495fa0094d ---> c=0
dc4b69078d987c7abd101708c0c01a060f2a79561d76190c850ac5790081ef56 ---> c=0
e5ebb78ccb337da06f5d894eea5442db6080650518a9ec37e4fc4d8cf1274a25 ---> c=0
75ae82e3cc3e262f8679aa3806af94a58f804493f972447d8c1aa5d33da569fb ---> c=1
d1d135082a4ff829ab4e561be09af5ce05659e3902da941335860ec84b61bc27 ---> c=0
0ac0a7f83cb2c53a135d49693e4f5ed8e7157859c9dcd65896816355dc98a18e ---> c=4
cbe889823a5251920b561bfd19876d39ac1e9c360da5a414c761935663284ca0 ---> c=0
5d5a25403d333e65f456d0c3d15209a12d2773fee65f94f0c0e2895250e76b28 ---> c=1
dd9557e75e4a1120a538945b630e8325947459ef703d6d115f79902948e737de ---> c=0
fe2dff562eb0023fcee1d4a01c074f38531a3387048984bef6e36dbca64b656f ---> c=0
46439af8cb62780b004102ca3e5713ec899f943697971263280e4f5ebbff64e9 ---> c=1
b813378d14a50390b0726a29e61bdf54c5e2c276c63778118099bfeb88f6c4f5 ---> c=0
39e7dff6f950c321d2386ff1bb4374b71e5ed97b4b3d5a4fc44090f876d4952f ---> c=2
993643d263c1c0ea5d50eb04461fe4b7a66e84823b1d2067d65ed7c4a4ca35bf ---> c=0
93fca5636954c11475f4c3f933ffa54c681d8b8b1a54a679cd06b01b8be0bc6b ---> c=0
4b403b75a561ddcbec41454dcced8f1b703998df0b8d906136187d426dff092a ---> c=1
8962be3c068c58369bbda3d1ea6f1919a6622e7f7da8feb18fe989db894038e8 ---> c=0
6748dc6199f79374216bf96125157979bd5a2e977a016c6b15f2df889fcf45b8 ---> c=1
8c7f3faf089d9418ea53d4b8c11fa0bc873eb2a59f144e68c4beae2ba7132bcd ---> c=0
5e6e5d7179e1361e2058ce4638d4077c66b5ce47ab9589034bbc4b6da2889fc4 ---> c=1
7e37307075c527915d364a53e023a778aef1720d6e88e8cf467ae1129d8d44ba ---> c=1
217f7fb9be0620ecd9d214e3409c05f8a882374fabd0642553275ad4bfa500bc ---> c=2
20d48408235da691e2fae85161f24f5fde9750802c54beb133378afbee067131 ---> c=2
54cd91297c7f85151a56e04dc81ca75fa0df8e29b4267e9bb97013867cb41964 ---> c=1
a4249cee63c18ecdfb3f678aefeaad13249b1468e998f90b16b961535e184847 ---> c=0
0f9dc8faabf0f67a0482e9448b6158c0dbae3858eab70098acc1dceae9f72048 ---> c=4
205ebc8765cbe4e08350b03122b47198f4b06c1a6d6392e9cb9f3866389c7476 ---> c=2
6ec8b8a89ac7171b0f32d2152e47b79f5a1a678ceb351c01835bbc13a9e9f608 ---> c=1
ac803f22995d827fd84140d3fccba085e44ceac968b65d786ee84cdc325ed2f8 ---> c=0
79cda0e6697827652e912bdccc199e9dd5de42b30c8e7e0679b76c15d9281062 ---> c=1
4c0e2890af41b106689617393a0ea0fe3b196769daae3cf441c8323d7471ebfd ---> c=1
341483db0a9d43fe30f50001381f7b6ce4d1d220f16210eb798a4d6b13536a06 ---> c=2
3b0e72806f8271fbe66b2de9587b6816e33f1287cc1369b84aafefc9b45ec615 ---> c=2
e227a3c1e7744f23ffb8940ac160d48fcdfd2e33f6ac79e5ef3689da4211d6fc ---> c=0
b3cfb6d353fd34cbecc4f19cc9a3ed919c5774dc98872c73cd51373e47950820 ---> c=0
517d7336c512a61f1684f4bc4956593cac0563aa4c03136c0f7915fb09689e84 ---> c=1
e1b4de23fc7ebf1491bda18053b0cd1cf0a000da916294ac5704125c307552fb ---> c=0
09ab9873d18985d7386f6a8a093e6b1d9a684ed3ff6e16855e2bf43a54d97a42 ---> c=4
2de441fb8f9358f2a1209361808963075a0e704e3bf0396697a7ccfa6773e392 ---> c=2
61bed6ef78c4a7c77b9d5b41adb7aea91c77d865271a9f338df524810c90a19e ---> c=1
5f776787319ee1e368670f23cdf7a8b6fe277c0d55d8c1fe84a057979e6e62c9 ---> c=1
d23031f9d225c6e687e535f411d778afc35620800cf0a8ac5a954be6f6b1abf4 ---> c=0
1323580b48999e1dea85a1dfd8210486d80ddf9e45a45e70bd58e9dee892586d ---> c=3
a10703421f5cc52051049670bda209c1d4138667fc83710d5d5cbdc8c76e9a5f ---> c=0
50f948e107e284ba8bdc874542af2cf26f9abb0c0658ce691a966dcb2915434f ---> c=1
b9292bf63839e8d10ca33699ee206eb1ba7c1be63cbe89963844d82255643bc4 ---> c=0
eca44fb730a10830f61eb3d868e1404869f3db7c75cc1201345e06fb4613f0cc ---> c=0
b3b8d3186044f2dc176bc6256c86883450048bd2be107f4614fa1552d668131a ---> c=0
ed4e9ebd181fc3a753761e52997bc61ca620fa2bf084a13e9bd00921f7728de1 ---> c=0
31c81522a0c26d5975b467d6bf241d4ed924f3d98c52b99d11153cc16f0eddd1 ---> c=2
e8ae3cc241b7999bfbd04a02a5cee1ba07b26da07c2f9701692bfc35ce251e15 ---> c=0
88fdbb921f54e1510ab6d1b2f6c0c10f0383580908c90308400dd53cb7bbf914 ---> c=0
f0a003739bb27a8390ac7a9ee7520962b9315214eaa02671b7e0c2dad68f7f8a ---> c=0
71585212a79613a02e76918b6185fcf741f9064635ed2f22a16f6fe24b3dc837 ---> c=1
7fd9f9e7e2e6e2a9a798bd1a96205502e5f6761bfb2f5349fb15f27da757e3fd ---> c=1
b987d5234af47c5b2c615c9ddc65fa43e805ac1c68d390627a6c5766fd2dfba3 ---> c=0
02ec135fd9b25633310b6b0641d80afba991235a884c6a5024cd1f22e0c0fb4a ---> c=6
d6921cef7e8b32f67917b40564b2b3698c669047ca18df52b883aa75129a04a2 ---> c=0
ee27ea122d7ac4eb9d05ee8af8706ba036f8b4dc4778b0c36c384475b9533934 ---> c=0
6783da7822a4768b5fbcb49724825fa805878d930f4f7cb75d27e0ca27be9e43 ---> c=1
9f444df8c29d69aa71fc64b516f43a8e9e53ac94a956adece3984c7893ebc562 ---> c=0
4776cb716ab6d260c751f61150a7601608feee2c6924035b8fee90b258144932 ---> c=1
ce74d8df34844c6425aa63c39f997ee4f5c03198dad44865f79862346b1c80f2 ---> c=0
c963c55dc35eecb8ca6bd562a686b9e3fa44afa32166b803fa93c2a0160316ae ---> c=0
118ba6e159bb52eaaad89214f05ff38add7cabef8c436b04a2d3f7000d81bcdc ---> c=3
12a454cea68b11febfdad470576c87dce02a8b2d3d2b525ba5a7f2180a2f7bf0 ---> c=3
0afa0a9f5790f192c7e90e419163db02af680adf7e7332225a80a62cf7334b1f ---> c=4
bb1f3ead12be5ae759e3f4e05fad32dc05a39a6202cc4218203c0596c7777aea ---> c=0
b47e36856b3c4caf37b44f10efbc8a627e330fb7251c64b9d8f092d11ae9025f ---> c=0
28e6c7ca78bbbaf63226dddeafa9653c02a9b558458e4df6441a4828d1011980 ---> c=2
c12dd73eae744be0cf14b44075d3041a6c779fed1e5e8f9a6f01a49578b6148c ---> c=0
457b056bb7c1360c630c072611e9c49a1ce0da5208f916dff48a825bf04e665a ---> c=1
45089f5c7b818a530b0a19c499c93cf2818dde05b8eb7d995abf945a0866dbf9 ---> c=1
062a6346d020805767fe0a67f6dffbad6fa7630d57b41d33058a22c424104344 ---> c=5
6839224411a838ea5122556a1b085247baf49e6e5d7ce31f76838a1fc179c38b ---> c=1
123bbb3320e9774039e9243defa5cb7d6cd8dab2ea549e20284b45917197f78a ---> c=3
6f7b8349f3f509d8b16cc6a51fcc359953365b22999e616e0a1d8ec628daa798 ---> c=1
b2c68b197bd6544024d8838919ee65836474332246720411a3bec8ab43fc3074 ---> c=0
17eae552ee066e073af29477f2316f7a65008cf965327b99955c3ad795c77ca5 ---> c=3
1339aef785d2ff429c362bc9913f3be4962a7d6f703f03b87d0617ab044dcb70 ---> c=3
da6e8ce33da63107b82d6db71a0f7739811785e3d6606c5edbaac6eea685a97e ---> c=0
ca83aa6d4542c1f24f52f5e4fd108fbef74cbedea983738c13d39376a1a77aa2 ---> c=0
2b0088fc076743953b1efb8d1b73a89e439272ed1f10377da527f1ddeac481c7 ---> c=2
46f3728c022e3d091bac0233459d1c7614ec268c91b6a2035a00a60ba284de5c ---> c=1
389d025e40b0d587690e2418018aca333653cd0195c9cb17c8c1c5b59e0e06cc ---> c=2
7bbf6aa8534f5cb6a48e171cabefd36f2aeda61877ddfc4f2df8d3fe0c5ec196 ---> c=1
40fe7657008e0472d45667b6dfeaf64d7fa5695054c589d57fb2a75701ee3b40 ---> c=1
60732ec3e1e753f408d8fd075c46b1d79792668d9c5856873ce31b5b4b08dd96 ---> c=1
f6bec7ac292b90d530ea2140ede8a7213109962300ff79666721e39786b39ae5 ---> c=0
0e75b1070e6298826a980c21927d95604aeb18fcd2fbc9aea699ce60bb3fc936 ---> c=4
821184cfaa38702a47f3a3de9cc7537eb6d501baec5c616fefb868eafde6cb3a ---> c=0
58b301727b563760808ed12a1fd440af72d6c87f129bd75c5d0688a2cb294a97 ---> c=1
3e361e0cc0a5e93d854c34c244e547eaaa32369a1160bd2f7ee2139cbd8e75d6 ---> c=2
00275c3865542e66b1599f99bfc69f8afff3b086c9a4da3ac0315273f8493593 ---> c=10
--- PASS: TestValidity (0.01s)
PASS
ok github.com/proullon/bitcoin/block 0.011s
=== RUN TestValidate
--- PASS: TestValidate (0.00s)
PASS
The difficulty to find n
increases exponentially, for testing purpose, let’s stay at 9: 1 byte + 1 bit.
Signature
The whitepaper specifies Each owner transfers the coin to the next by digitally signing a hash of the previous transation and the public key of the next owner and adding these to the end of the coin. A payee can verifiy the signatures to verify the chain of ownership
.
The creator of the block signs the hash, to prove they’re the one doing the work and claiming the reward. We can use crypto/ecdsa
for this.
So, when creating a block, one need to
- sign the hash of the previous transaction
- include its own public key in the block
- do the proof-of-work
Let’s adapt the code:
sig, err := ecdsa.SignASN1(rand.Reader, privateKey, prev[:])
if err != nil {
t.Fatalf("cannot sign previous block's hash: %s", err)
}
t.Logf("Signature: %x", sig)
b.Header.Prev = [sha256.Size]byte(sig)
b.Owner, err = asn1.Marshal(privateKey.PublicKey)
if err != nil {
t.Fatalf("cannot DER encode public key: %s", err)
}
block_test.go:75:
Signature: 30450220411cb91d166b180fbe4c3b8033ae451a67f0d3d86d7911b9dcd6d5df4de30824022100d62cfb761fc0f53b08ca39d72b598b05975798e51bbd7d3986e5856be11bb0f9
block_test.go:80: cannot encode public key: asn1: structure error: unknown Go type: elliptic.Curve
Ah. There is no asn1
encoder for ecdsa.PublicKey
type. I’m learning the crypto/elliptic
.Curve
type represents a short-form Weierstrass curve. I’ll have to check that.
Anyway, the ecdsa
package exposes a function to convert to an ecdh.PublicKey
, and those keys “can be parsed with crypto/x509.ParsePKIXPublicKey
and encoded with crypto/x509.MarshalPKIXPublicKey
. For NIST curves, they then need to be converted with crypto/ecdsa.PublibKey.ECDH
after parsing.”
…Ok then, let’s do that.
x509.MarshalPKIXPublicKey
converts the public key to PKIX, ASN.1 DER form. Cool.
sig, err := ecdsa.SignASN1(rand.Reader, privateKey, prev[:])
if err != nil {
t.Fatalf("cannot sign previous block's hash: %s", err)
}
t.Logf("Signature: %x", sig)
b.Header.Prev = [sha256.Size]byte(sig)
ecdshPublicKey, err := privateKey.PublicKey.ECDH()
if err != nil {
t.Fatalf("cannot convert public key to ECDH: %s", err)
}
pubkey, err := x509.MarshalPKIXPublicKey(ecdshPublicKey)
if err != nil {
t.Fatalf("cannot encode public key to ASN1 DER: %s", err)
}
t.Logf("Pubkey: %x (len=%d)", pubkey, len(pubkey))
}
=== RUN TestSignature
block_test.go:75: Signature: 304502207155b0fbf29e14f3c0fce2a42c21196e712549a7b93e9318f6147fe73a701b2f022100b0e2a58a1d2aa2e86935b85e0b5f8a1d02e719c758b6b738e1fd175527490a3f
block_test.go:88: Pubkey: 3059301306072a8648ce3d020106082a8648ce3d0301070342000478fc7058e8f908bf78491a99cf0f4a34ac81f1ceb41c1d9c8424e46e8feb5320a6bf7881d14374b00dc4a9ed817cb30a97f274764165ecde1f22941ce4647587 (len=91)
--- PASS: TestSignature (0.00s)
FAIL
Looks like we got what we need. So far the Go standard library has everything we could wish for.
Sources:
- https://pkg.go.dev/crypto/elliptic#Curve
- https://yourbasic.org/golang/bitwise-operator-cheat-sheet/
- https://pkg.go.dev/crypto/ecdh#PublicKey
- https://pkg.go.dev/crypto/ecdsa@go1.21.6#PublicKey.ECDH
- https://pkg.go.dev/encoding/asn1
- https://en.wikipedia.org/wiki/ASN.1
P2P network
Now, we need to build an application able to communicate peer-to-peer with other nodes to share the blockchain.
We need each node to:
- load a private key
- connect to peers
- load a blockchain from disk
- share the blockchain with peer
- update blockchain on disk
- listen to transaction request
- listen to new blocks
- build new block
- share new block
We’ll also need a cli to:
- generate private key
- display information about the current blockchain
- create a transaction
Let’s deal with all the cli boileplate with cobra:
$ bitcoin-cli
CLI utility to interact with bitcoin-node network
Usage:
bitcoin-cli [command]
Available Commands:
completion Generate the autocompletion script for the specified shell
generate-key Generate an ECDSA key to be used on bitcoin network
help Help about any command
version Software version number
Flags:
-h, --help help for bitcoin-cli
Use "bitcoin-cli [command] --help" for more information about a command.
$ bitcoin-cli version
This is a naive implementation, do not use it.
Let’s use libp2p|libp2p for the network setup. libp2p provides a lot of building blocks for peer-to-peer applications.Like discovery and rendez-vous in place of etcd
, secret sharing in place of vault
or pub/sub in place of kafka
, Google pubsub
or rabbitmq
. It is a different paradigm, and the base of any true distributed application.
// Run initialise the node network with provided private key and disk working directory
func Run() error {
host, err := libp2p.New()
if err != nil {
return err
}
defer host.Close()
fmt.Println(host.Addrs())
// Exit properly on SIGTERM and SIGINT
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGTERM, syscall.SIGINT)
<-sigCh
return nil
}
Let’s add peer discovery with mDNS (doc):
// Peer discovery
mdnsService := mdns.NewMdnsService(host, discoveryNamespace, &discoveryNotifee{})
err = mdnsService.Start()
if err != nil {
return fmt.Errorf("cannot start mdnsService: %s", err)
}
When I run 3 nodes in my local network I get:
# proullon @ desktop in ~/work/src/github.com/proullon/bitcoin [11:10:24] C:1
$ bitcoin-node run
[/ip4/127.0.0.1/tcp/35981 /ip4/127.0.0.1/udp/47685/quic-v1/webtransport/certhash/uEiCRyP7Zw31I5hJgeqNe3DpuT_R6SrJyW5GdVo6hFa3Rxw/certhash/uEiAg-olgLZwFB1BIeVAo3MSc0rAh1tOVuSbSJhEnap_zxQ /ip4/127.0.0.1/udp/55469/quic-v1 /ip4/192.168.1.39/tcp/35981 /ip4/192.168.1.39/udp/47685/quic-v1/webtransport/certhash/uEiCRyP7Zw31I5hJgeqNe3DpuT_R6SrJyW5GdVo6hFa3Rxw/certhash/uEiAg-olgLZwFB1BIeVAo3MSc0rAh1tOVuSbSJhEnap_zxQ /ip4/192.168.1.39/udp/55469/quic-v1 /ip6/::1/tcp/40381 /ip6/::1/udp/57576/quic-v1/webtransport/certhash/uEiCRyP7Zw31I5hJgeqNe3DpuT_R6SrJyW5GdVo6hFa3Rxw/certhash/uEiAg-olgLZwFB1BIeVAo3MSc0rAh1tOVuSbSJhEnap_zxQ /ip6/::1/udp/58728/quic-v1 /ip6/2a01:cb08:8c43:3700:ef07:63fd:ab1b:9637/tcp/40381 /ip6/2a01:cb08:8c43:3700:ef07:63fd:ab1b:9637/udp/57576/quic-v1/webtransport/certhash/uEiCRyP7Zw31I5hJgeqNe3DpuT_R6SrJyW5GdVo6hFa3Rxw/certhash/uEiAg-olgLZwFB1BIeVAo3MSc0rAh1tOVuSbSJhEnap_zxQ /ip6/2a01:cb08:8c43:3700:ef07:63fd:ab1b:9637/udp/58728/quic-v1]
found peer {12D3KooWGydHBHZKQW7Bozfra8R4QGGpi9kcB8D6tQge7nBwcPAQ: [/ip6/::1/tcp/41573 /ip6/2a01:cb08:8c43:3700:8f90:55c:d91d:459a/tcp/41573 /ip6/2a01:cb08:8c43:3700:60f1:d983:8a38:24ea/tcp/41573 /ip6/2a01:cb08:8c43:3700:7cae:1b8b:bf99:1f65/tcp/41573 /ip6/2a01:cb08:8c43:3700:c4f1:42a3:c7dc:346/tcp/41573 /ip6/2a01:cb08:8c43:3700:5ba0:4bfa:2dc6:c535/tcp/41573 /ip6/::1/udp/47629/quic-v1 /ip6/2a01:cb08:8c43:3700:8f90:55c:d91d:459a/udp/47629/quic-v1 /ip6/2a01:cb08:8c43:3700:60f1:d983:8a38:24ea/udp/47629/quic-v1 /ip6/2a01:cb08:8c43:3700:7cae:1b8b:bf99:1f65/udp/47629/quic-v1 /ip6/2a01:cb08:8c43:3700:c4f1:42a3:c7dc:346/udp/47629/quic-v1 /ip6/2a01:cb08:8c43:3700:5ba0:4bfa:2dc6:c535/udp/47629/quic-v1 /ip6/::1/udp/59863/quic-v1/webtransport/certhash/uEiD1jH39lBy1llJoSyf8qfTvaCrvXpKLkwbj5iHJmofE-g/certhash/uEiBWVcFlYKYMWA31_0CmKHa_wiiNivP0mmmbfV9PUKKuwQ /ip6/2a01:cb08:8c43:3700:8f90:55c:d91d:459a/udp/59863/quic-v1/webtransport/certhash/uEiD1jH39lBy1llJoSyf8qfTvaCrvXpKLkwbj5iHJmofE-g/certhash/uEiBWVcFlYKYMWA31_0CmKHa_wiiNivP0mmmbfV9PUKKuwQ /ip6/2a01:cb08:8c43:3700:60f1:d983:8a38:24ea/udp/59863/quic-v1/webtransport/certhash/uEiD1jH39lBy1llJoSyf8qfTvaCrvXpKLkwbj5iHJmofE-g/certhash/uEiBWVcFlYKYMWA31_0CmKHa_wiiNivP0mmmbfV9PUKKuwQ /ip6/2a01:cb08:8c43:3700:7cae:1b8b:bf99:1f65/udp/59863/quic-v1/webtransport/certhash/uEiD1jH39lBy1llJoSyf8qfTvaCrvXpKLkwbj5iHJmofE-g/certhash/uEiBWVcFlYKYMWA31_0CmKHa_wiiNivP0mmmbfV9PUKKuwQ /ip6/2a01:cb08:8c43:3700:c4f1:42a3:c7dc:346/udp/59863/quic-v1/webtransport/certhash/uEiD1jH39lBy1llJoSyf8qfTvaCrvXpKLkwbj5iHJmofE-g/certhash/uEiBWVcFlYKYMWA31_0CmKHa_wiiNivP0mmmbfV9PUKKuwQ /ip6/2a01:cb08:8c43:3700:5ba0:4bfa:2dc6:c535/udp/59863/quic-v1/webtransport/certhash/uEiD1jH39lBy1llJoSyf8qfTvaCrvXpKLkwbj5iHJmofE-g/certhash/uEiBWVcFlYKYMWA31_0CmKHa_wiiNivP0mmmbfV9PUKKuwQ /ip4/127.0.0.1/tcp/36593 /ip4/192.168.1.24/tcp/36593 /ip4/127.0.0.1/udp/56926/quic-v1 /ip4/192.168.1.24/udp/56926/quic-v1 /ip4/127.0.0.1/udp/36373/quic-v1/webtransport/certhash/uEiD1jH39lBy1llJoSyf8qfTvaCrvXpKLkwbj5iHJmofE-g/certhash/uEiBWVcFlYKYMWA31_0CmKHa_wiiNivP0mmmbfV9PUKKuwQ /ip4/192.168.1.24/udp/36373/quic-v1/webtransport/certhash/uEiD1jH39lBy1llJoSyf8qfTvaCrvXpKLkwbj5iHJmofE-g/certhash/uEiBWVcFlYKYMWA31_0CmKHa_wiiNivP0mmmbfV9PUKKuwQ]}
found peer {12D3KooWNG7wymXvy9RtcpSJ6EGKUHmDxW4SGvxFTFqk15mfUKUY: [/ip4/127.0.0.1/tcp/37665 /ip4/192.168.1.28/tcp/37665 /ip4/172.17.0.1/tcp/37665 /ip4/127.0.0.1/udp/34720/quic-v1 /ip4/192.168.1.28/udp/34720/quic-v1 /ip4/172.17.0.1/udp/34720/quic-v1 /ip4/127.0.0.1/udp/53323/quic-v1/webtransport/certhash/uEiAOaKv5VmO5mWhQL-HPYV2-qNWoZVGi8Fz8wH2pZZfOfA/certhash/uEiB8H8oAYnxELY7FNlnazwZgpGx2WbKeFUhGdVZzyPPvKQ /ip4/192.168.1.28/udp/53323/quic-v1/webtransport/certhash/uEiAOaKv5VmO5mWhQL-HPYV2-qNWoZVGi8Fz8wH2pZZfOfA/certhash/uEiB8H8oAYnxELY7FNlnazwZgpGx2WbKeFUhGdVZzyPPvKQ /ip4/172.17.0.1/udp/53323/quic-v1/webtransport/certhash/uEiAOaKv5VmO5mWhQL-HPYV2-qNWoZVGi8Fz8wH2pZZfOfA/certhash/uEiB8H8oAYnxELY7FNlnazwZgpGx2WbKeFUhGdVZzyPPvKQ /ip6/::1/tcp/37753 /ip6/2a01:cb08:8c43:3700:8a37:f1d2:b7d6:e864/tcp/37753 /ip6/2a01:cb08:8c43:3700:dfca:9ba9:7888:5780/tcp/37753 /ip6/2a01:cb08:8c43:3700:129b:387f:8fc1:5589/tcp/37753 /ip6/::1/udp/35773/quic-v1 /ip6/2a01:cb08:8c43:3700:8a37:f1d2:b7d6:e864/udp/35773/quic-v1 /ip6/2a01:cb08:8c43:3700:dfca:9ba9:7888:5780/udp/35773/quic-v1 /ip6/2a01:cb08:8c43:3700:129b:387f:8fc1:5589/udp/35773/quic-v1 /ip6/::1/udp/52021/quic-v1/webtransport/certhash/uEiAOaKv5VmO5mWhQL-HPYV2-qNWoZVGi8Fz8wH2pZZfOfA/certhash/uEiB8H8oAYnxELY7FNlnazwZgpGx2WbKeFUhGdVZzyPPvKQ /ip6/2a01:cb08:8c43:3700:8a37:f1d2:b7d6:e864/udp/52021/quic-v1/webtransport/certhash/uEiAOaKv5VmO5mWhQL-HPYV2-qNWoZVGi8Fz8wH2pZZfOfA/certhash/uEiB8H8oAYnxELY7FNlnazwZgpGx2WbKeFUhGdVZzyPPvKQ /ip6/2a01:cb08:8c43:3700:dfca:9ba9:7888:5780/udp/52021/quic-v1/webtransport/certhash/uEiAOaKv5VmO5mWhQL-HPYV2-qNWoZVGi8Fz8wH2pZZfOfA/certhash/uEiB8H8oAYnxELY7FNlnazwZgpGx2WbKeFUhGdVZzyPPvKQ /ip6/2a01:cb08:8c43:3700:129b:387f:8fc1:5589/udp/52021/quic-v1/webtransport/certhash/uEiAOaKv5VmO5mWhQL-HPYV2-qNWoZVGi8Fz8wH2pZZfOfA/certhash/uEiB8H8oAYnxELY7FNlnazwZgpGx2WbKeFUhGdVZzyPPvKQ]}
Not easily readable, but in less than 1 second, all nodes have found each other and a callback has be fired.
So, it’s able to find it’s own peers in a local network, that’s great. Now they should actually communicate with each other.
We also want a pub/sub for new transaction and new block (doc):
// PubSub
ps, err := pubsub.NewGossipSub(ctx, host)
if err != nil {
return fmt.Errorf("cannot start pubsub service: %s", err)
}
// join topics for transactions and blocks
txTopic, err := ps.Join(TxTopic)
if err != nil {
return fmt.Errorf("cannot join transaction topic: %s", err)
}
txSub, err := txTopic.Subscribe()
if err != nil {
return fmt.Errorf("cannot subscribe to transaction topic: %s", err)
}
bTopic, err := ps.Join(BlockTopic)
if err != nil {
return fmt.Errorf("cannot join block topic: %s", err)
}
bSub, err := bTopic.Subscribe()
if err != nil {
return fmt.Errorf("cannot subscribe to block topic: %s", err)
}
Now we need to piece everything together in an easily testable finite state machine. Something like:
type Node struct {
privateKey *ecdsa.PrivateKey
bc *blockchain.Blockchain
tentativeBlock *blockchain.Block
}
func (n *Node) Start() {
}
func (n *Node) Stop() {
}
func (n *Node) run() {
}
func (n *Node) incomingTransaction(tx *blockchain.Transaction) {
}
func (n *Node) shareTransaction(tx *blockchain.Transaction) {
}
func (n *Node) incomingBlock(b *blockchain.Block) {
}
func (n *Node) startBuildingBlock() {
}
func (n *Node) stopBuildingBlock() {
}
func (n *Node) shareBlock() {
data, err := n.tentativeBlock.Encode()
if err != nil {
return err
}
err = n.bTopic.Publish(context.Background(), data)
if err != nil {
return err
}
}
Compatibility with official implementation
OK cool, but the whitepaper doesn’t specify the format of blocks, chain nor transactions. We’ll need to look at the official implementation to see what the protocols specs.
Let’s see what are the standards used in bitcoin:
SHA-256
for hashECDSA
withDER
encoding for signature- hash of ECDSA public key for addresses
source:
Building official implementation
Haha lib-boost. What a pain.
# proullon @ desktop in ~/work/src/github.com/bitcoin on git:master x [11:28:23] C:1
$ ./configure
...
checking whether miniUPnPc API version is supported... yes
checking for natpmp.h... yes
checking for initnatpmp in -lnatpmp... yes
checking for Boost headers >= 1.73.0 (107300)... configure: We could not detect the boost libraries (version 1.73.0 or higher). If you have a staged boost library (still not installed) please specify $BOOST_ROOT in your environment and do not give a PATH to --with-boost option. If you are sure you have boost installed, then check your version number looking in <boost/version.hpp>. See http://randspringer.de/boost for more documentation.
configure: error: Boost is not available!
Ok…
# proullon @ desktop in ~/work/src/github.com/bitcoin on git:master x [11:31:34]
$ sudo pacman -S boost
Let’s buiiiiiiild now.
- Insert 3 hours later meme *
Testing the official implementation
So there is a rust file checking the python implementation of the integration test of the C++ implementation. What a journey. I wanted to use them on my implementation but it’s in fact not trivial.
Running the official implementation
You need 500Gb and 10Gb per month…to run a full node.
OK then:
# fdisk /dev/sdc
# mount -t ext4 /dev/sdc3 /mnt/storage
# proullon @ desktop in ~/work/src/github.com/proullon/bitcoin [11:35:58] C:1
$ df -h
Filesystem Size Used Avail Use% Mounted on
/dev/sdc3 935G 2,1M 888G 1% /mnt/storage
Ready.
Next step ? Maybe look at Bitcoin book official protocol and adapt my implementation to be compatible with it.
The code is available on Github.