Permalink
Cannot retrieve contributors at this time
Name already in use
A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
ctfblockchain/miner.go
Go to fileThis commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
363 lines (317 sloc)
10.3 KB
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import "fmt" | |
import "os" | |
import "runtime" | |
import "strings" | |
import "bytes" | |
import "strconv" | |
import "time" | |
import "io/ioutil" | |
import "math/rand" | |
import "crypto/sha256" | |
import "net/http" | |
import "encoding/json" | |
import "github.com/pkg/errors" | |
import (log "github.com/sirupsen/logrus") | |
var blockserver string = "https://rob.salmond.ca/mine" | |
//var blockserver string = "http://localhost:5000/mine" | |
type Status struct { | |
Username string `json:"username"` | |
ClientId string `json:"client_id"` | |
HashRate int `json:"hashrate"` | |
} | |
type Config struct { | |
Username string `json:"username"` | |
ClientId string `json:"client_id"` | |
MaxWorkers int `json:"max_workers"` | |
} | |
type Block struct { | |
Identifier string `json:"identifier"` | |
Nonce int `json:"nonce"` | |
Data string `json:"data"` | |
Previous_hash string `json:"previous_hash"` | |
Difficulty int `json:"difficulty"` | |
} | |
func (b *Block) hash() []byte { | |
h := sha256.New() | |
h.Write([]byte(b.Identifier)) | |
h.Write([]byte(strconv.Itoa(b.Nonce))) | |
h.Write([]byte(b.Data)) | |
h.Write([]byte(b.Previous_hash)) | |
return h.Sum(nil) | |
} | |
func (b *Block) set_blockid() { | |
// set a consistent block identifier by hashing what we have to start with | |
b.Identifier = strings.ToUpper(fmt.Sprintf("%x", b.hash())[0:32]) | |
} | |
func getChain() (blocks []Block, err error) { | |
// fetch and decode blockchain state json | |
log.Info("Getting blockchain from server.") | |
endpoint := fmt.Sprintf("%s/chain", blockserver) | |
resp, err := http.Get(endpoint) | |
if err != nil { | |
return nil, errors.Wrap(err, "Failed to connect to blockserver: " + blockserver) | |
} | |
defer resp.Body.Close() | |
body, err := ioutil.ReadAll(resp.Body) | |
if err != nil { | |
return nil, errors.Wrap(err, "Failed to retrieve blocks from blockserver: " + string(body)) | |
} | |
if err := json.Unmarshal(body, &blocks); err != nil { | |
return nil, errors.Wrap(err, "Failed to parse block JSON: " + string(body)) | |
} | |
return blocks, nil | |
} | |
func postChain(blocks_json []byte) error { | |
//TODO: some better checking on this response | |
log.Info("Posting update to blockserver.") | |
endpoint := fmt.Sprintf("%s/chain", blockserver) | |
resp, err := http.Post(endpoint, "application/json", bytes.NewBuffer(blocks_json)) | |
if err != nil { | |
return errors.Wrap(err, "Failed to post updated blockchain to blockserver.") | |
} | |
body, err := ioutil.ReadAll(resp.Body) | |
if err != nil { | |
return errors.Wrap(err, "Blockserver did not validate chain update." + string(body)) | |
} | |
log.Info(fmt.Sprintf("Blockserver responded: %s", string(body))) | |
return nil | |
} | |
func difficultyTarget(target int) string { | |
// difficulty is based on the number of 0's at the beginning of the hash | |
// so build a buffer of 0's to compare to the hash | |
var target_buffer bytes.Buffer | |
for i := 0; i < target; i++ { | |
target_buffer.WriteString("0") | |
} | |
return target_buffer.String() | |
} | |
func hashWorker(result *chan Block, end_hashrate *chan int, current_block *Block, worker_id int) { | |
hashed := make([]byte,0) | |
hash_count := 0 | |
hashes_per_sec := 0 | |
report_interval := 30 | |
random := rand.New(rand.NewSource(time.Now().UnixNano())) | |
difficulty_target := difficultyTarget(current_block.Difficulty) | |
start_time := time.Now().Unix() | |
// hash block with random nonce values until we hit a hash that has enough 0's | |
// at the beginning to satisfy the difficulty requirement | |
for { | |
current_block.Nonce = random.Int() | |
hashed = current_block.hash() | |
hashed_string := fmt.Sprintf("%x", hashed) | |
if hashed_string[0:current_block.Difficulty] == difficulty_target { | |
*result <- *current_block | |
} | |
hash_count++ | |
if start_time + int64(report_interval) < time.Now().Unix() { | |
hashes_per_sec = hash_count / report_interval | |
log.Info(fmt.Sprintf("Worker: %d hashes per second: %d", worker_id, hashes_per_sec)) | |
*end_hashrate <- hashes_per_sec | |
return | |
} | |
} | |
} | |
func mineChain(c *[]Block, hashrate_report *chan int, quit *chan bool, max_workers int) ([]Block, error) { | |
// mine a block on the chain until it is solved then return the chain for processing back to the server | |
var currentBlock Block | |
var currentBlock_num int | |
var previousBlock Block | |
for i, block := range *c { | |
if block.Nonce == 0 { | |
previousBlock = (*c)[i-1] | |
currentBlock_num = i | |
currentBlock = block | |
break | |
} | |
} | |
// stop mining if all blocks are solved | |
if currentBlock.Difficulty == 0 { | |
return nil, errors.New("No blocks found to solve.") | |
close(*quit) | |
} | |
log.Info(fmt.Sprintf("Working on block number %d at difficulty rating %d", currentBlock_num, currentBlock.Difficulty)) | |
currentBlock.set_blockid() | |
currentBlock.Previous_hash = fmt.Sprintf("%x", previousBlock.hash()) | |
//func hashWorker(result *chan Block, hashrate_report *chan int, current_block *Block) { | |
result := make(chan Block) | |
end_hashrate := make(chan int) | |
ended_count := 0 | |
total_hashrate := 0 | |
for { | |
// launch an async hashworker to try random nonces | |
for i := 0; i < max_workers; i++ { | |
log.Debug(fmt.Sprintf("Launching worker id: %d", i)) | |
go hashWorker(&result, &end_hashrate, ¤tBlock, i) | |
} | |
for j := 0; j < max_workers; j++ { | |
select { | |
// return if we mine a block so it can be sent to the blockserver and we'll fetch the next block | |
case retval := <-result: | |
(*c)[currentBlock_num] = retval | |
return *c, nil | |
// if all the hashworkers ended their report interval, continue and spawn new ones | |
case rate := <-end_hashrate: | |
ended_count++ | |
total_hashrate += rate | |
if ended_count == max_workers { | |
log.Info(fmt.Sprintf("Total hashes per second: %d", total_hashrate)) | |
*hashrate_report <- total_hashrate | |
ended_count = 0 | |
total_hashrate = 0 | |
return nil, nil | |
} | |
} | |
} | |
} | |
log.Info(fmt.Sprintf("Block: %+v\n", currentBlock)) | |
return nil, errors.New("Unable to solve this block!") | |
} | |
func getConfig(username string) (*Config, error) { | |
// either produce a new config file or read an existing one | |
config := new(Config) | |
config_filename := "miner_config.json" | |
config_default_username := "your username here" | |
if username != "" { | |
config_default_username = username | |
} | |
if _, err := os.Stat(config_filename); os.IsNotExist(err) { | |
// generate a new config file | |
config.Username = config_default_username | |
config.MaxWorkers = runtime.NumCPU() | |
random := rand.New(rand.NewSource(time.Now().UnixNano())) | |
config.ClientId = strings.ToUpper(fmt.Sprintf("%x", random.Int())) | |
config_data, _ := json.Marshal(config) | |
file, file_err := os.Create(config_filename) | |
if file_err != nil { | |
return nil, errors.Wrap(file_err, "Unable to create config file: " + config_filename) | |
} | |
defer file.Close() | |
_, write_err := file.Write(config_data) | |
if write_err != nil { | |
return nil, errors.Wrap(file_err, "Unable to write to config file: " + config_filename) | |
} | |
return nil, errors.New("No config file found, a new one has been created. Please configure your username and restart the miner.") | |
} else { | |
config_data, file_err := ioutil.ReadFile(config_filename) | |
if err != nil { | |
return nil, errors.Wrap(file_err, "Unable to read config file: " + config_filename) | |
} | |
if json_err := json.Unmarshal(config_data, &config); json_err != nil { | |
return nil, errors.Wrap(json_err, "Unable to parse config json.") | |
} | |
if config.Username == config_default_username { | |
return nil, errors.New("Please edit config file and add a username before restarting miner.") | |
} | |
return config, nil | |
} | |
return nil, nil | |
} | |
func postStatus(status_json []byte) error { | |
log.Info("Posting status to blockserver.") | |
endpoint := fmt.Sprintf("%s/status", blockserver) | |
resp, err := http.Post(endpoint, "application/json", bytes.NewBuffer(status_json)) | |
if err != nil { | |
return errors.Wrap(err, "Failed to post status to blockserver.") | |
} | |
body, err := ioutil.ReadAll(resp.Body) | |
if err != nil { | |
return errors.Wrap(err, "Blockserver did not accept status update." + string(body)) | |
} | |
log.Info(fmt.Sprintf("Blockserver responded: %s", string(body))) | |
return nil | |
} | |
func toil(hashrate_report *chan int, quit *chan bool, max_workers int) { | |
// slave away in the mines | |
for { | |
// fetch blocks | |
blockchain, err := getChain() | |
if err != nil { | |
log.Warn(fmt.Sprintf("Chain fetcher exited with message: %s", err)) | |
log.Info("Sleeping until retry.") | |
time.Sleep(10 * time.Second) | |
continue | |
} | |
// mine a block | |
newchain, err := mineChain(&blockchain, hashrate_report, quit, max_workers) | |
if err != nil { | |
log.Info(fmt.Sprintf("Miner exited with message: %s", err)) | |
log.Info("Exiting.") | |
return | |
} | |
if newchain != nil { | |
// json encode mined block | |
json_chain, err := json.Marshal(newchain); | |
if err != nil { | |
panic(err) | |
} | |
// post mined block to blockserver | |
if err := postChain(json_chain); err != nil { | |
panic(err) | |
} | |
} | |
} | |
} | |
func reportStatus(username string, client_id string, hashrate_report *chan int) { | |
// send a status report every time the miner writes current hashrate to the report channel | |
for { | |
status := Status{} | |
status.Username = username | |
status.ClientId = client_id | |
status.HashRate = <-*hashrate_report | |
status_data, _ := json.Marshal(status) | |
if err := postStatus(status_data); err != nil { | |
log.Warn(fmt.Sprintf("Error posting status to blockserver: %s", err)) | |
continue | |
} | |
} | |
} | |
func printUsage() { | |
fmt.Println("Usage:") | |
fmt.Printf("\t%s <username>", os.Args[1]) | |
fmt.Printf("\tGenerate a new config file using the specified username.") | |
fmt.Printf("\t%s", os.Args[1]) | |
fmt.Printf("\tStart mining using existing config file or generate a default config if none is present.") | |
} | |
func needHelp(param string) (n bool) { | |
switch param { | |
case "-h": | |
return true | |
case "--help": | |
return true | |
case "help": | |
return true | |
default: | |
return false | |
} | |
} | |
func main() { | |
var config *Config | |
var err error | |
// fiddle about with args | |
if len(os.Args) == 2 { | |
// display help and exit | |
if needHelp(os.Args[1]) { | |
printUsage() | |
return | |
// if arg doesnt look like help accept it as username for config | |
} else { | |
config, err = getConfig(os.Args[1]) | |
} | |
// just go ahead and load config then | |
} else { | |
config, err = getConfig("") | |
} | |
if err != nil { | |
fmt.Printf("%s\n", err) | |
fmt.Println("Exiting.") | |
return | |
} | |
log.Info(fmt.Sprintf("Detected %d available CPU cores.", runtime.NumCPU())) | |
log.Info(fmt.Sprintf("Launching with configuration=> username: %s, client_id: %s, max_workers: %d", config.Username, config.ClientId, config.MaxWorkers)) | |
hashrate_report := make(chan int) | |
quit := make(chan bool) | |
go toil(&hashrate_report, &quit, config.MaxWorkers) | |
go reportStatus(config.Username, config.ClientId, &hashrate_report) | |
select {} | |
} |