├── README.md ├── nebulous-configs ├── README.md ├── testing-stub.json └── basic-renter-host.json ├── .travis.yml ├── .gitignore ├── ant ├── jobrunner_test.go ├── job_wallet_littlesupplier.go ├── siad_test.go ├── job_gateway.go ├── job_wallet_bigspender.go ├── job_miner.go ├── jobrunner.go ├── job_balancemaintainer.go ├── ant_test.go ├── job_host.go ├── siad.go ├── ant.go └── job_renter.go ├── Makefile ├── LICENSE └── sia-antfarm ├── main.go ├── antfarm_test.go ├── ant_test.go ├── antfarm.go └── ant.go /README.md: -------------------------------------------------------------------------------- 1 | # sia-antfarm 2 | 3 | The Sia-Ant-Farm repository has moved to [GitLab](https://gitlab.com/NebulousLabs/Sia-Ant-Farm/). 4 | -------------------------------------------------------------------------------- /nebulous-configs/README.md: -------------------------------------------------------------------------------- 1 | The nebulous configs are configurations of the Sia-Ant-Farm that get run on 2 | release candidates before greenlighting them for distribution. 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | branches: 4 | only: 5 | - master 6 | 7 | notifications: 8 | email: false 9 | 10 | os: 11 | - linux 12 | 13 | go: 14 | - 1.8 15 | 16 | install: 17 | make dependencies 18 | 19 | script: 20 | make test 21 | 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | -------------------------------------------------------------------------------- /nebulous-configs/testing-stub.json: -------------------------------------------------------------------------------- 1 | { 2 | "antconfigs": 3 | [ 4 | { 5 | "APIAddr": "localhost:9980", 6 | "jobs": [ 7 | "gateway", 8 | "miner" 9 | ] 10 | }, 11 | { 12 | "jobs": [ 13 | "host" 14 | ], 15 | "desiredcurrency": 100000 16 | }, 17 | { 18 | "jobs": [ 19 | "host" 20 | ], 21 | "desiredcurrency": 100000 22 | }, 23 | { 24 | "jobs": [ 25 | "host" 26 | ], 27 | "desiredcurrency": 100000 28 | } 29 | ], 30 | "autoconnect": true 31 | } 32 | -------------------------------------------------------------------------------- /ant/jobrunner_test.go: -------------------------------------------------------------------------------- 1 | package ant 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "testing" 7 | ) 8 | 9 | func TestNewJobRunner(t *testing.T) { 10 | datadir, err := ioutil.TempDir("", "testing-data") 11 | if err != nil { 12 | t.Fatal(err) 13 | } 14 | defer os.RemoveAll(datadir) 15 | siad, err := newSiad("siad", datadir, "localhost:31337", "localhost:31338", "localhost:31339") 16 | if err != nil { 17 | t.Fatal(err) 18 | } 19 | defer stopSiad("localhost:31337", siad.Process) 20 | 21 | j, err := newJobRunner("localhost:31337", "", datadir) 22 | if err != nil { 23 | t.Fatal(err) 24 | } 25 | defer j.Stop() 26 | } 27 | -------------------------------------------------------------------------------- /nebulous-configs/basic-renter-host.json: -------------------------------------------------------------------------------- 1 | { 2 | "antconfigs": 3 | [ 4 | { 5 | "jobs": [ 6 | "gateway", 7 | "miner" 8 | ] 9 | }, 10 | { 11 | "Name": "host1", 12 | "jobs": [ 13 | "host" 14 | ], 15 | "desiredcurrency": 100000 16 | }, 17 | { 18 | "Name": "host2", 19 | "jobs": [ 20 | "host" 21 | ], 22 | "desiredcurrency": 100000 23 | }, 24 | { 25 | "Name": "host3", 26 | "jobs": [ 27 | "host" 28 | ], 29 | "desiredcurrency": 100000 30 | }, 31 | { 32 | "Name": "renter", 33 | "jobs": [ 34 | "renter" 35 | ], 36 | "desiredcurrency": 100000 37 | } 38 | ], 39 | "autoconnect": true 40 | } 41 | -------------------------------------------------------------------------------- /ant/job_wallet_littlesupplier.go: -------------------------------------------------------------------------------- 1 | package ant 2 | 3 | import ( 4 | "log" 5 | "time" 6 | 7 | "github.com/NebulousLabs/Sia/types" 8 | ) 9 | 10 | var ( 11 | sendInterval = time.Second * 2 12 | sendAmount = types.NewCurrency64(1000).Mul(types.SiacoinPrecision) 13 | ) 14 | 15 | func (j *jobRunner) littleSupplier(sendAddress types.UnlockHash) { 16 | j.tg.Add() 17 | defer j.tg.Done() 18 | 19 | for { 20 | select { 21 | case <-j.tg.StopChan(): 22 | return 23 | case <-time.After(sendInterval): 24 | } 25 | 26 | walletGet, err := j.client.WalletGet() 27 | if err != nil { 28 | log.Printf("[%v jobSpender ERROR]: %v\n", j.siaDirectory, err) 29 | return 30 | } 31 | 32 | if walletGet.ConfirmedSiacoinBalance.Cmp(sendAmount) < 0 { 33 | continue 34 | } 35 | 36 | _, err = j.client.WalletSiacoinsPost(sendAmount, sendAddress) 37 | if err != nil { 38 | log.Printf("[%v jobSupplier ERROR]: %v\n", j.siaDirectory, err) 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /ant/siad_test.go: -------------------------------------------------------------------------------- 1 | package ant 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "testing" 7 | 8 | "github.com/NebulousLabs/Sia/node/api/client" 9 | ) 10 | 11 | // TestNewSiad tests that NewSiad creates a reachable Sia API 12 | func TestNewSiad(t *testing.T) { 13 | if testing.Short() { 14 | t.SkipNow() 15 | } 16 | 17 | datadir, err := ioutil.TempDir("", "sia-testing") 18 | if err != nil { 19 | t.Fatal(err) 20 | } 21 | defer os.RemoveAll(datadir) 22 | 23 | siad, err := newSiad("siad", datadir, "localhost:9990", "localhost:0", "localhost:0") 24 | if err != nil { 25 | t.Error(err) 26 | return 27 | } 28 | defer siad.Process.Kill() 29 | 30 | c := client.New("localhost:9990") 31 | if _, err := c.ConsensusGet(); err != nil { 32 | t.Error(err) 33 | } 34 | siad.Process.Kill() 35 | 36 | // verify that NewSiad returns an error given invalid args 37 | _, err = newSiad("siad", datadir, "this_is_an_invalid_addres:1000000", "localhost:0", "localhost:0") 38 | if err == nil { 39 | t.Fatal("expected newsiad to return an error with invalid args") 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: install 2 | 3 | dependencies: 4 | go get -u github.com/NebulousLabs/Sia/... 5 | go install -tags='dev' github.com/NebulousLabs/Sia/cmd/siad 6 | go install -race std 7 | go get -u golang.org/x/lint/golint 8 | 9 | pkgs = ./sia-antfarm ./ant 10 | 11 | fmt: 12 | gofmt -s -l -w $(pkgs) 13 | 14 | vet: 15 | go vet $(pkgs) 16 | 17 | # install builds and installs binaries. 18 | install: 19 | go install $(pkgs) 20 | 21 | test: fmt vet install 22 | go test -timeout=1200s -race -v ./ant 23 | go test -timeout=1200s -race -v ./sia-antfarm 24 | 25 | lint: 26 | @for package in $(pkgs); do \ 27 | golint -min_confidence=1.0 $$package \ 28 | && test -z $$(golint -min_confidence=1.0 $$package) ; \ 29 | done 30 | 31 | clean: 32 | rm -rf cover 33 | 34 | cover: clean 35 | mkdir -p cover/ 36 | @for package in $(pkgs); do \ 37 | go test -covermode=atomic -coverprofile=cover/$$package.out ./$$package \ 38 | && go tool cover -html=cover/$$package.out -o=cover/$$package.html \ 39 | && rm cover/$$package.out ; \ 40 | done 41 | 42 | .PHONY: all dependencies pkgs fmt vet install test lint clean cover 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Nebulous 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /ant/job_gateway.go: -------------------------------------------------------------------------------- 1 | package ant 2 | 3 | import ( 4 | "log" 5 | "time" 6 | ) 7 | 8 | // gatewayConnectability will print an error to the log if the node has zero 9 | // peers at any time. 10 | func (j *jobRunner) gatewayConnectability() { 11 | j.tg.Add() 12 | defer j.tg.Done() 13 | 14 | // Initially wait a while to give the other ants some time to spin up. 15 | select { 16 | case <-j.tg.StopChan(): 17 | return 18 | case <-time.After(time.Minute): 19 | } 20 | 21 | for { 22 | // Wait 30 seconds between iterations. 23 | select { 24 | case <-j.tg.StopChan(): 25 | return 26 | case <-time.After(time.Second * 30): 27 | } 28 | 29 | // Count the number of peers that the gateway has. An error is reported 30 | // for less than two peers because the gateway is likely connected to 31 | // itself. 32 | gatewayInfo, err := j.client.GatewayGet() 33 | if err != nil { 34 | log.Printf("[ERROR] [gateway] [%v] error when calling /gateway: %v\n", j.siaDirectory, err) 35 | } 36 | if len(gatewayInfo.Peers) < 2 { 37 | log.Printf("[ERROR] [gateway] [%v] ant has less than two peers: %v\n", j.siaDirectory, gatewayInfo.Peers) 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /ant/job_wallet_bigspender.go: -------------------------------------------------------------------------------- 1 | package ant 2 | 3 | import ( 4 | "log" 5 | "time" 6 | 7 | "github.com/NebulousLabs/Sia/types" 8 | ) 9 | 10 | var ( 11 | spendInterval = time.Second * 30 12 | spendThreshold = types.NewCurrency64(5e4).Mul(types.SiacoinPrecision) 13 | ) 14 | 15 | func (j *jobRunner) bigSpender() { 16 | j.tg.Add() 17 | defer j.tg.Done() 18 | 19 | for { 20 | select { 21 | case <-j.tg.StopChan(): 22 | return 23 | case <-time.After(spendInterval): 24 | } 25 | 26 | walletGet, err := j.client.WalletGet() 27 | if err != nil { 28 | log.Printf("[%v jobSpender ERROR]: %v\n", j.siaDirectory, err) 29 | return 30 | } 31 | 32 | if walletGet.ConfirmedSiacoinBalance.Cmp(spendThreshold) < 0 { 33 | continue 34 | } 35 | 36 | log.Printf("[%v jobSpender INFO]: sending a large transaction\n", j.siaDirectory) 37 | 38 | voidaddress := types.UnlockHash{} 39 | _, err = j.client.WalletSiacoinsPost(spendThreshold, voidaddress) 40 | if err != nil { 41 | log.Printf("[%v jobSpender ERROR]: %v\n", j.siaDirectory, err) 42 | continue 43 | } 44 | 45 | log.Printf("[%v jobSpender INFO]: large transaction send successful\n", j.siaDirectory) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /sia-antfarm/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "flag" 6 | "fmt" 7 | "os" 8 | "os/signal" 9 | ) 10 | 11 | func main() { 12 | configPath := flag.String("config", "config.json", "path to the sia-antfarm configuration file") 13 | flag.Parse() 14 | 15 | sigchan := make(chan os.Signal, 1) 16 | signal.Notify(sigchan, os.Interrupt) 17 | 18 | // Read and decode the sia-antfarm configuration file. 19 | var antfarmConfig AntfarmConfig 20 | f, err := os.Open(*configPath) 21 | if err != nil { 22 | fmt.Fprintf(os.Stderr, "error opening %v: %v\n", *configPath, err) 23 | os.Exit(1) 24 | } 25 | 26 | if err = json.NewDecoder(f).Decode(&antfarmConfig); err != nil { 27 | fmt.Fprintf(os.Stderr, "error decoding %v: %v\n", *configPath, err) 28 | os.Exit(1) 29 | } 30 | f.Close() 31 | 32 | farm, err := createAntfarm(antfarmConfig) 33 | if err != nil { 34 | fmt.Fprintf(os.Stderr, "error creating antfarm: %v\n", err) 35 | os.Exit(1) 36 | } 37 | defer farm.Close() 38 | go farm.ServeAPI() 39 | go farm.permanentSyncMonitor() 40 | 41 | fmt.Printf("Finished. Running sia-antfarm with %v ants.\n", len(antfarmConfig.AntConfigs)) 42 | <-sigchan 43 | fmt.Println("Caught quit signal, quitting...") 44 | } 45 | -------------------------------------------------------------------------------- /ant/job_miner.go: -------------------------------------------------------------------------------- 1 | package ant 2 | 3 | import ( 4 | "log" 5 | "time" 6 | ) 7 | 8 | // blockMining indefinitely mines blocks. If more than 100 9 | // seconds passes before the wallet has received some amount of currency, this 10 | // job will print an error. 11 | func (j *jobRunner) blockMining() { 12 | j.tg.Add() 13 | defer j.tg.Done() 14 | 15 | err := j.client.MinerStartGet() 16 | if err != nil { 17 | log.Printf("[%v blockMining ERROR]: %v\n", j.siaDirectory, err) 18 | return 19 | } 20 | 21 | walletInfo, err := j.client.WalletGet() 22 | if err != nil { 23 | log.Printf("[%v blockMining ERROR]: %v\n", j.siaDirectory, err) 24 | return 25 | } 26 | lastBalance := walletInfo.ConfirmedSiacoinBalance 27 | 28 | // Every 100 seconds, verify that the balance has increased. 29 | for { 30 | select { 31 | case <-j.tg.StopChan(): 32 | return 33 | case <-time.After(time.Second * 100): 34 | } 35 | 36 | walletInfo, err = j.client.WalletGet() 37 | if err != nil { 38 | log.Printf("[%v blockMining ERROR]: %v\n", j.siaDirectory, err) 39 | } 40 | if walletInfo.ConfirmedSiacoinBalance.Cmp(lastBalance) > 0 { 41 | log.Printf("[%v SUCCESS] blockMining job succeeded", j.siaDirectory) 42 | lastBalance = walletInfo.ConfirmedSiacoinBalance 43 | } else { 44 | log.Printf("[%v blockMining ERROR]: it took too long to receive new funds in miner job\n", j.siaDirectory) 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /ant/jobrunner.go: -------------------------------------------------------------------------------- 1 | package ant 2 | 3 | import ( 4 | "github.com/NebulousLabs/Sia/node/api/client" 5 | "github.com/NebulousLabs/Sia/sync" 6 | ) 7 | 8 | // A jobRunner is used to start up jobs on the running Sia node. 9 | type jobRunner struct { 10 | client *client.Client 11 | walletPassword string 12 | siaDirectory string 13 | tg sync.ThreadGroup 14 | } 15 | 16 | // newJobRunner creates a new job runner, using the provided api address, 17 | // authentication password, and sia directory. It expects the connected api to 18 | // be newly initialized, and initializes a new wallet, for usage in the jobs. 19 | // siadirectory is used in logging to identify the job runner. 20 | func newJobRunner(apiaddr string, authpassword string, siadirectory string) (*jobRunner, error) { 21 | client := client.New(apiaddr) 22 | client.Password = authpassword 23 | jr := &jobRunner{ 24 | client: client, 25 | siaDirectory: siadirectory, 26 | } 27 | walletParams, err := jr.client.WalletInitPost("", false) 28 | if err != nil { 29 | return nil, err 30 | } 31 | jr.walletPassword = walletParams.PrimarySeed 32 | 33 | err = jr.client.WalletUnlockPost(jr.walletPassword) 34 | if err != nil { 35 | return nil, err 36 | } 37 | 38 | return jr, nil 39 | } 40 | 41 | // Stop signals all running jobs to stop and blocks until the jobs have 42 | // finished stopping. 43 | func (j *jobRunner) Stop() { 44 | j.tg.Stop() 45 | } 46 | -------------------------------------------------------------------------------- /ant/job_balancemaintainer.go: -------------------------------------------------------------------------------- 1 | package ant 2 | 3 | import ( 4 | "log" 5 | "time" 6 | 7 | "github.com/NebulousLabs/Sia/types" 8 | ) 9 | 10 | // balanceMaintainer mines when the balance is below desiredBalance. The miner 11 | // is stopped if the balance exceeds the desired balance. 12 | func (j *jobRunner) balanceMaintainer(desiredBalance types.Currency) { 13 | j.tg.Add() 14 | defer j.tg.Done() 15 | 16 | minerRunning := true 17 | err := j.client.MinerStartGet() 18 | if err != nil { 19 | log.Printf("[%v balanceMaintainer ERROR]: %v\n", j.siaDirectory, err) 20 | return 21 | } 22 | 23 | // Every 20 seconds, check if the balance has exceeded the desiredBalance. If 24 | // it has and the miner is running, the miner is throttled. If the desired 25 | // balance has not been reached and the miner is not running, the miner is 26 | // started. 27 | for { 28 | select { 29 | case <-j.tg.StopChan(): 30 | return 31 | case <-time.After(time.Second * 20): 32 | } 33 | 34 | walletInfo, err := j.client.WalletGet() 35 | if err != nil { 36 | log.Printf("[%v balanceMaintainer ERROR]: %v\n", j.siaDirectory, err) 37 | return 38 | } 39 | 40 | haveDesiredBalance := walletInfo.ConfirmedSiacoinBalance.Cmp(desiredBalance) > 0 41 | if !minerRunning && !haveDesiredBalance { 42 | log.Printf("[%v balanceMaintainer INFO]: not enough currency, starting the miner\n", j.siaDirectory) 43 | minerRunning = true 44 | if err = j.client.MinerStartGet(); err != nil { 45 | log.Printf("[%v miner ERROR]: %v\n", j.siaDirectory, err) 46 | return 47 | } 48 | } else if minerRunning && haveDesiredBalance { 49 | log.Printf("[%v balanceMaintainer INFO]: mined enough currency, stopping the miner\n", j.siaDirectory) 50 | minerRunning = false 51 | if err = j.client.MinerStopGet(); err != nil { 52 | log.Printf("[%v balanceMaintainer ERROR]: %v\n", j.siaDirectory, err) 53 | return 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /ant/ant_test.go: -------------------------------------------------------------------------------- 1 | package ant 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "testing" 7 | 8 | "github.com/NebulousLabs/Sia/node/api/client" 9 | "github.com/NebulousLabs/Sia/types" 10 | ) 11 | 12 | func TestNewAnt(t *testing.T) { 13 | datadir, err := ioutil.TempDir("", "testing-data") 14 | if err != nil { 15 | t.Fatal(err) 16 | } 17 | defer os.RemoveAll(datadir) 18 | config := AntConfig{ 19 | APIAddr: "localhost:31337", 20 | RPCAddr: "localhost:31338", 21 | HostAddr: "localhost:31339", 22 | SiaDirectory: datadir, 23 | SiadPath: "siad", 24 | } 25 | 26 | ant, err := New(config) 27 | if err != nil { 28 | t.Fatal(err) 29 | } 30 | defer ant.Close() 31 | 32 | c := client.New("localhost:31337") 33 | if _, err = c.ConsensusGet(); err != nil { 34 | t.Fatal(err) 35 | } 36 | } 37 | 38 | func TestStartJob(t *testing.T) { 39 | datadir, err := ioutil.TempDir("", "testing-data") 40 | if err != nil { 41 | t.Fatal(err) 42 | } 43 | defer os.RemoveAll(datadir) 44 | 45 | config := AntConfig{ 46 | APIAddr: "localhost:31337", 47 | RPCAddr: "localhost:31338", 48 | HostAddr: "localhost:31339", 49 | SiaDirectory: datadir, 50 | SiadPath: "siad", 51 | } 52 | 53 | ant, err := New(config) 54 | if err != nil { 55 | t.Fatal(err) 56 | } 57 | defer ant.Close() 58 | 59 | // nonexistent job should throw an error 60 | err = ant.StartJob("thisjobdoesnotexist") 61 | if err == nil { 62 | t.Fatal("StartJob should return an error with a nonexistent job") 63 | } 64 | } 65 | 66 | func TestWalletAddress(t *testing.T) { 67 | datadir, err := ioutil.TempDir("", "testing-data") 68 | if err != nil { 69 | t.Fatal(err) 70 | } 71 | defer os.RemoveAll(datadir) 72 | 73 | config := AntConfig{ 74 | APIAddr: "localhost:31337", 75 | RPCAddr: "localhost:31338", 76 | HostAddr: "localhost:31339", 77 | SiaDirectory: datadir, 78 | SiadPath: "siad", 79 | } 80 | 81 | ant, err := New(config) 82 | if err != nil { 83 | t.Fatal(err) 84 | } 85 | defer ant.Close() 86 | 87 | addr, err := ant.WalletAddress() 88 | if err != nil { 89 | t.Fatal(err) 90 | } 91 | blankaddr := types.UnlockHash{} 92 | if *addr == blankaddr { 93 | t.Fatal("WalletAddress returned an empty address") 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /sia-antfarm/antfarm_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | "testing" 8 | "time" 9 | 10 | "github.com/NebulousLabs/Sia-Ant-Farm/ant" 11 | "github.com/NebulousLabs/Sia/node/api/client" 12 | ) 13 | 14 | // verify that createAntfarm() creates a new antfarm correctly. 15 | func TestNewAntfarm(t *testing.T) { 16 | if testing.Short() { 17 | t.SkipNow() 18 | } 19 | 20 | config := AntfarmConfig{ 21 | ListenAddress: "localhost:31337", 22 | AntConfigs: []ant.AntConfig{ 23 | { 24 | RPCAddr: "localhost:3337", 25 | Jobs: []string{ 26 | "gateway", 27 | }, 28 | }, 29 | }, 30 | } 31 | 32 | antfarm, err := createAntfarm(config) 33 | if err != nil { 34 | t.Fatal(err) 35 | } 36 | defer antfarm.Close() 37 | 38 | go antfarm.ServeAPI() 39 | 40 | res, err := http.DefaultClient.Get("http://localhost:31337/ants") 41 | if err != nil { 42 | t.Fatal(err) 43 | } 44 | defer res.Body.Close() 45 | 46 | var ants []*ant.Ant 47 | err = json.NewDecoder(res.Body).Decode(&ants) 48 | if err != nil { 49 | t.Fatal(err) 50 | } 51 | if len(ants) != len(config.AntConfigs) { 52 | t.Fatal("expected /ants to return the correct number of ants") 53 | } 54 | if ants[0].RPCAddr != config.AntConfigs[0].RPCAddr { 55 | t.Fatal("expected /ants to return the correct rpc address") 56 | } 57 | } 58 | 59 | // verify that connectExternalAntfarm connects antfarms to eachother correctly 60 | func TestConnectExternalAntfarm(t *testing.T) { 61 | if testing.Short() { 62 | t.SkipNow() 63 | } 64 | 65 | config1 := AntfarmConfig{ 66 | ListenAddress: "127.0.0.1:31337", 67 | DataDirPrefix: "antfarm-data1", 68 | AntConfigs: []ant.AntConfig{ 69 | { 70 | RPCAddr: "127.0.0.1:3337", 71 | Jobs: []string{ 72 | "gateway", 73 | }, 74 | }, 75 | }, 76 | } 77 | 78 | config2 := AntfarmConfig{ 79 | ListenAddress: "127.0.0.1:31338", 80 | DataDirPrefix: "antfarm-data2", 81 | AntConfigs: []ant.AntConfig{ 82 | { 83 | RPCAddr: "127.0.0.1:3338", 84 | Jobs: []string{ 85 | "gateway", 86 | }, 87 | }, 88 | }, 89 | } 90 | 91 | farm1, err := createAntfarm(config1) 92 | if err != nil { 93 | t.Fatal(err) 94 | } 95 | defer farm1.Close() 96 | go farm1.ServeAPI() 97 | 98 | farm2, err := createAntfarm(config2) 99 | if err != nil { 100 | t.Fatal(err) 101 | } 102 | defer farm2.Close() 103 | go farm2.ServeAPI() 104 | 105 | err = farm1.connectExternalAntfarm(config2.ListenAddress) 106 | if err != nil { 107 | t.Fatal(err) 108 | } 109 | 110 | // give a bit of time for the connection to succeed 111 | time.Sleep(time.Second * 3) 112 | 113 | // verify that farm2 has farm1 as its peer 114 | c := client.New(farm1.ants[0].APIAddr) 115 | gatewayInfo, err := c.GatewayGet() 116 | if err != nil { 117 | t.Fatal(err) 118 | } 119 | 120 | for _, ant := range farm2.ants { 121 | hasAddr := false 122 | for _, peer := range gatewayInfo.Peers { 123 | if fmt.Sprintf("%s", peer.NetAddress) == ant.RPCAddr { 124 | hasAddr = true 125 | } 126 | } 127 | if !hasAddr { 128 | t.Fatalf("farm1 is missing %v", ant.RPCAddr) 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /ant/job_host.go: -------------------------------------------------------------------------------- 1 | package ant 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "path/filepath" 7 | "time" 8 | 9 | "github.com/NebulousLabs/Sia/modules" 10 | "github.com/NebulousLabs/Sia/node/api/client" 11 | "github.com/NebulousLabs/Sia/types" 12 | ) 13 | 14 | // jobHost unlocks the wallet, mines some currency, and starts a host offering 15 | // storage to the ant farm. 16 | func (j *jobRunner) jobHost() { 17 | j.tg.Add() 18 | defer j.tg.Done() 19 | 20 | // Mine at least 50,000 SC 21 | desiredbalance := types.NewCurrency64(50000).Mul(types.SiacoinPrecision) 22 | success := false 23 | for start := time.Now(); time.Since(start) < 5*time.Minute; time.Sleep(time.Second) { 24 | walletInfo, err := j.client.WalletGet() 25 | if err != nil { 26 | log.Printf("[%v jobHost ERROR]: %v\n", j.siaDirectory, err) 27 | return 28 | } 29 | if walletInfo.ConfirmedSiacoinBalance.Cmp(desiredbalance) > 0 { 30 | success = true 31 | break 32 | } 33 | } 34 | if !success { 35 | log.Printf("[%v jobHost ERROR]: timeout: could not mine enough currency after 5 minutes\n", j.siaDirectory) 36 | return 37 | } 38 | 39 | // Create a temporary folder for hosting 40 | hostdir, _ := filepath.Abs(filepath.Join(j.siaDirectory, "hostdata")) 41 | os.MkdirAll(hostdir, 0700) 42 | 43 | // Add the storage folder. 44 | size := modules.SectorSize * 4096 45 | err := j.client.HostStorageFoldersAddPost(hostdir, size) 46 | if err != nil { 47 | log.Printf("[%v jobHost ERROR]: %v\n", j.siaDirectory, err) 48 | return 49 | } 50 | 51 | // Announce the host to the network, retrying up to 5 times before reporting 52 | // failure and returning. 53 | success = false 54 | for try := 0; try < 5; try++ { 55 | err = j.client.HostAnnouncePost() 56 | if err != nil { 57 | log.Printf("[%v jobHost ERROR]: %v\n", j.siaDirectory, err) 58 | } else { 59 | success = true 60 | break 61 | } 62 | time.Sleep(time.Second * 5) 63 | } 64 | if !success { 65 | log.Printf("[%v jobHost ERROR]: could not announce after 5 tries.\n", j.siaDirectory) 66 | return 67 | } 68 | log.Printf("[%v jobHost INFO]: succesfully performed host announcement\n", j.siaDirectory) 69 | 70 | // Accept contracts 71 | err = j.client.HostModifySettingPost(client.HostParamAcceptingContracts, true) 72 | if err != nil { 73 | log.Printf("[%v jobHost ERROR]: %v\n", j.siaDirectory, err) 74 | return 75 | } 76 | 77 | // Poll the API for host settings, logging them out with `INFO` tags. If 78 | // `StorageRevenue` decreases, log an ERROR. 79 | maxRevenue := types.NewCurrency64(0) 80 | for { 81 | select { 82 | case <-j.tg.StopChan(): 83 | return 84 | case <-time.After(time.Second * 15): 85 | } 86 | 87 | hostInfo, err := j.client.HostGet() 88 | if err != nil { 89 | log.Printf("[%v jobHost ERROR]: %v\n", j.siaDirectory, err) 90 | } 91 | 92 | // Print an error if storage revenue has decreased 93 | if hostInfo.FinancialMetrics.StorageRevenue.Cmp(maxRevenue) >= 0 { 94 | maxRevenue = hostInfo.FinancialMetrics.StorageRevenue 95 | } else { 96 | // Storage revenue has decreased! 97 | log.Printf("[%v jobHost ERROR]: StorageRevenue decreased! was %v is now %v\n", j.siaDirectory, maxRevenue, hostInfo.FinancialMetrics.StorageRevenue) 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /ant/siad.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package ant provides an abstraction for the functionality of 'ants' in the 3 | antfarm. Ants are Sia clients that have a myriad of user stories programmed as 4 | their behavior and report their successfullness at each user store. 5 | */ 6 | package ant 7 | 8 | import ( 9 | "errors" 10 | "fmt" 11 | "os" 12 | "os/exec" 13 | "path/filepath" 14 | "strings" 15 | "time" 16 | 17 | "github.com/NebulousLabs/Sia/node/api/client" 18 | ) 19 | 20 | // newSiad spawns a new siad process using os/exec and waits for the api to 21 | // become available. siadPath is the path to Siad, passed directly to 22 | // exec.Command. An error is returned if starting siad fails, otherwise a 23 | // pointer to siad's os.Cmd object is returned. The data directory `datadir` 24 | // is passed as siad's `--sia-directory`. 25 | func newSiad(siadPath string, datadir string, apiAddr string, rpcAddr string, hostAddr string) (*exec.Cmd, error) { 26 | if err := checkSiadConstants(siadPath); err != nil { 27 | return nil, err 28 | } 29 | // create a logfile for Sia's stderr and stdout. 30 | logfile, err := os.Create(filepath.Join(datadir, "sia-output.log")) 31 | if err != nil { 32 | return nil, err 33 | } 34 | cmd := exec.Command(siadPath, "--modules=cgthmrw", "--no-bootstrap", "--sia-directory="+datadir, "--api-addr="+apiAddr, "--rpc-addr="+rpcAddr, "--host-addr="+hostAddr) 35 | cmd.Stderr = logfile 36 | cmd.Stdout = logfile 37 | 38 | if err := cmd.Start(); err != nil { 39 | return nil, err 40 | } 41 | 42 | if err := waitForAPI(apiAddr, cmd); err != nil { 43 | return nil, err 44 | } 45 | 46 | return cmd, nil 47 | } 48 | 49 | // checkSiadConstants runs `siad version` and verifies that the supplied siad 50 | // is running the correct, dev, constants. Returns an error if the correct 51 | // constants are not running, otherwise returns nil. 52 | func checkSiadConstants(siadPath string) error { 53 | cmd := exec.Command(siadPath, "version") 54 | output, err := cmd.Output() 55 | if err != nil { 56 | return err 57 | } 58 | 59 | if !strings.Contains(string(output), "-dev") { 60 | return errors.New("supplied siad is not running required dev constants") 61 | } 62 | 63 | return nil 64 | } 65 | 66 | // stopSiad tries to stop the siad running at `apiAddr`, issuing a kill to its 67 | // `process` after a timeout. 68 | func stopSiad(apiAddr string, process *os.Process) { 69 | if err := client.New(apiAddr).DaemonStopGet(); err != nil { 70 | process.Kill() 71 | } 72 | 73 | // wait for 120 seconds for siad to terminate, then issue a kill signal. 74 | done := make(chan error) 75 | go func() { 76 | _, err := process.Wait() 77 | done <- err 78 | }() 79 | select { 80 | case <-done: 81 | case <-time.After(120 * time.Second): 82 | process.Kill() 83 | } 84 | } 85 | 86 | // waitForAPI blocks until the Sia API at apiAddr becomes available. 87 | // if siad returns while waiting for the api, return an error. 88 | func waitForAPI(apiAddr string, siad *exec.Cmd) error { 89 | c := client.New(apiAddr) 90 | 91 | exitchan := make(chan error) 92 | go func() { 93 | _, err := siad.Process.Wait() 94 | exitchan <- err 95 | }() 96 | 97 | // Wait for the Sia API to become available. 98 | success := false 99 | for start := time.Now(); time.Since(start) < 5*time.Minute; time.Sleep(time.Millisecond * 100) { 100 | if success { 101 | break 102 | } 103 | select { 104 | case err := <-exitchan: 105 | return fmt.Errorf("siad exited unexpectedly while waiting for api, exited with error: %v", err) 106 | default: 107 | if _, err := c.ConsensusGet(); err == nil { 108 | success = true 109 | } 110 | } 111 | } 112 | if !success { 113 | stopSiad(apiAddr, siad.Process) 114 | return errors.New("timeout: couldnt reach api after 5 minutes") 115 | } 116 | return nil 117 | } 118 | -------------------------------------------------------------------------------- /sia-antfarm/ant_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "reflect" 7 | "testing" 8 | "time" 9 | 10 | "github.com/NebulousLabs/Sia-Ant-Farm/ant" 11 | "github.com/NebulousLabs/Sia/node/api/client" 12 | ) 13 | 14 | // TestStartAnts verifies that startAnts successfully starts ants given some 15 | // configs. 16 | func TestStartAnts(t *testing.T) { 17 | if testing.Short() { 18 | t.SkipNow() 19 | } 20 | 21 | configs := []ant.AntConfig{ 22 | {}, 23 | {}, 24 | {}, 25 | } 26 | 27 | os.MkdirAll("./antfarm-data", 0700) 28 | defer os.RemoveAll("./antfarm-data") 29 | 30 | ants, err := startAnts(configs...) 31 | if err != nil { 32 | t.Fatal(err) 33 | } 34 | defer func() { 35 | for _, ant := range ants { 36 | ant.Close() 37 | } 38 | }() 39 | 40 | // verify each ant has a reachable api 41 | for _, ant := range ants { 42 | c := client.New(ant.APIAddr) 43 | if _, err := c.ConsensusGet(); err != nil { 44 | t.Fatal(err) 45 | } 46 | } 47 | } 48 | 49 | func TestConnectAnts(t *testing.T) { 50 | if testing.Short() { 51 | t.SkipNow() 52 | } 53 | 54 | // connectAnts should throw an error if only one ant is provided 55 | if err := connectAnts(&ant.Ant{}); err == nil { 56 | t.Fatal("connectAnts didnt throw an error with only one ant") 57 | } 58 | 59 | configs := []ant.AntConfig{ 60 | {}, 61 | {}, 62 | {}, 63 | {}, 64 | {}, 65 | } 66 | 67 | os.MkdirAll("./antfarm-data", 0700) 68 | defer os.RemoveAll("./antfarm-data") 69 | 70 | ants, err := startAnts(configs...) 71 | if err != nil { 72 | t.Fatal(err) 73 | } 74 | defer func() { 75 | for _, ant := range ants { 76 | ant.Close() 77 | } 78 | }() 79 | 80 | err = connectAnts(ants...) 81 | if err != nil { 82 | t.Fatal(err) 83 | } 84 | 85 | c := client.New(ants[0].APIAddr) 86 | gatewayInfo, err := c.GatewayGet() 87 | if err != nil { 88 | t.Fatal(err) 89 | } 90 | 91 | for _, ant := range ants[1:] { 92 | hasAddr := false 93 | for _, peer := range gatewayInfo.Peers { 94 | if fmt.Sprintf("%s", peer.NetAddress) == "127.0.0.1"+ant.RPCAddr { 95 | hasAddr = true 96 | } 97 | } 98 | if !hasAddr { 99 | t.Fatalf("the central ant is missing %v", "127.0.0.1"+ant.RPCAddr) 100 | } 101 | } 102 | } 103 | 104 | func TestAntConsensusGroups(t *testing.T) { 105 | if testing.Short() { 106 | t.SkipNow() 107 | } 108 | 109 | // spin up our testing ants 110 | configs := []ant.AntConfig{ 111 | {}, 112 | {}, 113 | {}, 114 | } 115 | 116 | os.MkdirAll("./antfarm-data", 0700) 117 | defer os.RemoveAll("./antfarm-data") 118 | 119 | ants, err := startAnts(configs...) 120 | if err != nil { 121 | t.Fatal(err) 122 | } 123 | defer func() { 124 | for _, ant := range ants { 125 | ant.Close() 126 | } 127 | }() 128 | 129 | groups, err := antConsensusGroups(ants...) 130 | if err != nil { 131 | t.Fatal(err) 132 | } 133 | if len(groups) != 1 { 134 | t.Fatal("expected 1 consensus group initially") 135 | } 136 | if len(groups[0]) != len(ants) { 137 | t.Fatal("expected the consensus group to have all the ants") 138 | } 139 | 140 | // Start an ant that is desynced from the rest of the network 141 | cfg, err := parseConfig(ant.AntConfig{Jobs: []string{"miner"}}) 142 | if err != nil { 143 | t.Fatal(err) 144 | } 145 | otherAnt, err := ant.New(cfg) 146 | if err != nil { 147 | t.Fatal(err) 148 | } 149 | ants = append(ants, otherAnt) 150 | 151 | // Wait for the other ant to mine a few blocks 152 | time.Sleep(time.Second * 30) 153 | 154 | groups, err = antConsensusGroups(ants...) 155 | if err != nil { 156 | t.Fatal(err) 157 | } 158 | if len(groups) != 2 { 159 | t.Fatal("expected 2 consensus groups") 160 | } 161 | if len(groups[0]) != len(ants)-1 { 162 | t.Fatal("expected the first consensus group to have 3 ants") 163 | } 164 | if len(groups[1]) != 1 { 165 | t.Fatal("expected the second consensus group to have 1 ant") 166 | } 167 | if !reflect.DeepEqual(groups[1][0], otherAnt) { 168 | t.Fatal("expected the miner ant to be in the second consensus group") 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /ant/ant.go: -------------------------------------------------------------------------------- 1 | package ant 2 | 3 | import ( 4 | "errors" 5 | "log" 6 | "net" 7 | "os/exec" 8 | 9 | "github.com/NebulousLabs/Sia/types" 10 | "github.com/NebulousLabs/go-upnp" 11 | ) 12 | 13 | // AntConfig represents a configuration object passed to New(), used to 14 | // configure a newly created Sia Ant. 15 | type AntConfig struct { 16 | APIAddr string `json:",omitempty"` 17 | RPCAddr string `json:",omitempty"` 18 | HostAddr string `json:",omitempty"` 19 | SiaDirectory string `json:",omitempty"` 20 | Name string `json:",omitempty"` 21 | SiadPath string 22 | Jobs []string 23 | DesiredCurrency uint64 24 | } 25 | 26 | // An Ant is a Sia Client programmed with network user stories. It executes 27 | // these user stories and reports on their successfulness. 28 | type Ant struct { 29 | APIAddr string 30 | RPCAddr string 31 | 32 | Config AntConfig 33 | 34 | siad *exec.Cmd 35 | jr *jobRunner 36 | 37 | // A variable to track which blocks + heights the sync detector has seen 38 | // for this ant. The map will just keep growing, but it shouldn't take up a 39 | // prohibitive amount of space. 40 | SeenBlocks map[types.BlockHeight]types.BlockID `json:"-"` 41 | } 42 | 43 | // clearPorts discovers the UPNP enabled router and clears the ports used by an 44 | // ant before the ant is started. 45 | func clearPorts(config AntConfig) error { 46 | rpcaddr, err := net.ResolveTCPAddr("tcp", config.RPCAddr) 47 | if err != nil { 48 | return err 49 | } 50 | 51 | hostaddr, err := net.ResolveTCPAddr("tcp", config.HostAddr) 52 | if err != nil { 53 | return err 54 | } 55 | 56 | upnprouter, err := upnp.Discover() 57 | if err != nil { 58 | return err 59 | } 60 | 61 | err = upnprouter.Clear(uint16(rpcaddr.Port)) 62 | if err != nil { 63 | return err 64 | } 65 | 66 | err = upnprouter.Clear(uint16(hostaddr.Port)) 67 | if err != nil { 68 | return err 69 | } 70 | 71 | return nil 72 | } 73 | 74 | // New creates a new Ant using the configuration passed through `config`. 75 | func New(config AntConfig) (*Ant, error) { 76 | var err error 77 | // unforward the ports required for this ant 78 | err = clearPorts(config) 79 | if err != nil { 80 | log.Printf("error clearing upnp ports for ant: %v\n", err) 81 | } 82 | 83 | // Construct the ant's Siad instance 84 | siad, err := newSiad(config.SiadPath, config.SiaDirectory, config.APIAddr, config.RPCAddr, config.HostAddr) 85 | if err != nil { 86 | return nil, err 87 | } 88 | 89 | // Ensure siad is always stopped if an error is returned. 90 | defer func() { 91 | if err != nil { 92 | stopSiad(config.APIAddr, siad.Process) 93 | } 94 | }() 95 | 96 | j, err := newJobRunner(config.APIAddr, "", config.SiaDirectory) 97 | if err != nil { 98 | return nil, err 99 | } 100 | 101 | for _, job := range config.Jobs { 102 | switch job { 103 | case "miner": 104 | go j.blockMining() 105 | case "host": 106 | go j.jobHost() 107 | case "renter": 108 | go j.storageRenter() 109 | case "gateway": 110 | go j.gatewayConnectability() 111 | } 112 | } 113 | 114 | if config.DesiredCurrency != 0 { 115 | go j.balanceMaintainer(types.SiacoinPrecision.Mul64(config.DesiredCurrency)) 116 | } 117 | 118 | return &Ant{ 119 | APIAddr: config.APIAddr, 120 | RPCAddr: config.RPCAddr, 121 | Config: config, 122 | 123 | siad: siad, 124 | jr: j, 125 | 126 | SeenBlocks: make(map[types.BlockHeight]types.BlockID), 127 | }, nil 128 | } 129 | 130 | // Close releases all resources created by the ant, including the Siad 131 | // subprocess. 132 | func (a *Ant) Close() error { 133 | a.jr.Stop() 134 | stopSiad(a.APIAddr, a.siad.Process) 135 | return nil 136 | } 137 | 138 | // StartJob starts the job indicated by `job` after an ant has been 139 | // initialized. Arguments are passed to the job using args. 140 | func (a *Ant) StartJob(job string, args ...interface{}) error { 141 | if a.jr == nil { 142 | return errors.New("ant is not running") 143 | } 144 | 145 | switch job { 146 | case "miner": 147 | go a.jr.blockMining() 148 | case "host": 149 | go a.jr.jobHost() 150 | case "renter": 151 | go a.jr.storageRenter() 152 | case "gateway": 153 | go a.jr.gatewayConnectability() 154 | case "bigspender": 155 | go a.jr.bigSpender() 156 | case "littlesupplier": 157 | go a.jr.littleSupplier(args[0].(types.UnlockHash)) 158 | default: 159 | return errors.New("no such job") 160 | } 161 | 162 | return nil 163 | } 164 | 165 | // BlockHeight returns the highest block height seen by the ant. 166 | func (a *Ant) BlockHeight() types.BlockHeight { 167 | height := types.BlockHeight(0) 168 | for h := range a.SeenBlocks { 169 | if h > height { 170 | height = h 171 | } 172 | } 173 | return height 174 | } 175 | 176 | // WalletAddress returns a wallet address that this ant can receive coins on. 177 | func (a *Ant) WalletAddress() (*types.UnlockHash, error) { 178 | if a.jr == nil { 179 | return nil, errors.New("ant is not running") 180 | } 181 | 182 | addressGet, err := a.jr.client.WalletAddressGet() 183 | if err != nil { 184 | return nil, err 185 | } 186 | 187 | return &addressGet.Address, nil 188 | } 189 | -------------------------------------------------------------------------------- /sia-antfarm/antfarm.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/julienschmidt/httprouter" 6 | "log" 7 | "net" 8 | "net/http" 9 | "os" 10 | "time" 11 | 12 | "github.com/NebulousLabs/Sia-Ant-Farm/ant" 13 | ) 14 | 15 | type ( 16 | // AntfarmConfig contains the fields to parse and use to create a sia-antfarm. 17 | AntfarmConfig struct { 18 | ListenAddress string 19 | DataDirPrefix string 20 | AntConfigs []ant.AntConfig 21 | AutoConnect bool 22 | 23 | // ExternalFarms is a slice of net addresses representing the API addresses 24 | // of other antFarms to connect to. 25 | ExternalFarms []string 26 | } 27 | 28 | // antFarm defines the 'antfarm' type. antFarm orchestrates a collection of 29 | // ants and provides an API server to interact with them. 30 | antFarm struct { 31 | apiListener net.Listener 32 | 33 | // ants is a slice of Ants in this antfarm. 34 | ants []*ant.Ant 35 | 36 | // externalAnts is a slice of externally connected ants, that is, ants that 37 | // are connected to this antfarm but managed by another antfarm. 38 | externalAnts []*ant.Ant 39 | router *httprouter.Router 40 | } 41 | ) 42 | 43 | // createAntfarm creates a new antFarm given the supplied AntfarmConfig 44 | func createAntfarm(config AntfarmConfig) (*antFarm, error) { 45 | // clear old antfarm data before creating an antfarm 46 | datadir := "./antfarm-data" 47 | if config.DataDirPrefix != "" { 48 | datadir = config.DataDirPrefix 49 | } 50 | 51 | os.RemoveAll(datadir) 52 | os.MkdirAll(datadir, 0700) 53 | 54 | farm := &antFarm{} 55 | 56 | // start up each ant process with its jobs 57 | ants, err := startAnts(config.AntConfigs...) 58 | if err != nil { 59 | return nil, err 60 | } 61 | 62 | err = startJobs(ants...) 63 | if err != nil { 64 | return nil, err 65 | } 66 | 67 | farm.ants = ants 68 | defer func() { 69 | if err != nil { 70 | farm.Close() 71 | } 72 | }() 73 | 74 | // if the AutoConnect flag is set, use connectAnts to bootstrap the network. 75 | if config.AutoConnect { 76 | if err = connectAnts(ants...); err != nil { 77 | return nil, err 78 | } 79 | } 80 | // connect the external antFarms 81 | for _, address := range config.ExternalFarms { 82 | if err = farm.connectExternalAntfarm(address); err != nil { 83 | return nil, err 84 | } 85 | } 86 | // start up the api server listener 87 | farm.apiListener, err = net.Listen("tcp", config.ListenAddress) 88 | if err != nil { 89 | return nil, err 90 | } 91 | 92 | // construct the router and serve the API. 93 | farm.router = httprouter.New() 94 | farm.router.GET("/ants", farm.getAnts) 95 | 96 | return farm, nil 97 | } 98 | 99 | // allAnts returns all ants, external and internal, associated with this 100 | // antFarm. 101 | func (af *antFarm) allAnts() []*ant.Ant { 102 | return append(af.ants, af.externalAnts...) 103 | } 104 | 105 | // connectExternalAntfarm connects the current antfarm to an external antfarm, 106 | // using the antfarm api at externalAddress. 107 | func (af *antFarm) connectExternalAntfarm(externalAddress string) error { 108 | res, err := http.DefaultClient.Get("http://" + externalAddress + "/ants") 109 | if err != nil { 110 | return err 111 | } 112 | defer res.Body.Close() 113 | 114 | var externalAnts []*ant.Ant 115 | err = json.NewDecoder(res.Body).Decode(&externalAnts) 116 | if err != nil { 117 | return err 118 | } 119 | af.externalAnts = append(af.externalAnts, externalAnts...) 120 | return connectAnts(af.allAnts()...) 121 | } 122 | 123 | // ServeAPI serves the antFarm's http API. 124 | func (af *antFarm) ServeAPI() error { 125 | http.Serve(af.apiListener, af.router) 126 | return nil 127 | } 128 | 129 | // permanentSyncMonitor checks that all ants in the antFarm are on the same 130 | // blockchain. 131 | func (af *antFarm) permanentSyncMonitor() { 132 | // Give 30 seconds for everything to start up. 133 | time.Sleep(time.Second * 30) 134 | 135 | // Every 20 seconds, list all consensus groups and display the block height. 136 | for { 137 | time.Sleep(time.Second * 20) 138 | 139 | groups, err := antConsensusGroups(af.allAnts()...) 140 | if err != nil { 141 | log.Println("error checking sync status of antfarm: ", err) 142 | continue 143 | } 144 | if len(groups) == 1 { 145 | log.Println("Ants are synchronized. Block Height: ", af.ants[0].BlockHeight()) 146 | } else { 147 | log.Println("Ants split into multiple groups.") 148 | for i, group := range groups { 149 | if i != 0 { 150 | log.Println() 151 | } 152 | log.Println("Group ", i+1) 153 | for _, a := range group { 154 | log.Println(a.APIAddr) 155 | } 156 | } 157 | } 158 | } 159 | } 160 | 161 | // getAnts is a http handler that returns the ants currently running on the 162 | // antfarm. 163 | func (af *antFarm) getAnts(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { 164 | err := json.NewEncoder(w).Encode(af.ants) 165 | if err != nil { 166 | http.Error(w, "error encoding ants", 500) 167 | } 168 | } 169 | 170 | // Close signals all the ants to stop and waits for them to return. 171 | func (af *antFarm) Close() error { 172 | if af.apiListener != nil { 173 | af.apiListener.Close() 174 | } 175 | for _, ant := range af.ants { 176 | ant.Close() 177 | } 178 | return nil 179 | } 180 | -------------------------------------------------------------------------------- /sia-antfarm/ant.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io/ioutil" 7 | "net" 8 | "os" 9 | 10 | "github.com/NebulousLabs/Sia-Ant-Farm/ant" 11 | "github.com/NebulousLabs/Sia/modules" 12 | "github.com/NebulousLabs/Sia/node/api/client" 13 | "github.com/NebulousLabs/Sia/types" 14 | ) 15 | 16 | // getAddrs returns n free listening ports by leveraging the 17 | // behaviour of net.Listen(":0"). Addresses are returned in the format of 18 | // ":port" 19 | func getAddrs(n int) ([]string, error) { 20 | var addrs []string 21 | 22 | for i := 0; i < n; i++ { 23 | l, err := net.Listen("tcp", ":0") 24 | if err != nil { 25 | return nil, err 26 | } 27 | defer l.Close() 28 | addrs = append(addrs, fmt.Sprintf(":%v", l.Addr().(*net.TCPAddr).Port)) 29 | } 30 | return addrs, nil 31 | } 32 | 33 | // connectAnts connects two or more ants to the first ant in the slice, 34 | // effectively bootstrapping the antfarm. 35 | func connectAnts(ants ...*ant.Ant) error { 36 | if len(ants) < 2 { 37 | return errors.New("you must call connectAnts with at least two ants") 38 | } 39 | targetAnt := ants[0] 40 | c := client.New(targetAnt.APIAddr) 41 | for _, ant := range ants[1:] { 42 | connectQuery := ant.RPCAddr 43 | addr := modules.NetAddress(ant.RPCAddr) 44 | if addr.Host() == "" { 45 | connectQuery = "127.0.0.1" + ant.RPCAddr 46 | } 47 | err := c.GatewayConnectPost(modules.NetAddress(connectQuery)) 48 | if err != nil { 49 | return err 50 | } 51 | } 52 | return nil 53 | } 54 | 55 | // antConsensusGroups iterates through all of the ants known to the antFarm 56 | // and returns the different consensus groups that have been formed between the 57 | // ants. 58 | // 59 | // The outer slice is the list of gorups, and the inner slice is a list of ants 60 | // in each group. 61 | func antConsensusGroups(ants ...*ant.Ant) (groups [][]*ant.Ant, err error) { 62 | for _, a := range ants { 63 | c := client.New(a.APIAddr) 64 | cg, err := c.ConsensusGet() 65 | if err != nil { 66 | return nil, err 67 | } 68 | a.SeenBlocks[cg.Height] = cg.CurrentBlock 69 | 70 | // Compare this ant to all of the other groups. If the ant fits in a 71 | // group, insert it. If not, add it to the next group. 72 | found := false 73 | for gi, group := range groups { 74 | for i := types.BlockHeight(0); i < 8; i++ { 75 | id1, exists1 := a.SeenBlocks[cg.Height-i] 76 | id2, exists2 := group[0].SeenBlocks[cg.Height-i] // no group should have a length of zero 77 | if exists1 && exists2 && id1 == id2 { 78 | groups[gi] = append(groups[gi], a) 79 | found = true 80 | break 81 | } 82 | } 83 | if found { 84 | break 85 | } 86 | } 87 | if !found { 88 | groups = append(groups, []*ant.Ant{a}) 89 | } 90 | } 91 | return groups, nil 92 | } 93 | 94 | // startAnts starts the ants defined by configs and blocks until every API 95 | // has loaded. 96 | func startAnts(configs ...ant.AntConfig) ([]*ant.Ant, error) { 97 | var ants []*ant.Ant 98 | var err error 99 | 100 | // Ensure that, if an error occurs, all the ants that have been started are 101 | // closed before returning. 102 | defer func() { 103 | if err != nil { 104 | for _, ant := range ants { 105 | ant.Close() 106 | } 107 | } 108 | }() 109 | 110 | for i, config := range configs { 111 | cfg, err := parseConfig(config) 112 | if err != nil { 113 | return nil, err 114 | } 115 | fmt.Printf("[INFO] starting ant %v with config %v\n", i, cfg) 116 | ant, err := ant.New(cfg) 117 | if err != nil { 118 | return nil, err 119 | } 120 | defer func() { 121 | if err != nil { 122 | ant.Close() 123 | } 124 | }() 125 | ants = append(ants, ant) 126 | } 127 | 128 | return ants, nil 129 | } 130 | 131 | // startJobs starts all the jobs for each ant. 132 | func startJobs(ants ...*ant.Ant) error { 133 | // first, pull out any constants needed for the jobs 134 | var spenderAddress *types.UnlockHash 135 | for _, ant := range ants { 136 | for _, job := range ant.Config.Jobs { 137 | if job == "bigspender" { 138 | addr, err := ant.WalletAddress() 139 | if err != nil { 140 | return err 141 | } 142 | spenderAddress = addr 143 | } 144 | } 145 | } 146 | // start jobs requiring those constants 147 | for _, ant := range ants { 148 | for _, job := range ant.Config.Jobs { 149 | if job == "bigspender" { 150 | ant.StartJob(job) 151 | } 152 | if job == "littlesupplier" && spenderAddress != nil { 153 | err := ant.StartJob(job, *spenderAddress) 154 | if err != nil { 155 | return err 156 | } 157 | err = ant.StartJob("miner") 158 | if err != nil { 159 | return err 160 | } 161 | } 162 | } 163 | } 164 | return nil 165 | } 166 | 167 | // parseConfig takes an input `config` and fills it with default values if 168 | // required. 169 | func parseConfig(config ant.AntConfig) (ant.AntConfig, error) { 170 | // if config.SiaDirectory isn't set, use ioutil.TempDir to create a new 171 | // temporary directory. 172 | if config.SiaDirectory == "" && config.Name == "" { 173 | tempdir, err := ioutil.TempDir("./antfarm-data", "ant") 174 | if err != nil { 175 | return ant.AntConfig{}, err 176 | } 177 | config.SiaDirectory = tempdir 178 | } 179 | 180 | if config.Name != "" { 181 | siadir := fmt.Sprintf("./antfarm-data/%v", config.Name) 182 | err := os.Mkdir(siadir, 0755) 183 | if err != nil { 184 | return ant.AntConfig{}, err 185 | } 186 | config.SiaDirectory = siadir 187 | } 188 | 189 | if config.SiadPath == "" { 190 | config.SiadPath = "siad" 191 | } 192 | 193 | // DesiredCurrency and `miner` are mutually exclusive. 194 | hasMiner := false 195 | for _, job := range config.Jobs { 196 | if job == "miner" { 197 | hasMiner = true 198 | } 199 | } 200 | if hasMiner && config.DesiredCurrency != 0 { 201 | return ant.AntConfig{}, errors.New("error parsing config: cannot have desired currency with miner job") 202 | } 203 | 204 | // Automatically generate 3 free operating system ports for the Ant's api, 205 | // rpc, and host addresses 206 | addrs, err := getAddrs(3) 207 | if err != nil { 208 | return ant.AntConfig{}, err 209 | } 210 | if config.APIAddr == "" { 211 | config.APIAddr = "localhost" + addrs[0] 212 | } 213 | if config.RPCAddr == "" { 214 | config.RPCAddr = addrs[1] 215 | } 216 | if config.HostAddr == "" { 217 | config.HostAddr = addrs[2] 218 | } 219 | 220 | return config, nil 221 | } 222 | -------------------------------------------------------------------------------- /ant/job_renter.go: -------------------------------------------------------------------------------- 1 | package ant 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "io/ioutil" 7 | "log" 8 | "os" 9 | "path/filepath" 10 | "sync" 11 | "time" 12 | 13 | "github.com/NebulousLabs/merkletree" 14 | 15 | "github.com/NebulousLabs/Sia/crypto" 16 | "github.com/NebulousLabs/Sia/modules" 17 | "github.com/NebulousLabs/Sia/node/api" 18 | "github.com/NebulousLabs/Sia/node/api/client" 19 | "github.com/NebulousLabs/Sia/types" 20 | "github.com/NebulousLabs/fastrand" 21 | ) 22 | 23 | const ( 24 | // downloadFileFrequency defines how frequently the renter job downloads 25 | // files from the network. 26 | downloadFileFrequency = uploadFileFrequency * 3 / 2 27 | 28 | // initialBalanceWarningTimeout defines how long the renter will wait 29 | // before reporting to the user that the required inital balance has not 30 | // been reached. 31 | initialBalanceWarningTimeout = time.Minute * 10 32 | 33 | // setAllowanceWarningTimeout defines how long the renter will wait before 34 | // reporting to the user that the allowance has not yet been set 35 | // successfully. 36 | setAllowanceWarningTimeout = time.Minute * 2 37 | 38 | // uploadFileFrequency defines how frequently the renter job uploads files 39 | // to the network. 40 | uploadFileFrequency = time.Second * 60 41 | 42 | // deleteFileFrequency defines how frequently the renter job deletes files 43 | // from the network. 44 | deleteFileFrequency = time.Minute * 2 45 | 46 | // deleteFileThreshold defines the minimum number of files uploaded before 47 | // deletion occurs. 48 | deleteFileThreshold = 30 49 | 50 | // uploadTimeout defines the maximum time allowed for an upload operation to 51 | // complete, ie for an upload to reach 100%. 52 | maxUploadTime = time.Minute * 10 53 | 54 | // renterAllowancePeriod defines the block duration of the renter's allowance 55 | renterAllowancePeriod = 100 56 | 57 | // uploadFileSize defines the size of the test files to be uploaded. Test 58 | // files are filled with random data. 59 | uploadFileSize = 1e8 60 | ) 61 | 62 | var ( 63 | // renterAllowance defines the number of coins that the renter has to 64 | // spend. 65 | renterAllowance = types.NewCurrency64(20e3).Mul(types.SiacoinPrecision) 66 | 67 | // requiredInitialBalance sets the number of coins that the renter requires 68 | // before uploading will begin. 69 | requiredInitialBalance = types.NewCurrency64(100e3).Mul(types.SiacoinPrecision) 70 | ) 71 | 72 | // renterFile stores the location and checksum of a file active on the renter. 73 | type renterFile struct { 74 | merkleRoot crypto.Hash 75 | sourceFile string 76 | } 77 | 78 | // renterJob contains statefulness that is used to drive the renter. Most 79 | // importantly, it contains a list of files that the renter is currently 80 | // uploading to the network. 81 | type renterJob struct { 82 | files []renterFile 83 | 84 | jr *jobRunner 85 | mu sync.Mutex 86 | } 87 | 88 | // randFillFile will append 'size' bytes to the input file, returning the 89 | // merkle root of the bytes that were appended. 90 | func randFillFile(f *os.File, size uint64) (h crypto.Hash, err error) { 91 | tee := io.TeeReader(io.LimitReader(fastrand.Reader, int64(size)), f) 92 | root, err := merkletree.ReaderRoot(tee, crypto.NewHash(), crypto.SegmentSize) 93 | copy(h[:], root) 94 | return 95 | } 96 | 97 | // permanentDownloader is a function that continuously runs for the renter job, 98 | // downloading a file at random every 400 seconds. 99 | func (r *renterJob) permanentDownloader() { 100 | // Wait for the first file to be uploaded before starting the download 101 | // loop. 102 | for { 103 | select { 104 | case <-r.jr.tg.StopChan(): 105 | return 106 | case <-time.After(downloadFileFrequency): 107 | } 108 | 109 | // Download a file. 110 | if err := r.download(); err != nil { 111 | log.Printf("[ERROR] [renter] [%v]: %v\n", r.jr.siaDirectory, err) 112 | } 113 | } 114 | } 115 | 116 | // permanentUploader is a function that continuously runs for the renter job, 117 | // uploading a 500MB file every 240 seconds (10 blocks). The renter should have 118 | // already set an allowance. 119 | func (r *renterJob) permanentUploader() { 120 | // Make the source files directory 121 | os.Mkdir(filepath.Join(r.jr.siaDirectory, "renterSourceFiles"), 0700) 122 | for { 123 | // Wait a while between upload attempts. 124 | select { 125 | case <-r.jr.tg.StopChan(): 126 | return 127 | case <-time.After(uploadFileFrequency): 128 | } 129 | 130 | // Upload a file. 131 | if err := r.upload(); err != nil { 132 | log.Printf("[ERROR] [renter] [%v]: %v\n", r.jr.siaDirectory, err) 133 | } 134 | } 135 | } 136 | 137 | // permanentDeleter deletes one random file from the renter every 100 seconds 138 | // once 10 or more files have been uploaded. 139 | func (r *renterJob) permanentDeleter() { 140 | for { 141 | select { 142 | case <-r.jr.tg.StopChan(): 143 | return 144 | case <-time.After(deleteFileFrequency): 145 | } 146 | 147 | if err := r.deleteRandom(); err != nil { 148 | log.Printf("[ERROR] [renter] [%v]: %v\n", r.jr.siaDirectory, err) 149 | } 150 | } 151 | } 152 | 153 | // deleteRandom deletes a random file from the renter. 154 | func (r *renterJob) deleteRandom() error { 155 | r.mu.Lock() 156 | defer r.mu.Unlock() 157 | 158 | // no-op with fewer than 10 files 159 | if len(r.files) < deleteFileThreshold { 160 | return nil 161 | } 162 | 163 | randindex := fastrand.Intn(len(r.files)) 164 | 165 | if err := r.jr.client.RenterDeletePost(r.files[randindex].sourceFile); err != nil { 166 | return err 167 | } 168 | 169 | log.Printf("[%v jobStorageRenter INFO]: successfully deleted file\n", r.jr.siaDirectory) 170 | os.Remove(r.files[randindex].sourceFile) 171 | r.files = append(r.files[:randindex], r.files[randindex+1:]...) 172 | 173 | return nil 174 | } 175 | 176 | // isFileInDownloads grabs the files currently being downloaded by the 177 | // renter and returns bool `true` if fileToDownload exists in the 178 | // download list. It also returns the DownloadInfo for the requested `file`. 179 | func isFileInDownloads(client *client.Client, file modules.FileInfo) (bool, api.DownloadInfo, error) { 180 | var dlinfo api.DownloadInfo 181 | renterDownloads, err := client.RenterDownloadsGet() 182 | if err != nil { 183 | return false, dlinfo, err 184 | } 185 | 186 | hasFile := false 187 | for _, download := range renterDownloads.Downloads { 188 | if download.SiaPath == file.SiaPath { 189 | hasFile = true 190 | dlinfo = download 191 | } 192 | } 193 | 194 | return hasFile, dlinfo, nil 195 | } 196 | 197 | // download will download a random file from the network. 198 | func (r *renterJob) download() error { 199 | r.jr.tg.Add() 200 | defer r.jr.tg.Done() 201 | 202 | // Download a random file from the renter's file list 203 | renterFiles, err := r.jr.client.RenterFilesGet() 204 | if err != nil { 205 | return fmt.Errorf("error calling /renter/files: %v", err) 206 | } 207 | 208 | // Filter out files which are not available. 209 | availableFiles := renterFiles.Files[:0] 210 | for _, file := range renterFiles.Files { 211 | if file.Available { 212 | availableFiles = append(availableFiles, file) 213 | } 214 | } 215 | 216 | // Do nothing if there are not any files to be downloaded. 217 | if len(availableFiles) == 0 { 218 | return fmt.Errorf("tried to download a file, but none were available") 219 | } 220 | 221 | // Download a file at random. 222 | fileToDownload := availableFiles[fastrand.Intn(len(availableFiles))] 223 | 224 | // Use ioutil.TempFile to get a random temporary filename. 225 | f, err := ioutil.TempFile("", "antfarm-renter") 226 | if err != nil { 227 | return fmt.Errorf("failed to create temporary file for download: %v", err) 228 | } 229 | defer f.Close() 230 | destPath, _ := filepath.Abs(f.Name()) 231 | os.Remove(destPath) 232 | 233 | log.Printf("[INFO] [renter] [%v] downloading %v to %v", r.jr.siaDirectory, fileToDownload.SiaPath, destPath) 234 | 235 | err = r.jr.client.RenterDownloadGet(fileToDownload.SiaPath, destPath, 0, fileToDownload.Filesize, true) 236 | if err != nil { 237 | return fmt.Errorf("failed in call to /renter/download: %v", err) 238 | } 239 | 240 | // Wait for the file to appear in the download list 241 | success := false 242 | for start := time.Now(); time.Since(start) < 3*time.Minute; { 243 | select { 244 | case <-r.jr.tg.StopChan(): 245 | return nil 246 | case <-time.After(time.Second): 247 | } 248 | 249 | hasFile, _, err := isFileInDownloads(r.jr.client, fileToDownload) 250 | if err != nil { 251 | return fmt.Errorf("error waiting for the file to appear in the download queue: %v", err) 252 | } 253 | if hasFile { 254 | success = true 255 | break 256 | } 257 | } 258 | if !success { 259 | return fmt.Errorf("file %v did not appear in the renter download queue", fileToDownload.SiaPath) 260 | } 261 | 262 | // Wait for the file to be finished downloading, with a timeout of 15 minutes. 263 | success = false 264 | for start := time.Now(); time.Since(start) < 15*time.Minute; { 265 | select { 266 | case <-r.jr.tg.StopChan(): 267 | return nil 268 | case <-time.After(time.Second): 269 | } 270 | 271 | hasFile, info, err := isFileInDownloads(r.jr.client, fileToDownload) 272 | if err != nil { 273 | return fmt.Errorf("error waiting for the file to disappear from the download queue: %v", err) 274 | } 275 | if hasFile && info.Received == info.Filesize { 276 | success = true 277 | break 278 | } else if !hasFile { 279 | log.Printf("[INFO] [renter] [%v]: file unexpectedly missing from download list\n", r.jr.siaDirectory) 280 | } else { 281 | log.Printf("[INFO] [renter] [%v]: currently downloading %v, received %v bytes\n", r.jr.siaDirectory, fileToDownload.SiaPath, info.Received) 282 | } 283 | } 284 | if !success { 285 | return fmt.Errorf("file %v did not complete downloading", fileToDownload.SiaPath) 286 | } 287 | log.Printf("[INFO] [renter] [%v]: successfully downloaded %v to %v\n", r.jr.siaDirectory, fileToDownload.SiaPath, destPath) 288 | return nil 289 | } 290 | 291 | // upload will upload a file to the network. If the api reports that there are 292 | // more than 10 files successfully uploaded, then a file is deleted at random. 293 | func (r *renterJob) upload() error { 294 | r.jr.tg.Add() 295 | defer r.jr.tg.Done() 296 | 297 | // Generate some random data to upload. The file needs to be closed before 298 | // the upload to the network starts, so this code is wrapped in a func such 299 | // that a `defer Close()` can be used on the file. 300 | log.Printf("[INFO] [renter] [%v] File upload preparation beginning.\n", r.jr.siaDirectory) 301 | var sourcePath string 302 | var merkleRoot crypto.Hash 303 | success, err := func() (bool, error) { 304 | f, err := ioutil.TempFile(filepath.Join(r.jr.siaDirectory, "renterSourceFiles"), "renterFile") 305 | if err != nil { 306 | return false, fmt.Errorf("unable to open tmp file for renter source file: %v", err) 307 | } 308 | defer f.Close() 309 | sourcePath, _ = filepath.Abs(f.Name()) 310 | 311 | // Fill the file with random data. 312 | merkleRoot, err = randFillFile(f, uploadFileSize) 313 | if err != nil { 314 | return false, fmt.Errorf("unable to fill file with randomness: %v", err) 315 | } 316 | return true, nil 317 | }() 318 | if !success { 319 | return err 320 | } 321 | 322 | // use the sourcePath with its leading slash stripped for the sia path 323 | siapath := sourcePath[1:] 324 | if string(sourcePath[1]) == ":" { 325 | // looks like a Windows path - Cut Differently! 326 | siapath = sourcePath[3:] 327 | } 328 | 329 | // Add the file to the renter. 330 | rf := renterFile{ 331 | merkleRoot: merkleRoot, 332 | sourceFile: sourcePath, 333 | } 334 | r.mu.Lock() 335 | r.files = append(r.files, rf) 336 | r.mu.Unlock() 337 | log.Printf("[INFO] [renter] [%v] File upload preparation complete, beginning file upload.\n", r.jr.siaDirectory) 338 | 339 | // Upload the file to the network. 340 | if err := r.jr.client.RenterUploadPost(sourcePath, siapath, 10, 20); err != nil { 341 | return fmt.Errorf("unable to upload file to network: %v", err) 342 | } 343 | log.Printf("[INFO] [renter] [%v] /renter/upload call completed successfully. Waiting for the upload to complete\n", r.jr.siaDirectory) 344 | 345 | // Block until the upload has reached 100%. 346 | uploadProgress := 0.0 347 | for start := time.Now(); time.Since(start) < maxUploadTime; { 348 | select { 349 | case <-r.jr.tg.StopChan(): 350 | return nil 351 | case <-time.After(time.Second * 20): 352 | } 353 | 354 | rfg, err := r.jr.client.RenterFilesGet() 355 | if err != nil { 356 | return fmt.Errorf("error calling /renter/files: %v", err) 357 | } 358 | 359 | for _, file := range rfg.Files { 360 | if file.SiaPath == siapath { 361 | uploadProgress = file.UploadProgress 362 | } 363 | } 364 | log.Printf("[INFO] [renter] [%v]: upload progress: %v%%\n", r.jr.siaDirectory, uploadProgress) 365 | if uploadProgress == 100 { 366 | break 367 | } 368 | } 369 | if uploadProgress < 100 { 370 | return fmt.Errorf("file with siapath %v could not be fully uploaded after 10 minutes. progress reached: %v", siapath, uploadProgress) 371 | } 372 | log.Printf("[INFO] [renter] [%v]: file has been successfully uploaded to 100%%.\n", r.jr.siaDirectory) 373 | return nil 374 | } 375 | 376 | // storageRenter unlocks the wallet, mines some currency, sets an allowance 377 | // using that currency, and uploads some files. It will periodically try to 378 | // download or delete those files, printing any errors that occur. 379 | func (j *jobRunner) storageRenter() { 380 | j.tg.Add() 381 | defer j.tg.Done() 382 | 383 | // Block until a minimum threshold of coins have been mined. 384 | start := time.Now() 385 | var walletInfo api.WalletGET 386 | log.Printf("[INFO] [renter] [%v] Blocking until wallet is sufficiently full\n", j.siaDirectory) 387 | for walletInfo.ConfirmedSiacoinBalance.Cmp(requiredInitialBalance) < 0 { 388 | // Log an error if the time elapsed has exceeded the warning threshold. 389 | if time.Since(start) > initialBalanceWarningTimeout { 390 | log.Printf("[ERROR] [renter] [%v] Minimum balance for allowance has not been reached. Time elapsed: %v\n", j.siaDirectory, time.Since(start)) 391 | } 392 | 393 | // Wait before trying to get the balance again. 394 | select { 395 | case <-j.tg.StopChan(): 396 | return 397 | case <-time.After(time.Second * 15): 398 | } 399 | 400 | // Update the wallet balance. 401 | _, err := j.client.WalletGet() 402 | if err != nil { 403 | log.Printf("[ERROR] [renter] [%v] Trouble when calling /wallet: %v\n", j.siaDirectory, err) 404 | } 405 | } 406 | log.Printf("[INFO] [renter] [%v] Wallet filled successfully. Blocking until allowance has been set.\n", j.siaDirectory) 407 | 408 | // Block until a renter allowance has successfully been set. 409 | start = time.Now() 410 | for { 411 | log.Printf("[DEBUG] [renter] [%v] Attempting to set allowance.\n", j.siaDirectory) 412 | err := j.client.RenterPostAllowance(modules.Allowance{Funds: renterAllowance, Period: renterAllowancePeriod}) 413 | log.Printf("[DEBUG] [renter] [%v] Allowance attempt complete: %v\n", j.siaDirectory, err) 414 | if err == nil { 415 | // Success, we can exit the loop. 416 | break 417 | } 418 | if err != nil && time.Since(start) > setAllowanceWarningTimeout { 419 | log.Printf("[ERROR] [renter] [%v] Trouble when setting renter allowance: %v\n", j.siaDirectory, err) 420 | } 421 | 422 | // Wait a bit before trying again. 423 | select { 424 | case <-j.tg.StopChan(): 425 | return 426 | case <-time.After(time.Second * 15): 427 | } 428 | } 429 | log.Printf("[INFO] [renter] [%v] Renter allowance has been set successfully.\n", j.siaDirectory) 430 | 431 | // Spawn the uploader and downloader threads. 432 | rj := renterJob{ 433 | jr: j, 434 | } 435 | 436 | go rj.permanentUploader() 437 | go rj.permanentDownloader() 438 | go rj.permanentDeleter() 439 | } 440 | --------------------------------------------------------------------------------