├── .env ├── go.mod ├── go.sum ├── README.md └── main.go /.env: -------------------------------------------------------------------------------- 1 | PORT=9000 -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module akhil-pos-blockchain 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/davecgh/go-spew v1.1.1 7 | github.com/joho/godotenv v1.4.0 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= 4 | github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Few days back, we implemented a simple blockchain on my youtube channel. 2 | 3 | But blockchains aren't in silos, they need consensus mechanisms for adding new blocks. 4 | 5 | I've added a proof of work example in my github, this one is a proof of stake example. 6 | 7 | For proof of stake, you need to give different amount of tokens to different peers. 8 | 9 | This means, you will create multiple peers by - 10 | 11 | 1. Starting the server for the blockchain by `go run main.go` 12 | 2. Opening multiple terminal instances and typing `nc localhost 9000` 13 | 3. Each of these terminals will be a new peer and you can see the proof of stake happening in action in the terminal 14 | 4. There will be one clear winner and the hash of that peer will be mentioned in the terminal -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "crypto/sha256" 6 | "encoding/hex" 7 | "encoding/json" 8 | "fmt" 9 | "io" 10 | "log" 11 | "math/rand" 12 | "net" 13 | "os" 14 | "strconv" 15 | "sync" 16 | "time" 17 | 18 | "github.com/davecgh/go-spew/spew" 19 | "github.com/joho/godotenv" 20 | ) 21 | 22 | // Block represents each 'item' in the blockchain 23 | type Block struct { 24 | Index int 25 | Timestamp string 26 | BPM int 27 | Hash string 28 | PrevHash string 29 | Validator string 30 | } 31 | 32 | // Blockchain is a series of validated Blocks 33 | var Blockchain []Block 34 | var tempBlocks []Block 35 | 36 | // candidateBlocks handles incoming blocks for validation 37 | var candidateBlocks = make(chan Block) 38 | 39 | // announcements broadcasts winning validator to all nodes 40 | var announcements = make(chan string) 41 | 42 | var mutex = &sync.Mutex{} 43 | 44 | // validators keeps track of open validators and balances 45 | var validators = make(map[string]int) 46 | 47 | func main() { 48 | err := godotenv.Load() 49 | if err != nil { 50 | log.Fatal(err) 51 | } 52 | 53 | // create genesis block 54 | t := time.Now() 55 | genesisBlock := Block{} 56 | genesisBlock = Block{0, t.String(), 0, calculateBlockHash(genesisBlock), "", ""} 57 | spew.Dump(genesisBlock) 58 | Blockchain = append(Blockchain, genesisBlock) 59 | 60 | tcpPort := os.Getenv("PORT") 61 | 62 | // start TCP and serve TCP server 63 | server, err := net.Listen("tcp", ":"+tcpPort) 64 | if err != nil { 65 | log.Fatal(err) 66 | } 67 | log.Println("TCP Server Listening on port :", tcpPort) 68 | defer server.Close() 69 | 70 | go func() { 71 | for candidate := range candidateBlocks { 72 | mutex.Lock() 73 | tempBlocks = append(tempBlocks, candidate) 74 | mutex.Unlock() 75 | } 76 | }() 77 | 78 | go func() { 79 | for { 80 | pickWinner() 81 | } 82 | }() 83 | 84 | for { 85 | conn, err := server.Accept() 86 | if err != nil { 87 | log.Fatal(err) 88 | } 89 | go handleConn(conn) 90 | } 91 | } 92 | 93 | // pickWinner creates a lottery pool of validators and chooses the validator who gets to forge a block to the blockchain 94 | // by random selecting from the pool, weighted by amount of tokens staked 95 | func pickWinner() { 96 | time.Sleep(30 * time.Second) 97 | mutex.Lock() 98 | temp := tempBlocks 99 | mutex.Unlock() 100 | 101 | lotteryPool := []string{} 102 | if len(temp) > 0 { 103 | 104 | // slightly modified traditional proof of stake algorithm 105 | // from all validators who submitted a block, weight them by the number of staked tokens 106 | // in traditional proof of stake, validators can participate without submitting a block to be forged 107 | OUTER: 108 | for _, block := range temp { 109 | // if already in lottery pool, skip 110 | for _, node := range lotteryPool { 111 | if block.Validator == node { 112 | continue OUTER 113 | } 114 | } 115 | 116 | // lock list of validators to prevent data race 117 | mutex.Lock() 118 | setValidators := validators 119 | mutex.Unlock() 120 | 121 | k, ok := setValidators[block.Validator] 122 | if ok { 123 | for i := 0; i < k; i++ { 124 | lotteryPool = append(lotteryPool, block.Validator) 125 | } 126 | } 127 | } 128 | 129 | // randomly pick winner from lottery pool 130 | s := rand.NewSource(time.Now().Unix()) 131 | r := rand.New(s) 132 | lotteryWinner := lotteryPool[r.Intn(len(lotteryPool))] 133 | 134 | // add block of winner to blockchain and let all the other nodes know 135 | for _, block := range temp { 136 | if block.Validator == lotteryWinner { 137 | mutex.Lock() 138 | Blockchain = append(Blockchain, block) 139 | mutex.Unlock() 140 | for _ = range validators { 141 | announcements <- "\nwinning validator: " + lotteryWinner + "\n" 142 | } 143 | break 144 | } 145 | } 146 | } 147 | 148 | mutex.Lock() 149 | tempBlocks = []Block{} 150 | mutex.Unlock() 151 | } 152 | 153 | func handleConn(conn net.Conn) { 154 | defer conn.Close() 155 | 156 | go func() { 157 | for { 158 | msg := <-announcements 159 | io.WriteString(conn, msg) 160 | } 161 | }() 162 | // validator address 163 | var address string 164 | 165 | // allow user to allocate number of tokens to stake 166 | // the greater the number of tokens, the greater chance to forging a new block 167 | io.WriteString(conn, "Enter token balance:") 168 | scanBalance := bufio.NewScanner(conn) 169 | for scanBalance.Scan() { 170 | balance, err := strconv.Atoi(scanBalance.Text()) 171 | if err != nil { 172 | log.Printf("%v not a number: %v", scanBalance.Text(), err) 173 | return 174 | } 175 | t := time.Now() 176 | address = calculateHash(t.String()) 177 | validators[address] = balance 178 | fmt.Println(validators) 179 | break 180 | } 181 | 182 | io.WriteString(conn, "\nEnter a new BPM:") 183 | 184 | scanBPM := bufio.NewScanner(conn) 185 | 186 | go func() { 187 | for { 188 | // take in BPM from stdin and add it to blockchain after conducting necessary validation 189 | for scanBPM.Scan() { 190 | bpm, err := strconv.Atoi(scanBPM.Text()) 191 | // if malicious party tries to mutate the chain with a bad input, delete them as a validator and they lose their staked tokens 192 | if err != nil { 193 | log.Printf("%v not a number: %v", scanBPM.Text(), err) 194 | delete(validators, address) 195 | conn.Close() 196 | } 197 | 198 | mutex.Lock() 199 | oldLastIndex := Blockchain[len(Blockchain)-1] 200 | mutex.Unlock() 201 | 202 | // create newBlock for consideration to be forged 203 | newBlock, err := generateBlock(oldLastIndex, bpm, address) 204 | if err != nil { 205 | log.Println(err) 206 | continue 207 | } 208 | if isBlockValid(newBlock, oldLastIndex) { 209 | candidateBlocks <- newBlock 210 | } 211 | io.WriteString(conn, "\nEnter a new BPM:") 212 | } 213 | } 214 | }() 215 | 216 | // simulate receiving broadcast 217 | for { 218 | time.Sleep(time.Minute) 219 | mutex.Lock() 220 | output, err := json.Marshal(Blockchain) 221 | mutex.Unlock() 222 | if err != nil { 223 | log.Fatal(err) 224 | } 225 | io.WriteString(conn, string(output)+"\n") 226 | } 227 | 228 | } 229 | 230 | // isBlockValid makes sure block is valid by checking index 231 | // and comparing the hash of the previous block 232 | func isBlockValid(newBlock, oldBlock Block) bool { 233 | if oldBlock.Index+1 != newBlock.Index { 234 | return false 235 | } 236 | 237 | if oldBlock.Hash != newBlock.PrevHash { 238 | return false 239 | } 240 | 241 | if calculateBlockHash(newBlock) != newBlock.Hash { 242 | return false 243 | } 244 | 245 | return true 246 | } 247 | 248 | // SHA256 hasing 249 | // calculateHash is a simple SHA256 hashing function 250 | func calculateHash(s string) string { 251 | h := sha256.New() 252 | h.Write([]byte(s)) 253 | hashed := h.Sum(nil) 254 | return hex.EncodeToString(hashed) 255 | } 256 | 257 | //calculateBlockHash returns the hash of all block information 258 | func calculateBlockHash(block Block) string { 259 | record := string(block.Index) + block.Timestamp + string(block.BPM) + block.PrevHash 260 | return calculateHash(record) 261 | } 262 | 263 | // generateBlock creates a new block using previous block's hash 264 | func generateBlock(oldBlock Block, BPM int, address string) (Block, error) { 265 | 266 | var newBlock Block 267 | 268 | t := time.Now() 269 | 270 | newBlock.Index = oldBlock.Index + 1 271 | newBlock.Timestamp = t.String() 272 | newBlock.BPM = BPM 273 | newBlock.PrevHash = oldBlock.Hash 274 | newBlock.Hash = calculateBlockHash(newBlock) 275 | newBlock.Validator = address 276 | 277 | return newBlock, nil 278 | } 279 | --------------------------------------------------------------------------------