├── .gitignore ├── faucet ├── _cert │ └── .gitkeep ├── .gitignore ├── static │ ├── background.jpg │ ├── assets │ │ └── favicon.ico │ ├── js │ │ └── scripts.js │ └── index.html ├── pkg │ └── version │ │ └── version.go ├── internal │ ├── data │ │ ├── fund.go │ │ └── hello.go │ ├── platform │ │ ├── web │ │ │ ├── web.go │ │ │ └── error.go │ │ └── lotus │ │ │ └── lotus.go │ ├── itests │ │ ├── detector_test.go │ │ ├── kit │ │ │ └── fake_lotus.go │ │ ├── health_test.go │ │ └── faucet_test.go │ ├── http │ │ ├── http.go │ │ ├── faucet.go │ │ └── health.go │ ├── db │ │ ├── db_test.go │ │ └── db.go │ ├── failure │ │ └── failure.go │ └── faucet │ │ └── faucet.go ├── .golangci.yaml ├── README.md ├── Makefile ├── cmd │ ├── health │ │ └── main.go │ └── faucet │ │ └── main.go └── go.mod ├── deployment ├── scripts │ ├── setup.sh │ ├── custom.sh │ ├── connect-daemon.sh │ ├── kill.sh │ ├── generate-validator-identity.sh │ ├── install-go.sh │ ├── start-validator.sh │ ├── start-health-monitoring.sh │ ├── start-faucet.sh │ ├── start-daemon.sh │ ├── start-bootstrap.sh │ └── generate-membership.py ├── status.yaml ├── fetch-logs.yaml ├── custom-script.yaml ├── stop-monitoring.yaml ├── clean.yaml ├── start-faucet.yaml ├── kill.yaml ├── start-monitoring.yaml ├── deep-clean.yaml ├── update-faucet.yaml ├── deploy-new.yaml ├── start-daemons.yaml ├── rolling-update.sh ├── restart-validators.yaml ├── deploy-current.yaml ├── connect-daemons.yaml ├── start-bootstrap.yaml ├── start-validators.yaml ├── rotate-logs.sh ├── group_vars │ └── all.yaml ├── update-nodes.yaml ├── setup.yaml ├── spacenet_template.json └── README.md ├── assets ├── bootstrap-config.toml ├── spacenet-faucet.png └── spacenet-header.png ├── .github ├── workflows │ ├── add-bug-tracker.yaml │ ├── lint.yml │ ├── static.yml │ └── test.yml └── ISSUE_TEMPLATE │ └── bug-report.yaml ├── README.md └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | .idea -------------------------------------------------------------------------------- /faucet/_cert/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /faucet/.gitignore: -------------------------------------------------------------------------------- 1 | _db_data/ 2 | .idea 3 | _cert/*.pem 4 | _cert/*.key -------------------------------------------------------------------------------- /deployment/scripts/setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Nothing to set up for now. -------------------------------------------------------------------------------- /assets/bootstrap-config.toml: -------------------------------------------------------------------------------- 1 | [Libp2p] 2 | ListenAddresses = ["/ip4/0.0.0.0/tcp/1347"] 3 | -------------------------------------------------------------------------------- /assets/spacenet-faucet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/consensus-shipyard/spacenet/HEAD/assets/spacenet-faucet.png -------------------------------------------------------------------------------- /assets/spacenet-header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/consensus-shipyard/spacenet/HEAD/assets/spacenet-header.png -------------------------------------------------------------------------------- /faucet/static/background.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/consensus-shipyard/spacenet/HEAD/faucet/static/background.jpg -------------------------------------------------------------------------------- /faucet/static/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/consensus-shipyard/spacenet/HEAD/faucet/static/assets/favicon.ico -------------------------------------------------------------------------------- /deployment/scripts/custom.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Write any ad-hoc script here and run it using the custom-script.yaml playbook 4 | -------------------------------------------------------------------------------- /faucet/pkg/version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | var ( 4 | gittag = "unk" 5 | ) 6 | 7 | func Version() string { 8 | return gittag 9 | } 10 | -------------------------------------------------------------------------------- /deployment/scripts/connect-daemon.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd lotus || exit 4 | 5 | while IFS="" read -r addr || [ -n "$addr" ]; do # Read all lotus addresses from the provided file, liney by line 6 | if [ "$addr" != "$(cat ../.lotus/lotus-addr)" ]; then # Skip own address 7 | ./eudico net connect "$addr" || exit # Connect to each other address 8 | fi 9 | done < ../.lotus/lotus-addrs 10 | -------------------------------------------------------------------------------- /deployment/status.yaml: -------------------------------------------------------------------------------- 1 | # Check the statuses of the Eudico nodes. 2 | 3 | --- 4 | - name: Check node status 5 | hosts: all 6 | gather_facts: False 7 | become: False 8 | tasks: 9 | 10 | - name: Check the statuses of the nodes 11 | shell: 'cd lotus && ./eudico status && echo "Chain head: `./eudico chain head`" | cat' 12 | register: out 13 | - debug: 14 | msg: "{{ out.stdout | split('\n') | flatten }}" 15 | ... -------------------------------------------------------------------------------- /faucet/internal/data/fund.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import "time" 4 | 5 | type FundRequest struct { 6 | Address string `json:"address"` 7 | } 8 | 9 | type AddrInfo struct { 10 | Amount uint64 `json:"amount"` 11 | LatestWithdrawal time.Time `json:"latest_withdrawal"` 12 | } 13 | 14 | type TotalInfo struct { 15 | Amount uint64 `json:"amount"` 16 | LatestWithdrawal time.Time `json:"latest_withdrawal"` 17 | } 18 | -------------------------------------------------------------------------------- /deployment/fetch-logs.yaml: -------------------------------------------------------------------------------- 1 | # Fetches logs from all hosts. 2 | # 3 | # Applies to all hosts by default, unless other nodes are specified using --extra-vars "nodes=..." 4 | 5 | --- 6 | - name: Fetch logs 7 | hosts: "{{nodes | default('all')}}" 8 | gather_facts: False 9 | become: False 10 | tasks: 11 | 12 | - name: Download logs from host 13 | ansible.builtin.synchronize: src=spacenet-logs dest=fetched-logs/{{ inventory_hostname }} mode=pull 14 | ... 15 | -------------------------------------------------------------------------------- /deployment/custom-script.yaml: -------------------------------------------------------------------------------- 1 | # Runs the scripts/custom.sh script. This is meant as a convenience tool for executing ad-hoc scripts. 2 | # 3 | # Applies to all hosts by default, unless other nodes are specified using --extra-vars "nodes=..." 4 | 5 | --- 6 | - name: Run custom script 7 | hosts: "{{nodes | default('all')}}" 8 | gather_facts: False 9 | become: False 10 | tasks: 11 | - name: "Run custom script" 12 | ansible.builtin.script: 13 | cmd: scripts/custom.sh 14 | ... -------------------------------------------------------------------------------- /deployment/scripts/kill.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | killall eudico 4 | killall spacenet-faucet 5 | killall spacenet-health 6 | tmux kill-server 7 | 8 | sleep 3 9 | 10 | killall -9 eudico 11 | killall -9 spacenet-faucet 12 | killall -9 spacenet-health 13 | tmux kill-server 14 | 15 | # Some of the above commands inevitably fail, as not all processes are running on all machines. 16 | # This will prevent Ansible (through which this script is expected to be run) from complaining about it. 17 | true 18 | -------------------------------------------------------------------------------- /.github/workflows/add-bug-tracker.yaml: -------------------------------------------------------------------------------- 1 | name: Add bugs to tracker 2 | 3 | on: 4 | issues: 5 | types: 6 | - opened 7 | - labeled 8 | 9 | jobs: 10 | add-to-project: 11 | name: Add issue to tracker 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/add-to-project@v0.5.0 15 | with: 16 | project-url: https://github.com/orgs/consensus-shipyard/projects/3 17 | github-token: ${{ secrets.ADD_TO_PROJECT_PAT }} 18 | labeled: bug -------------------------------------------------------------------------------- /deployment/stop-monitoring.yaml: -------------------------------------------------------------------------------- 1 | # Stops the health monitoring service. 2 | # 3 | # Applies to all hosts by default, unless other nodes are specified using --extra-vars "nodes=..." 4 | 5 | --- 6 | - name: Stop health monitoring 7 | hosts: "{{nodes | default('all')}}" 8 | gather_facts: False 9 | become: False 10 | tasks: 11 | 12 | - name: Stop monitoring service 13 | shell: "killall spacenet-health; sleep 3; killall -9 spacenet-health; tmux kill-session -t health; true" 14 | ... 15 | -------------------------------------------------------------------------------- /deployment/clean.yaml: -------------------------------------------------------------------------------- 1 | # Kills running Lotus daemon and Mir validator and deletes their associated state. 2 | # Does not touch the code and binaries. 3 | # 4 | # Applies to all hosts by default, unless other nodes are specified using --extra-vars "nodes=..." 5 | 6 | --- 7 | - import_playbook: kill.yaml 8 | 9 | - name: Delete the whole lotus state 10 | hosts: "{{nodes | default('all')}}" 11 | gather_facts: False 12 | become: False 13 | tasks: 14 | - name: "Delete the .lotus repo directory" 15 | file: 16 | state: absent 17 | path: ~/.lotus 18 | ... -------------------------------------------------------------------------------- /faucet/internal/data/hello.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | type LivenessResponse struct { 4 | LotusVersion string `json:"lotus_version"` 5 | Build string `json:"build"` 6 | Epoch uint64 `json:"epoch"` 7 | Behind uint64 `json:"behind"` 8 | PeerNumber int `json:"peer_number"` 9 | Host string `json:"host"` 10 | PeersToPublishMsgs int `json:"peers_to_publish_msgs"` 11 | PeersToPublishBlocks int `json:"peers_to_publish_blocks"` 12 | PeerID string `json:"peer_id"` 13 | ServiceVersion string `json:"service_version"` 14 | } 15 | -------------------------------------------------------------------------------- /deployment/start-faucet.yaml: -------------------------------------------------------------------------------- 1 | # Starts the faucet service that can be used to distribute coins. 2 | # Assumes that the bootstrap node is up and running (see start-bootstrap.yaml). 3 | # 4 | # Applies to the first bootstrap host by default, unless other nodes are specified using --extra-vars "nodes=..." 5 | 6 | 7 | --- 8 | - name: Start Faucet 9 | hosts: "{{nodes | default('bootstrap[0]')}}" 10 | gather_facts: False 11 | become: False 12 | tasks: 13 | 14 | - name: Start Faucet server 15 | ansible.builtin.script: 16 | cmd: scripts/start-faucet.sh '{{ log_file_lines }}' '{{ max_log_archive_size }}' 17 | ... 18 | -------------------------------------------------------------------------------- /deployment/kill.yaml: -------------------------------------------------------------------------------- 1 | # Kills running Lotus daemon and Mir validator. 2 | # Does not touch their persisted state or the code and binaries. 3 | # Reports but ignores errors, so it can be used even if the processes to be killed are not running. 4 | # 5 | # Applies to all hosts by default, unless other nodes are specified using --extra-vars "nodes=..." 6 | 7 | --- 8 | - name: Stop all nodes and delete their state 9 | hosts: "{{nodes | default('all')}}" 10 | gather_facts: False 11 | become: False 12 | tasks: 13 | 14 | - name: "Execute kill script" 15 | ansible.builtin.script: 16 | cmd: scripts/kill.sh 17 | ignore_errors: True 18 | ... -------------------------------------------------------------------------------- /deployment/start-monitoring.yaml: -------------------------------------------------------------------------------- 1 | # Starts the health monitoring service that can be used to check the status of the system. 2 | # Assumes that all the nodes (bootstraps, daemons, and validators) are up and running. 3 | # 4 | # Applies to all hosts by default, unless other nodes are specified using --extra-vars "nodes=..." 5 | 6 | --- 7 | - name: Start health monitoring 8 | hosts: "{{nodes | default('all')}}" 9 | gather_facts: False 10 | become: False 11 | tasks: 12 | 13 | - name: Start monitoring service 14 | ansible.builtin.script: 15 | cmd: scripts/start-health-monitoring.sh '{{ log_file_lines }}' '{{ max_log_archive_size }}' 16 | ... 17 | -------------------------------------------------------------------------------- /deployment/scripts/generate-validator-identity.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd lotus || exit 4 | 5 | # Create a new wallet to be used by the validator 6 | ./eudico wallet new || exit 7 | 8 | # Initialize a new configuration for the mir validator. 9 | # This will create mir-related config files in the $LOTUS_PATH directory. 10 | export LOTUS_PATH=/home/ubuntu/.lotus 11 | ./eudico mir validator config init || exit 12 | 13 | # Get the libp2p address of the local lotus node 14 | lotus_listen_addr=$(./eudico mir validator config validator-addr | grep -vE '(/ip6/)|(127.0.0.1)' | grep -E '/ip4/.*/tcp/1347') 15 | 16 | echo "${lotus_listen_addr}" > ~/.lotus/mir-validator-identity 17 | -------------------------------------------------------------------------------- /faucet/.golangci.yaml: -------------------------------------------------------------------------------- 1 | linters: 2 | disable-all: true 3 | enable: 4 | - asciicheck 5 | - dogsled 6 | - dupl 7 | - errcheck 8 | - errorlint 9 | - exportloopref 10 | - gocognit 11 | - goconst 12 | - gocyclo 13 | - gofmt 14 | - goimports 15 | - gosec 16 | - gosimple 17 | - govet 18 | - ineffassign 19 | - misspell 20 | - nakedret 21 | - prealloc 22 | - staticcheck 23 | - stylecheck 24 | - unconvert 25 | - unused 26 | 27 | linters-settings: 28 | goimports: 29 | local-prefixes: github.com/filecoin-project/ 30 | gocognit: 31 | min-complexity: 50 32 | 33 | run: 34 | timeout: 5m 35 | -------------------------------------------------------------------------------- /deployment/deep-clean.yaml: -------------------------------------------------------------------------------- 1 | # Performs deep cleaning of the host machines. 2 | # Runs clean.yaml and, in addition, deletes the cloned repository with the lotus code and binaries. 3 | # 4 | # Applies to all hosts by default, unless other nodes are specified using --extra-vars "nodes=..." 5 | 6 | --- 7 | - import_playbook: clean.yaml 8 | 9 | - name: Delete the whole lotus state 10 | hosts: "{{nodes | default('all')}}" 11 | gather_facts: False 12 | become: False 13 | tasks: 14 | - name: Remove the cloned code repositories 15 | file: 16 | path: "{{ item }}" 17 | state: absent 18 | with_items: 19 | - ~/lotus 20 | - ~/spacenet 21 | - ~/spacenet-logs 22 | ... 23 | -------------------------------------------------------------------------------- /deployment/update-faucet.yaml: -------------------------------------------------------------------------------- 1 | # Updates the faucet repo. 2 | # This script suggests that the environment for the faucet and monitoring is ready 3 | # and contains all dependencies. 4 | 5 | --- 6 | - name: Update Faucet 7 | hosts: "{{nodes | default('all')}}" 8 | gather_facts: False 9 | become: False 10 | environment: 11 | PATH: "{{ ansible_env.PATH }}:/home/{{ ansible_user }}/go/bin" 12 | tasks: 13 | 14 | - name: "Pull from git: {{ spacenet_git_version }}" 15 | ansible.builtin.git: 16 | repo: "{{ spacenet_git_repo }}" 17 | dest: ~/spacenet 18 | single_branch: True 19 | version: "{{ spacenet_git_version }}" 20 | force: True 21 | update: True 22 | ... 23 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Linting 2 | on: 3 | push: 4 | branches: [main] 5 | pull_request: 6 | branches: [main] 7 | jobs: 8 | golangci: 9 | name: lint 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Set up Golang 13 | uses: actions/setup-go@v4 14 | with: 15 | go-version: 1.18 16 | check-latest: true 17 | 18 | - name: Checkout 19 | uses: actions/checkout@v3 20 | 21 | - name: golangci-lint 22 | uses: golangci/golangci-lint-action@v3 23 | with: 24 | working-directory: faucet 25 | # Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version 26 | version: latest 27 | -------------------------------------------------------------------------------- /deployment/scripts/install-go.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # THIS IS A DIRTY HACK 4 | # It is tested on Ubuntu Linux 22.04. 5 | # No guarantees for other systems. 6 | # Anyway, when the required and available versions of Go change (as they do all the time), 7 | # this script will probably not be necessary and we'll be able to install Go using apt. 8 | # Only when writing this, the apt version of Go was outdated. 9 | 10 | wget https://go.dev/dl/go1.19.7.linux-amd64.tar.gz 11 | sudo rm -rf /usr/local/go && sudo tar -C /usr/local -xzf go1.19.7.linux-amd64.tar.gz 12 | sudo rm /usr/bin/go 13 | sudo ln -s /usr/local/go/bin/go /usr/bin/go 14 | sudo rm /usr/lib/go 15 | sudo ln -s /usr/local/go /usr/lib/go 16 | sudo rm /usr/share/go 17 | sudo ln -s /usr/local/go /usr/share/go 18 | -------------------------------------------------------------------------------- /deployment/deploy-new.yaml: -------------------------------------------------------------------------------- 1 | # Deploys the whole system from scratch. 2 | # Performs a deep clean by running deep-clean.yaml 3 | # (potentially producing and ignoring some errors, if nothing is running on the hosts - this is normal) 4 | # and sets up a new Spacenet deployment. 5 | # 6 | # The nodes variable must not be set, as this playbook must distinguish between different kinds of nodes 7 | # (such as bootstrap and validators). 8 | 9 | --- 10 | - hosts: all 11 | gather_facts: False 12 | tasks: 13 | - name: Verify that nodes variable is not defined 14 | fail: msg="Variable nodes must not be defined (nodes set to '{{ nodes }}')" 15 | when: nodes is defined 16 | 17 | - import_playbook: deep-clean.yaml 18 | - import_playbook: setup.yaml 19 | - import_playbook: deploy-current.yaml 20 | ... 21 | -------------------------------------------------------------------------------- /deployment/start-daemons.yaml: -------------------------------------------------------------------------------- 1 | # Starts the Lotus daemons and creates connections among them and to the bootstrap node. 2 | # Assumes that the bootstrap node is up and running (see start-bootstrap.yaml). 3 | # 4 | # Applies to the validator host by default, unless other nodes are specified using --extra-vars "nodes=..." 5 | 6 | --- 7 | - name: Start Lotus daemons 8 | hosts: "{{nodes | default('validators')}}" 9 | gather_facts: False 10 | become: False 11 | vars: 12 | bootstrap_identities: "{{ lookup('file', 'bootstrap-identities') }}" 13 | tasks: 14 | 15 | - name: Start Lotus daemons 16 | ansible.builtin.script: 17 | cmd: scripts/start-daemon.sh '{{ bootstrap_identities }}' '{{ log_file_lines }}' '{{ max_log_archive_size }}' 18 | 19 | - import_playbook: connect-daemons.yaml 20 | ... 21 | -------------------------------------------------------------------------------- /faucet/README.md: -------------------------------------------------------------------------------- 1 | # Faucet 2 | 3 | ## How to Run 4 | ### Enabled TLS 5 | ```azure 6 | go run ./cmd/main.go --tls-enabled --web-allowed-origins "https://frontend" --web-backend-host "https://faucet/fund" --filecoin-address "address" --tls-cert-file "path_to_cert.pem" --tls-key-file "path_to_key.pem" 7 | ``` 8 | ### Disabled TLS 9 | 10 | ```azure 11 | go run ./cmd/main.go --web-allowed-origins "http://frontend" --web-backend-host "https://faucet/fund" --filecoin-address "address" 12 | ``` 13 | 14 | ## Development 15 | To run the service even in the development mode, you must provide an X509 certificate. 16 | 17 | The easiest way to do that is to use [mkcert](https://github.com/FiloSottile/mkcert) 18 | tool and `make cert` command. 19 | 20 | The run `make all` to ensure that tests pass and `make demo` to run a demo accessible on localhost. -------------------------------------------------------------------------------- /deployment/scripts/start-validator.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Obtain number of lines per log file. 4 | log_file_lines="$1" 5 | [ "${log_file_lines}" -gt 0 ] || exit 6 | shift 7 | 8 | # Make sure that the maximal log archive size (in bytes) has been properly specified. 9 | max_archive_size=$1 10 | [ "${max_archive_size}" -gt 0 ] || exit 11 | shift 12 | 13 | cd lotus || exit 14 | 15 | # Create log directory 16 | log_dir=~/spacenet-logs/validator-$(date +%Y-%m-%d-%H-%M-%S_%Z) 17 | mkdir -p "$log_dir" 18 | 19 | # Kill a potentially running validator. 20 | tmux kill-session -t mir-validator 21 | tmux new-session -d -s mir-validator 22 | 23 | # Start the Mir validator. 24 | tmux send-keys "LOTUS_PATH=/home/ubuntu/.lotus ./eudico mir validator run --nosync --max-block-delay=15s 2>&1 | ./rotate-logs.sh ${log_dir} ${log_file_lines} ${max_archive_size}" C-m 25 | -------------------------------------------------------------------------------- /faucet/internal/platform/web/web.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "net/http" 7 | ) 8 | 9 | func Decode(r *http.Request, val interface{}) error { 10 | decoder := json.NewDecoder(r.Body) 11 | if err := decoder.Decode(val); err != nil { 12 | return NewRequestError(err, http.StatusBadRequest) 13 | } 14 | return nil 15 | } 16 | 17 | func Respond(ctx context.Context, w http.ResponseWriter, data any, statusCode int) error { 18 | if statusCode == http.StatusNoContent { 19 | w.WriteHeader(statusCode) 20 | return nil 21 | } 22 | 23 | jsonData, err := json.Marshal(data) 24 | if err != nil { 25 | return err 26 | } 27 | 28 | w.Header().Set("Content-Type", "application/json") 29 | w.WriteHeader(statusCode) 30 | 31 | if _, err := w.Write(jsonData); err != nil { 32 | return err 33 | } 34 | 35 | return nil 36 | } 37 | -------------------------------------------------------------------------------- /deployment/rolling-update.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This script performs a rolling update of selected nodes from an Ansible inventory. 4 | # 5 | # usage: rolling-update inventory_file node1 [node2 [...]] 6 | # 7 | # The first argument must be an Ansible inventory file that contains all the node arguments that follow. 8 | # This script updates (fetches the code, recompiles it, and restarts the node, using the update-nodes.yaml) 9 | # the nodes one by one, always waiting for a node to catch up with the others 10 | # and only then proceeding to updating the next one. 11 | 12 | inventory="$1" 13 | shift 14 | 15 | while [ -n "$1" ]; do 16 | echo -e "\n========================================" 17 | echo "Updating node: $1" 18 | echo -e "========================================\n" 19 | ansible-playbook -i "$inventory" update-nodes.yaml --extra-vars "nodes=$1" || exit 20 | shift 21 | done -------------------------------------------------------------------------------- /deployment/restart-validators.yaml: -------------------------------------------------------------------------------- 1 | # Restarts a given set of validators. 2 | # For safety, does NOT default to restarting all validators 3 | # and the set of hosts to restart must be explicitly given using --extra-vars "nodes=..." 4 | # 5 | # Note that this playbook always affects all hosts, regardless of the value of the nodes variable. 6 | # This is due to the necessity of reconnecting all daemons to the restarted one. 7 | 8 | --- 9 | - name: Make sure nodes are specified explicitly 10 | hosts: "{{ nodes }}" 11 | gather_facts: False 12 | tasks: 13 | 14 | 15 | - import_playbook: kill.yaml 16 | - import_playbook: start-daemons.yaml 17 | 18 | 19 | - name: Start only the specified validators 20 | hosts: "{{ nodes }}" 21 | gather_facts: False 22 | tasks: 23 | - name: Start validators 24 | ansible.builtin.script: 25 | cmd: scripts/start-validator.sh '{{ log_file_lines }}' '{{ max_log_archive_size }}' 26 | ... -------------------------------------------------------------------------------- /deployment/deploy-current.yaml: -------------------------------------------------------------------------------- 1 | # Deploys the bootstrap and the validators using existing binaries. 2 | # This playbook still cleans the lotus daemon state, but neither updates nor recompiles the code. 3 | # Performs the state cleanup by running clean.yaml, 4 | # potentially producing and ignoring some errors, if nothing is running on the hosts - this is normal. 5 | # 6 | # The nodes variable must not be set, as this playbook must distinguish between different kinds of nodes 7 | # (such as bootstrap and validators). 8 | 9 | --- 10 | - hosts: all 11 | gather_facts: False 12 | tasks: 13 | - name: Verify that nodes variable is not defined 14 | fail: msg="Variable nodes must not be defined (nodes set to '{{ nodes }}')" 15 | when: nodes is defined 16 | 17 | - import_playbook: clean.yaml 18 | - import_playbook: start-bootstrap.yaml 19 | - import_playbook: start-daemons.yaml 20 | - import_playbook: start-validators.yaml 21 | - import_playbook: start-faucet.yaml 22 | - import_playbook: start-monitoring.yaml 23 | ... 24 | -------------------------------------------------------------------------------- /deployment/scripts/start-health-monitoring.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Obtain number of lines per log file. 4 | log_file_lines="$1" 5 | [ "${log_file_lines}" -gt 0 ] || exit 6 | shift 7 | 8 | # Obtain maximal log archive size (in bytes) 9 | max_archive_size=$1 10 | [ "${max_archive_size}" -gt 0 ] || exit 11 | shift 12 | 13 | cd lotus || exit 14 | 15 | # Create log directories 16 | health_log_dir=~/spacenet-logs/health-$(date +%Y-%m-%d-%H-%M-%S_%Z) 17 | mkdir -p "$health_log_dir" 18 | 19 | tag=$(git describe --tags 2>/dev/null || echo "unk-$(git rev-parse --short=10 HEAD)") 20 | flags="-X=github.com/filecoin-project/faucet/pkg/version.gittag=${tag}" 21 | 22 | # Start the Hello service. 23 | cd ~/spacenet/faucet/ || exit 24 | go build -o spacenet-health -ldflags "$flags" ./cmd/health || exit 25 | tmux new-session -d -s health 26 | tmux send-keys "export LOTUS_PATH=~/.lotus && ./spacenet-health --web-host \"0.0.0.0:9000\" --lotus-api-host=127.0.0.1:1234 2>&1 | ~/lotus/rotate-logs.sh ${health_log_dir} ${log_file_lines} ${max_archive_size}" C-m 27 | -------------------------------------------------------------------------------- /deployment/connect-daemons.yaml: -------------------------------------------------------------------------------- 1 | # Connects all Lotus daemons to each other. This is required for the nodes to be able to sync their state. 2 | # It assumes the daemons and the bootstrap are up and running (but not necessarily the validators) 3 | 4 | --- 5 | - name: Connect lotus daemons to each other 6 | hosts: all 7 | gather_facts: False 8 | become: False 9 | tasks: 10 | 11 | - name: Collect Lotus daemon addresses 12 | ansible.builtin.fetch: 13 | src: .lotus/lotus-addr 14 | dest: tmp-lotus-addrs 15 | 16 | 17 | - name: Combine Lotus daemon addresses into a single file 18 | run_once: True 19 | delegate_to: localhost 20 | shell: 'rm -f lotus-addrs && cat tmp-lotus-addrs/*/.lotus/lotus-addr >> lotus-addrs && rm -r tmp-lotus-addrs' 21 | 22 | 23 | - name: Copy Lotus daemon address file to all nodes 24 | ansible.builtin.copy: 25 | src: lotus-addrs 26 | dest: .lotus/ 27 | 28 | 29 | - name: Connect all Lotus daemons to each other 30 | ansible.builtin.script: 31 | cmd: scripts/connect-daemon.sh 32 | ... 33 | -------------------------------------------------------------------------------- /faucet/Makefile: -------------------------------------------------------------------------------- 1 | all: tidy format lint test 2 | 3 | .PHONY: tidy 4 | tidy: 5 | go mod tidy 6 | 7 | .PHONY: test 8 | test: 9 | go test -v -shuffle=on -count=1 -race -timeout 20m ./... 10 | 11 | .PHONY: format 12 | format: 13 | gofmt -w -s . 14 | goimports -w -local "github.com/filecoin-project/" . 15 | 16 | .PHONY: lint 17 | lint: 18 | golangci-lint run ./... 19 | 20 | .PHONY: vulncheck 21 | vulncheck: 22 | govulncheck -v ./... 23 | 24 | .PHONY: cert 25 | cert: 26 | mkcert -cert-file "./_cert/cert.pem" -key-file "./_cert/key.pem" faucet.com 127.0.0.1 localhost 27 | 28 | .PHONY: demo 29 | demo: 30 | rm -rf ./_db_data 31 | go run ./cmd/main.go --web-host "127.0.0.1:8000" --web-allowed-origins "http://localhost:8000" --web-backend-host "http://localhost:8000/fund" --filecoin-address "f1cp4q4lqsdhob23ysywffg2tvbmar5cshia4rweq" 32 | 33 | .PHONY: demo-tls 34 | demo-tls: 35 | rm -rf ./_db_data 36 | go run ./cmd/main.go --web-allowed-origins "https://localhost" --web-backend-host "https://localhost:443/fund" --filecoin-address "f1cp4q4lqsdhob23ysywffg2tvbmar5cshia4rweq" --tls-cert-file "./_cert/cert.pem" --tls-key-file "./_cert/key.pem" 37 | 38 | .PHONY: health 39 | health: 40 | go run ./cmd/health/main.go --web-host "127.0.0.1:9000" -------------------------------------------------------------------------------- /faucet/internal/platform/web/error.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | ) 7 | 8 | type Error struct { 9 | Err error 10 | Status int 11 | } 12 | 13 | type HTMLError struct { 14 | Err error 15 | Status int 16 | } 17 | 18 | type ErrorResponse struct { 19 | Error string `json:"message,omitempty"` 20 | } 21 | 22 | func (err *Error) Error() string { 23 | return err.Err.Error() 24 | } 25 | 26 | func (err *HTMLError) Error() string { 27 | return err.Err.Error() 28 | } 29 | 30 | func NewRequestError(err error, status int) error { 31 | return &Error{err, status} 32 | } 33 | 34 | func NewResponseError(err error, status int) error { 35 | return &Error{err, status} 36 | } 37 | 38 | func NewHTMLError(err error, status int) error { 39 | return &HTMLError{err, status} 40 | } 41 | 42 | func RespondError(w http.ResponseWriter, status int, err error) { 43 | w.Header().Set("Content-Type", "application/json") 44 | w.WriteHeader(status) 45 | 46 | type ErrorResponse struct { 47 | Errors []string `json:"errors"` 48 | } 49 | resp := &ErrorResponse{Errors: make([]string, 0, 1)} 50 | if err != nil { 51 | resp.Errors = append(resp.Errors, err.Error()) 52 | } 53 | 54 | enc := json.NewEncoder(w) 55 | enc.Encode(resp) // nolint 56 | } 57 | -------------------------------------------------------------------------------- /faucet/internal/platform/lotus/lotus.go: -------------------------------------------------------------------------------- 1 | package lotus 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "path" 8 | 9 | "github.com/libp2p/go-libp2p/core/peer" 10 | 11 | "github.com/filecoin-project/go-address" 12 | "github.com/filecoin-project/lotus/api" 13 | "github.com/filecoin-project/lotus/api/v1api" 14 | ) 15 | 16 | type API interface { 17 | NodeStatus(ctx context.Context, inclChainStatus bool) (api.NodeStatus, error) 18 | NetPeers(context.Context) ([]peer.AddrInfo, error) 19 | Version(context.Context) (api.APIVersion, error) 20 | ID(context.Context) (peer.ID, error) 21 | } 22 | 23 | func GetToken() (string, error) { 24 | lotusPath := os.Getenv("LOTUS_PATH") 25 | fmt.Println("LOTUS_PATH=", lotusPath) 26 | if lotusPath == "" { 27 | return "", fmt.Errorf("LOTUS_PATH not set in environment") 28 | } 29 | token, err := os.ReadFile(path.Join(lotusPath, "/token")) 30 | return string(token), err 31 | } 32 | 33 | func VerifyWallet(ctx context.Context, api v1api.FullNode, addr address.Address) error { 34 | l, err := api.WalletList(ctx) 35 | if err != nil { 36 | return err 37 | } 38 | 39 | for _, w := range l { 40 | if w == addr { 41 | return nil 42 | } 43 | } 44 | return fmt.Errorf("faucet wallet not owned by peer targeted by faucet server") 45 | } 46 | -------------------------------------------------------------------------------- /.github/workflows/static.yml: -------------------------------------------------------------------------------- 1 | # Simple workflow for deploying static content to GitHub Pages 2 | name: Deploy static content to Pages 3 | 4 | on: 5 | # Runs on pushes targeting the default branch 6 | push: 7 | branches: ["main"] 8 | 9 | # Allows you to run this workflow manually from the Actions tab 10 | workflow_dispatch: 11 | 12 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 13 | permissions: 14 | contents: read 15 | pages: write 16 | id-token: write 17 | 18 | # Allow one concurrent deployment 19 | concurrency: 20 | group: "pages" 21 | cancel-in-progress: true 22 | 23 | jobs: 24 | # Single deploy job since we're just deploying 25 | deploy: 26 | environment: 27 | name: github-pages 28 | url: ${{ steps.deployment.outputs.page_url }} 29 | runs-on: ubuntu-latest 30 | steps: 31 | - name: Checkout 32 | uses: actions/checkout@v3 33 | - name: Setup Pages 34 | uses: actions/configure-pages@v2 35 | - name: Upload artifact 36 | uses: actions/upload-pages-artifact@v1 37 | with: 38 | # Upload entire repository 39 | path: './faucet/static' 40 | - name: Deploy to GitHub Pages 41 | id: deployment 42 | uses: actions/deploy-pages@v1 43 | -------------------------------------------------------------------------------- /deployment/start-bootstrap.yaml: -------------------------------------------------------------------------------- 1 | # Starts the bootstrap node and downloads its identity to localhost (using setup.yaml). 2 | # 3 | # Applies to the bootstrap host by default, unless other nodes are specified using --extra-vars "nodes=..." 4 | 5 | --- 6 | - name: Start bootstrap node 7 | hosts: "{{nodes | default('bootstrap')}}" 8 | gather_facts: False 9 | become: False 10 | tasks: 11 | 12 | - name: Copy keys to remote machine 13 | ansible.builtin.copy: 14 | src: "{{ item }}" 15 | dest: "lotus/{{ item }}" 16 | with_items: 17 | - spacenet-libp2p-bootstrap-{{ inventory_hostname }}.keyinfo 18 | - spacenet_faucet.key 19 | 20 | 21 | - name: Start bootstrap node 22 | ansible.builtin.script: 23 | cmd: scripts/start-bootstrap.sh spacenet-libp2p-bootstrap-{{ inventory_hostname }}.keyinfo '{{ log_file_lines }}' '{{ max_log_archive_size }}' 24 | 25 | 26 | - name: Collect bootstrap node identities 27 | ansible.builtin.fetch: 28 | src: .lotus/lotus-addr 29 | dest: tmp-bootstrap-identities 30 | 31 | 32 | - name: Combine bootstrap node identities into a single file 33 | run_once: True 34 | delegate_to: localhost 35 | shell: 'rm -f bootstrap-identities && cat tmp-bootstrap-identities/*/.lotus/lotus-addr >> bootstrap-identities && rm -r tmp-bootstrap-identities' 36 | ... 37 | -------------------------------------------------------------------------------- /deployment/scripts/start-faucet.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Obtain number of lines per log file. 4 | log_file_lines="$1" 5 | [ "${log_file_lines}" -gt 0 ] || exit 6 | shift 7 | 8 | # Obtain maximal log archive size (in bytes). 9 | max_archive_size=$1 10 | [ "${max_archive_size}" -gt 0 ] || exit 11 | shift 12 | 13 | # Create log directory. 14 | faucet_log_dir=~/spacenet-logs/faucet-$(date +%Y-%m-%d-%H-%M-%S_%Z) 15 | mkdir -p "$faucet_log_dir" 16 | 17 | # Kill a potentially running instance of the faucet. 18 | tmux kill-session -t faucet 19 | 20 | # Import faucet key to Eudico (he faucet's address has the coins that will be distributed). 21 | cd lotus || exit 22 | ./eudico wallet import --as-default --format=json-lotus spacenet_faucet.key 23 | 24 | tag=$(git describe --tags 2>/dev/null || echo "unk-$(git rev-parse --short=10 HEAD)") 25 | flags="-X=github.com/filecoin-project/faucet/pkg/version.gittag=${tag}" 26 | 27 | # Start the Faucet. 28 | cd ~/spacenet/faucet/ || exit 29 | go build -o spacenet-faucet -ldflags "$flags" ./cmd/faucet || exit 30 | tmux new-session -d -s faucet 31 | tmux send-keys "export LOTUS_PATH=~/.lotus && ./spacenet-faucet --web-host \"0.0.0.0:8000\" --web-allowed-origins \"*\" --web-backend-host \"https://spacenet.consensus.ninja/fund\" --filecoin-address=t1jlm55oqkdalh2l3akqfsaqmpjxgjd36pob34dqy --lotus-api-host=127.0.0.1:1234 2>&1 | ~/lotus/rotate-logs.sh ${faucet_log_dir} ${log_file_lines} ${max_archive_size}" C-m 32 | -------------------------------------------------------------------------------- /deployment/scripts/start-daemon.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Obtain bootstrap node's address as a parameter. 4 | bootstrap_addrs="$1" 5 | [ -n "$bootstrap_addrs" ] || exit 6 | shift 7 | 8 | # Obtain number of lines per log file. 9 | log_file_lines="$1" 10 | [ "${log_file_lines}" -gt 0 ] || exit 11 | shift 12 | 13 | # Make sure that the maximal log archive size (in bytes) has been properly specified. 14 | max_archive_size=$1 15 | [ "${max_archive_size}" -gt 0 ] || exit 16 | shift 17 | 18 | 19 | cd lotus || exit 20 | 21 | # Create log directory 22 | log_dir=~/spacenet-logs/daemon-$(date +%Y-%m-%d-%H-%M-%S_%Z) 23 | mkdir -p "$log_dir" 24 | 25 | # Enable chainstore without discard 26 | # (discard is only enabled in bootstraps for now) 27 | mkdir -p ~/.lotus 28 | echo '[Libp2p] 29 | ListenAddresses = ["/ip4/0.0.0.0/tcp/1357"] 30 | [Chainstore] 31 | EnableSplitstore = true 32 | ' > ~/.lotus/config.toml 33 | 34 | # Kill a potentially running instance of Lotus 35 | tmux kill-session -t lotus 36 | tmux new-session -d -s lotus 37 | 38 | # Start the Lotus daemon and import the bootstrap key. 39 | tmux send-keys "./eudico mir daemon --bootstrap=true --mir-validator 2>&1 | ./rotate-logs.sh ${log_dir} ${log_file_lines} ${max_archive_size}" C-m 40 | ./eudico wait-api 41 | for addr in $bootstrap_addrs; do 42 | ./eudico net connect "$addr" 43 | done 44 | ./eudico net listen | grep -vE '(/ip6/)|(127.0.0.1)' | grep -E '/ip4/.*/tcp/1357' > ~/.lotus/lotus-addr 45 | -------------------------------------------------------------------------------- /deployment/start-validators.yaml: -------------------------------------------------------------------------------- 1 | # Starts the Mir validators. 2 | # Assumes that the Lotus daemons are up and running (see start-daemons.yaml). 3 | # 4 | # Applies to the validator host by default, unless other nodes are specified using --extra-vars "nodes=..." 5 | 6 | --- 7 | - name: Start Mir validators 8 | hosts: "{{nodes | default('validators')}}" 9 | gather_facts: False 10 | become: False 11 | tasks: 12 | 13 | - name: Generate validator identities 14 | ansible.builtin.script: 15 | cmd: scripts/generate-validator-identity.sh 16 | creates: .lotus/mir-validator-identity 17 | 18 | 19 | - name: Collect validator identities 20 | ansible.builtin.fetch: 21 | src: .lotus/mir-validator-identity 22 | dest: tmp-validator-identities 23 | 24 | 25 | - name: Combine validator identities into a single file 26 | run_once: True 27 | delegate_to: localhost 28 | shell: 'rm -f mir.validators && cat tmp-validator-identities/*/.lotus/mir-validator-identity | python3 scripts/generate-membership.py >> mir.validators && rm -r tmp-validator-identities' 29 | 30 | 31 | - name: Copy validator identity file to all nodes 32 | ansible.builtin.copy: 33 | src: mir.validators 34 | dest: .lotus/ 35 | 36 | 37 | - name: Start validators 38 | ansible.builtin.script: 39 | cmd: scripts/start-validator.sh '{{ log_file_lines }}' '{{ max_log_archive_size }}' 40 | ... 41 | -------------------------------------------------------------------------------- /faucet/internal/itests/detector_test.go: -------------------------------------------------------------------------------- 1 | package itests 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | logging "github.com/ipfs/go-log/v2" 8 | "github.com/stretchr/testify/require" 9 | 10 | "github.com/filecoin-project/faucet/internal/failure" 11 | "github.com/filecoin-project/faucet/internal/itests/kit" 12 | ) 13 | 14 | func TestDetectorWhenBlockProduced(t *testing.T) { 15 | log := logging.Logger("TEST-HEALTH") 16 | lotus := kit.NewFakeLotusNoCrash() 17 | d := failure.NewDetector(log, lotus, 100*time.Millisecond, time.Second) 18 | 19 | lastBlockHeight := d.GetLastBlockHeight() 20 | 21 | for i := 0; i < 20; i++ { 22 | time.Sleep(300 * time.Millisecond) 23 | require.NoError(t, d.CheckProgress()) 24 | h := d.GetLastBlockHeight() 25 | require.Greater(t, h, lastBlockHeight) 26 | lastBlockHeight = h 27 | } 28 | } 29 | 30 | func TestDetectorWhenCrash(t *testing.T) { 31 | log := logging.Logger("TEST-HEALTH") 32 | lotus := kit.NewFakeLotus(true, 10) 33 | d := failure.NewDetector(log, lotus, 100*time.Millisecond, time.Second) 34 | 35 | lastBlockHeight := d.GetLastBlockHeight() 36 | 37 | for i := 0; i < 20; i++ { 38 | time.Sleep(300 * time.Millisecond) 39 | h := d.GetLastBlockHeight() 40 | if h < 10 { 41 | require.NoError(t, d.CheckProgress()) 42 | require.Greater(t, h, lastBlockHeight) 43 | } else if h > 10 { 44 | require.Error(t, d.CheckProgress()) 45 | require.Equal(t, uint64(10), h) 46 | } 47 | lastBlockHeight = h 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: 3 | push: 4 | branches: [main] 5 | pull_request: 6 | branches: [main] 7 | jobs: 8 | tests-go-stable: 9 | runs-on: ubuntu-latest 10 | defaults: 11 | run: 12 | shell: bash 13 | working-directory: faucet 14 | strategy: 15 | matrix: 16 | go: [ '1.18' ] 17 | steps: 18 | - name: Set up Golang 19 | uses: actions/setup-go@v3 20 | with: 21 | go-version: ${{ matrix.go }} 22 | 23 | - name: Checkout 24 | uses: actions/checkout@v3 25 | 26 | - name: Test 27 | run: make test 28 | 29 | - name: Retain event logs of failed tests 30 | if: failure() 31 | uses: actions/upload-artifact@v3 32 | with: 33 | name: event-log-go-1.18 34 | path: failed-test-data 35 | tests-go-latest: 36 | runs-on: ubuntu-latest 37 | defaults: 38 | run: 39 | shell: bash 40 | working-directory: faucet 41 | steps: 42 | - name: Set up Golang 43 | uses: actions/setup-go@v3 44 | with: 45 | go-version: 1.18 46 | check-latest: true 47 | 48 | - name: Checkout 49 | uses: actions/checkout@v3 50 | 51 | - name: Test 52 | run: make test 53 | 54 | - name: Retain event logs of failed tests 55 | if: failure() 56 | uses: actions/upload-artifact@v3 57 | with: 58 | name: event-log-go-latest 59 | path: failed-test-data -------------------------------------------------------------------------------- /faucet/internal/http/http.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gorilla/mux" 7 | "github.com/ipfs/go-datastore" 8 | logging "github.com/ipfs/go-log/v2" 9 | "github.com/rs/cors" 10 | 11 | "github.com/filecoin-project/faucet/internal/failure" 12 | "github.com/filecoin-project/faucet/internal/faucet" 13 | "github.com/filecoin-project/faucet/internal/platform/lotus" 14 | ) 15 | 16 | func FaucetHandler(logger *logging.ZapEventLogger, lotus faucet.PushWaiter, db datastore.Batching, cfg *faucet.Config) http.Handler { 17 | faucetService := faucet.NewService(logger, lotus, db, cfg) 18 | 19 | srv := NewWebService(logger, faucetService, cfg.BackendAddress) 20 | 21 | r := mux.NewRouter().StrictSlash(true) 22 | 23 | r.HandleFunc("/fund", srv.handleFunds).Methods("POST") 24 | 25 | r.HandleFunc("/", srv.handleHome) 26 | r.HandleFunc("/js/scripts.js", srv.handleScript) 27 | r.PathPrefix("/").Handler(http.StripPrefix("/", http.FileServer(http.Dir("./static")))) 28 | 29 | c := cors.New(cors.Options{ 30 | AllowedOrigins: cfg.AllowedOrigins, 31 | AllowCredentials: true, 32 | }) 33 | 34 | return c.Handler(r) 35 | } 36 | 37 | func HealthHandler(logger *logging.ZapEventLogger, lotusClient lotus.API, d *failure.Detector, build string, check ...ValidatorHealthCheck) http.Handler { 38 | h := NewHealth(logger, lotusClient, d, build, check...) 39 | r := mux.NewRouter().StrictSlash(true) 40 | r.HandleFunc("/readiness", h.Readiness).Methods("GET") 41 | r.HandleFunc("/liveness", h.Liveness).Methods("GET") 42 | return r 43 | } 44 | -------------------------------------------------------------------------------- /faucet/static/js/scripts.js: -------------------------------------------------------------------------------- 1 | const FAUCET_BACKEND="{{.}}"; 2 | // When DOM is loaded this 3 | // function will get executed 4 | $(() => { 5 | // function will get executed 6 | // on click of submit button 7 | $('#faucet').on('submit', function(e){ 8 | e.preventDefault(); 9 | ser = $(this).serialize(); 10 | data = JSON.stringify( { address: ser.split("=")[1]} ); 11 | console.log('request sent:', data); 12 | loader(); 13 | 14 | $.ajax({ 15 | type: "POST", 16 | url: FAUCET_BACKEND, 17 | crossDomain: true, // set as a cross domain request 18 | data: data, 19 | timeout: 60_000, 20 | success: function(data, status, xhr) { 21 | successAlert(); 22 | }, 23 | error: function(jqXhr, textStatus, errorThrown) { 24 | console.log("ajax error: ", errorThrown) 25 | if (jqXhr != null && jqXhr.responseText != null ) { 26 | resp = $.parseJSON(jqXhr.responseText); 27 | errorAlert(resp.errors[0]); 28 | } else { 29 | errorAlert(errorThrown); 30 | } 31 | } 32 | }); 33 | });}); 34 | 35 | function successAlert() { 36 | $('#result-msg').html(``); 39 | } 40 | 41 | function errorAlert(err) { 42 | $('#result-msg').html(``); 45 | } 46 | 47 | function loader(){ 48 | $('#result-msg').html(` 49 |
50 | 51 |
`); 52 | } 53 | -------------------------------------------------------------------------------- /deployment/rotate-logs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # TODO: MAKE THIS AN ANSIBLE VARIABLE 4 | max_archive_size=1073741824 # 1 GB 5 | 6 | # Make sure the destination directory has been specified. 7 | dest_dir=$1 8 | [ -n "${dest_dir}" ] || exit 9 | shift 10 | 11 | # Make sure that the batch size (in line numbers) has been properly specified. 12 | max_lines=$1 13 | [ "${max_lines}" -gt 0 ] || exit 14 | shift 15 | 16 | # Make sure that the maximal log archive size (in bytes) has been properly specified. 17 | max_archive_size=$1 18 | [ "${max_archive_size}" -gt 0 ] || exit 19 | shift 20 | 21 | # Initialize. 22 | cd "${dest_dir}" || exit 23 | lines=0 24 | n=0 25 | current_file=$(printf "%05d-%s.log" $n "$(date +%Y-%m-%d-%H-%M-%S_%Z)") 26 | 27 | # Copy standard input to standard output AND to a file. 28 | # Every $max_lines lines, compress the data output so far and start writing to a new file. 29 | while IFS='$\n' read -r line; do 30 | 31 | # Copy input to output and increment line counter. 32 | echo "$line" 33 | echo "$line" >> "${current_file}" 34 | lines=$((lines + 1)) 35 | 36 | # When maximal number of lines has been reached 37 | if [ $lines -ge "$max_lines" ]; then 38 | 39 | # Compress the current output. 40 | tar czf "${current_file}.tar.gz" "${current_file}" 41 | rm "${current_file}" 42 | 43 | # Reset / increment counters and create a new output file. 44 | lines=0 45 | n=$((n + 1)) 46 | current_file=$(printf "%05d-%s.log" $n "$(date +%Y-%m-%d-%H-%M-%S_%Z)") 47 | 48 | # Delete oldest logs until space limit is not exceeded any more. 49 | while [ "$(du -bs . | awk '{print $1}')" -gt $max_archive_size ]; do 50 | if ls *.tar.gz; then 51 | # If there are any files to delete, delete the first one. 52 | rm "$(ls -tr1 | head -n 1)" 53 | else 54 | # If there are no files to delete, stop the loop 55 | break 56 | fi 57 | done 58 | 59 | fi 60 | 61 | done 62 | -------------------------------------------------------------------------------- /deployment/scripts/start-bootstrap.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Obtain bootstrap key file. 4 | bootstrap_key="$1" 5 | [ -n "${bootstrap_key}" ] || exit 6 | shift 7 | 8 | # Obtain number of lines per log file. 9 | log_file_lines="$1" 10 | [ "${log_file_lines}" -gt 0 ] || exit 11 | shift 12 | 13 | # Obtain maximal log archive size (in bytes) 14 | max_archive_size=$1 15 | [ "${max_archive_size}" -gt 0 ] || exit 16 | shift 17 | 18 | 19 | cd lotus || exit 20 | 21 | # Create log directories 22 | bootstrap_log_dir=~/spacenet-logs/bootstrap-$(date +%Y-%m-%d-%H-%M-%S_%Z) 23 | #faucet_log_dir=~/spacenet-logs/faucet-$(date +%Y-%m-%d-%H-%M-%S_%Z) 24 | mkdir -p "$bootstrap_log_dir" 25 | #mkdir -p "$faucet_log_dir" 26 | 27 | # Kill a potentially running instance of Lotus 28 | tmux kill-session -t lotus 29 | tmux new-session -d -s lotus 30 | 31 | # Start the Lotus daemon and import the bootstrap key. 32 | mkdir -p ~/.lotus/keystore && chmod 0700 ~/.lotus/keystore 33 | ./lotus-shed keyinfo import "${bootstrap_key}" 34 | echo '[API] 35 | ListenAddress = "/ip4/0.0.0.0/tcp/1234/http" 36 | [Libp2p] 37 | ListenAddresses = ["/ip4/0.0.0.0/tcp/1347"] 38 | [Chainstore] 39 | EnableSplitstore = true 40 | [Chainstore.Splitstore] 41 | ColdStoreType = "discard" 42 | [Fevm] 43 | EnableEthRPC = true 44 | ' > ~/.lotus/config.toml 45 | tmux send-keys "./eudico mir daemon --profile=bootstrapper --bootstrap=false 2>&1 | ./rotate-logs.sh ${bootstrap_log_dir} ${log_file_lines} ${max_archive_size}" C-m 46 | ./eudico wait-api 47 | ./eudico net listen | grep -vE '(/ip6/)|(127.0.0.1)|(/tcp/1347)' | grep -E '/ip4/.*/tcp/' > ~/.lotus/lotus-addr 48 | 49 | ## Start the Faucet. 50 | #./eudico wallet import --as-default --format=json-lotus spacenet_faucet.key 51 | #cd ~/spacenet/faucet/ || exit 52 | #go build -o spacenet-faucet ./cmd/faucet || exit 53 | #tmux new-session -d -s faucet 54 | #tmux send-keys "export LOTUS_PATH=~/.lotus && ./spacenet-faucet --web-host \"0.0.0.0:8000\" --web-allowed-origins \"*\" --web-backend-host \"https://spacenet.consensus.ninja/fund\" --filecoin-address=t1jlm55oqkdalh2l3akqfsaqmpjxgjd36pob34dqy --lotus-api-host=127.0.0.1:1234 2>&1 | ~/lotus/rotate-logs.sh ${faucet_log_dir} ${log_file_lines} ${max_archive_size}" C-m 55 | -------------------------------------------------------------------------------- /faucet/internal/db/db_test.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "testing" 7 | "time" 8 | 9 | datastore "github.com/ipfs/go-ds-leveldb" 10 | "github.com/stretchr/testify/require" 11 | ldbopts "github.com/syndtr/goleveldb/leveldb/opt" 12 | 13 | "github.com/filecoin-project/faucet/internal/data" 14 | "github.com/filecoin-project/go-address" 15 | ) 16 | 17 | const ( 18 | dbTestStorePath = "./_db_test_store" 19 | dbTestAddr1 = "f1akaouty2buxxwb46l27pzrhl3te2lw5jem67xuy" 20 | ) 21 | 22 | func Test_Faucet(t *testing.T) { 23 | store, err := datastore.NewDatastore(dbTestStorePath, &datastore.Options{ 24 | Compression: ldbopts.NoCompression, 25 | NoSync: false, 26 | Strict: ldbopts.StrictAll, 27 | ReadOnly: false, 28 | }) 29 | require.NoError(t, err) 30 | 31 | defer func() { 32 | err = store.Close() 33 | require.NoError(t, err) 34 | err = os.RemoveAll(dbTestStorePath) 35 | require.NoError(t, err) 36 | }() 37 | 38 | db := NewDatabase(store) 39 | 40 | ctx := context.Background() 41 | 42 | addr, err := address.NewFromString(dbTestAddr1) 43 | require.NoError(t, err) 44 | 45 | addrInfo, err := db.GetAddrInfo(ctx, addr) 46 | require.NoError(t, err) 47 | require.Equal(t, data.AddrInfo{}, addrInfo) 48 | 49 | totalInfo, err := db.GetTotalInfo(ctx) 50 | require.NoError(t, err) 51 | require.Equal(t, data.TotalInfo{}, totalInfo) 52 | 53 | newAddrInfo := data.AddrInfo{ 54 | Amount: 12, 55 | LatestWithdrawal: time.Now(), 56 | } 57 | err = db.UpdateAddrInfo(ctx, addr, newAddrInfo) 58 | require.NoError(t, err) 59 | 60 | addrInfo, err = db.GetAddrInfo(ctx, addr) 61 | require.NoError(t, err) 62 | require.Equal(t, newAddrInfo.Amount, addrInfo.Amount) 63 | require.Equal(t, true, newAddrInfo.LatestWithdrawal.Equal(addrInfo.LatestWithdrawal)) 64 | 65 | newTotalInfo := data.TotalInfo{ 66 | Amount: 3000, 67 | LatestWithdrawal: time.Now(), 68 | } 69 | err = db.UpdateTotalInfo(ctx, newTotalInfo) 70 | require.NoError(t, err) 71 | 72 | totalInfo, err = db.GetTotalInfo(ctx) 73 | require.NoError(t, err) 74 | require.Equal(t, newTotalInfo.Amount, totalInfo.Amount) 75 | require.Equal(t, true, newTotalInfo.LatestWithdrawal.Equal(totalInfo.LatestWithdrawal)) 76 | } 77 | -------------------------------------------------------------------------------- /faucet/internal/itests/kit/fake_lotus.go: -------------------------------------------------------------------------------- 1 | package kit 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "sync" 7 | 8 | "github.com/ipfs/go-cid" 9 | "github.com/libp2p/go-libp2p/core/peer" 10 | ma "github.com/multiformats/go-multiaddr" 11 | 12 | "github.com/filecoin-project/go-state-types/abi" 13 | "github.com/filecoin-project/lotus/api" 14 | "github.com/filecoin-project/lotus/chain/types" 15 | ) 16 | 17 | type FakeLotus struct { 18 | m sync.Mutex 19 | failedVersion bool 20 | h uint64 21 | failed bool 22 | failedOn uint64 23 | } 24 | 25 | func NewFakeLotus(failed bool, failedOn uint64) *FakeLotus { 26 | return &FakeLotus{ 27 | failed: failed, 28 | failedOn: failedOn, 29 | } 30 | } 31 | 32 | func NewFakeLotusNoCrash() *FakeLotus { 33 | return &FakeLotus{ 34 | failed: false, 35 | failedOn: 0, 36 | } 37 | } 38 | 39 | func NewFakeLotusWithFailedVersion() *FakeLotus { 40 | return &FakeLotus{ 41 | failedVersion: true, 42 | } 43 | } 44 | 45 | func (l *FakeLotus) MpoolPushMessage(_ context.Context, msg *types.Message, _ *api.MessageSendSpec) (*types.SignedMessage, error) { 46 | smsg := types.SignedMessage{ 47 | Message: *msg, 48 | } 49 | return &smsg, nil 50 | } 51 | 52 | func (l *FakeLotus) StateWaitMsg(_ context.Context, _ cid.Cid, _ uint64, _ abi.ChainEpoch, _ bool) (*api.MsgLookup, error) { 53 | return nil, nil 54 | } 55 | 56 | func (l *FakeLotus) NodeStatus(_ context.Context, _ bool) (api.NodeStatus, error) { 57 | l.m.Lock() 58 | defer l.m.Unlock() 59 | 60 | s := api.NodeStatus{ 61 | SyncStatus: api.NodeSyncStatus{ 62 | Epoch: l.h, 63 | Behind: uint64(0), 64 | }, 65 | } 66 | if !l.failed { 67 | l.h++ 68 | } else { 69 | if l.h < l.failedOn { 70 | l.h++ 71 | } 72 | } 73 | return s, nil 74 | } 75 | 76 | func (l *FakeLotus) Version(_ context.Context) (api.APIVersion, error) { 77 | if l.failedVersion { 78 | return api.APIVersion{}, fmt.Errorf("failed to get version") 79 | } 80 | return api.APIVersion{Version: "1.0"}, nil 81 | 82 | } 83 | func (l *FakeLotus) NetPeers(context.Context) ([]peer.AddrInfo, error) { 84 | return []peer.AddrInfo{ 85 | { 86 | ID: "ID", 87 | Addrs: []ma.Multiaddr{}, 88 | }, 89 | }, nil 90 | } 91 | 92 | func (l *FakeLotus) ID(context.Context) (peer.ID, error) { 93 | return "fakeID", nil 94 | } 95 | -------------------------------------------------------------------------------- /faucet/internal/http/faucet.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "errors" 5 | "html/template" 6 | "net/http" 7 | "path" 8 | 9 | logging "github.com/ipfs/go-log/v2" 10 | 11 | "github.com/filecoin-project/faucet/internal/data" 12 | "github.com/filecoin-project/faucet/internal/faucet" 13 | "github.com/filecoin-project/faucet/internal/platform/web" 14 | "github.com/filecoin-project/go-address" 15 | ) 16 | 17 | type FaucetWebService struct { 18 | log *logging.ZapEventLogger 19 | faucet *faucet.Service 20 | backendAddress string 21 | } 22 | 23 | func NewWebService(log *logging.ZapEventLogger, faucet *faucet.Service, backendAddress string) *FaucetWebService { 24 | return &FaucetWebService{ 25 | log: log, 26 | faucet: faucet, 27 | backendAddress: backendAddress, 28 | } 29 | } 30 | 31 | func (h *FaucetWebService) handleFunds(w http.ResponseWriter, r *http.Request) { 32 | var req data.FundRequest 33 | if err := web.Decode(r, &req); err != nil { 34 | web.RespondError(w, http.StatusBadRequest, err) 35 | return 36 | } 37 | 38 | if req.Address == "" { 39 | web.RespondError(w, http.StatusBadRequest, errors.New("empty address")) 40 | return 41 | } 42 | 43 | h.log.Infof(">>> %s -> {%s}\n", r.RemoteAddr, req.Address) 44 | 45 | targetAddr, err := address.NewFromString(req.Address) 46 | if err != nil { 47 | web.RespondError(w, http.StatusBadRequest, err) 48 | return 49 | } 50 | 51 | err = h.faucet.FundAddress(r.Context(), targetAddr) 52 | if err != nil { 53 | h.log.Errorw("Failed to fund address", "addr", targetAddr, "err", err) 54 | web.RespondError(w, http.StatusInternalServerError, err) 55 | return 56 | } 57 | 58 | w.WriteHeader(http.StatusCreated) 59 | } 60 | 61 | func (h *FaucetWebService) handleHome(w http.ResponseWriter, r *http.Request) { 62 | p := path.Dir("./static/index.html") 63 | w.Header().Set("Content-type", "text/html") 64 | http.ServeFile(w, r, p) 65 | } 66 | 67 | func (h *FaucetWebService) handleScript(w http.ResponseWriter, _ *http.Request) { 68 | tmpl, err := template.ParseFiles("./static/js/scripts.js") 69 | if err != nil { 70 | web.RespondError(w, http.StatusInternalServerError, err) 71 | return 72 | } 73 | if err = tmpl.Execute(w, h.backendAddress); err != nil { 74 | web.RespondError(w, http.StatusInternalServerError, err) 75 | return 76 | } 77 | w.Header().Set("Content-type", "text/javascript") 78 | } 79 | -------------------------------------------------------------------------------- /deployment/group_vars/all.yaml: -------------------------------------------------------------------------------- 1 | # Username to use when logging in the remote machines. 2 | ansible_user: ubuntu # Example value (default on EC2 Ubuntu virtual machines) 3 | 4 | # SSH key for ansible to use when logging in the remote machines. 5 | ansible_ssh_private_key_file: ~/.ssh/spacenet-ec2-key # Meaningless example value. Set to your own ssh key location. 6 | 7 | # Git repository to obtain the Lotus code from. 8 | lotus_git_repo: https://github.com/consensus-shipyard/lotus.git 9 | 10 | # Version of the code to check out from the Lotus repository at setup. 11 | # This can be a branch name, a commit hash, etc... 12 | lotus_git_version: "spacenet" # Meaningless example value. Set to desired code version to check out from Git. 13 | 14 | # Git repository to obtain the Spacenet code from. 15 | spacenet_git_repo: https://github.com/consensus-shipyard/spacenet.git 16 | 17 | # Version of the code to check out from the Spacenet repository at setup. 18 | # This can be a branch name, a commit hash, etc... 19 | spacenet_git_version: "main" # Meaningless example value. Set to desired code version to check out from Git. 20 | 21 | # Alternative version of Mir to use. Adds the following line to Lotus' go.mod file: 22 | # replace github.com/filecoin-project/mir => {{ replace_mir }} 23 | # 24 | # Uncomment to use. 25 | #replace_mir: example.com/example/example-repo v0.1.2 26 | 27 | # Number of lines per file when saving the output of the daemon and the validator. 28 | # After log_file_lines lines have been written to the output, the file is compressed and a new file is started. 29 | log_file_lines: 65536 30 | 31 | # Maximum total size of compressed logs for each of the Lotus daemon and the Mir validator, and the faucet, in bytes. 32 | # I.e., max_log_archive_size will be allocated for one and another max_log_archive_size for the other. 33 | # When the logs exceed this size, the oldest ones will be deleted, until the total size is below this limit again. 34 | # (This means that the limit might be temporarily exceeded.) 35 | # Note that this does not include the current log being written, which might reach a significant size. 36 | max_log_archive_size: 1073741824 # 1GB 37 | 38 | # Other variables Ansible might use, probably no need to touch those... 39 | ansible_ssh_common_args: '-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ServerAliveInterval=60' 40 | num_hosts: "{{ groups['all'] | length }}" 41 | num_validators: "{{ groups['validators'] | length }}" 42 | -------------------------------------------------------------------------------- /faucet/internal/db/db.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | 8 | "github.com/ipfs/go-datastore" 9 | "github.com/pkg/errors" 10 | 11 | "github.com/filecoin-project/faucet/internal/data" 12 | "github.com/filecoin-project/go-address" 13 | ) 14 | 15 | var ( 16 | totalInfoKey = datastore.NewKey("total_info_key") 17 | ) 18 | 19 | type Database struct { 20 | store datastore.Datastore 21 | } 22 | 23 | func NewDatabase(store datastore.Datastore) *Database { 24 | return &Database{ 25 | store: store, 26 | } 27 | } 28 | 29 | func (db *Database) GetTotalInfo(ctx context.Context) (data.TotalInfo, error) { 30 | var info data.TotalInfo 31 | 32 | b, err := db.store.Get(ctx, totalInfoKey) 33 | if err != nil && !errors.Is(err, datastore.ErrNotFound) { 34 | return data.TotalInfo{}, fmt.Errorf("failed to get total info: %w", err) 35 | } 36 | if errors.Is(err, datastore.ErrNotFound) { 37 | return info, nil 38 | } 39 | if err := json.Unmarshal(b, &info); err != nil { 40 | return data.TotalInfo{}, fmt.Errorf("failed to decode total info: %w", err) 41 | } 42 | return info, nil 43 | } 44 | 45 | func (db *Database) GetAddrInfo(ctx context.Context, addr address.Address) (data.AddrInfo, error) { 46 | var info data.AddrInfo 47 | 48 | b, err := db.store.Get(ctx, addrKey(addr)) 49 | if err != nil && !errors.Is(err, datastore.ErrNotFound) { 50 | return data.AddrInfo{}, fmt.Errorf("failed to get addr info: %w", err) 51 | } 52 | if errors.Is(err, datastore.ErrNotFound) { 53 | return info, nil 54 | } 55 | if err := json.Unmarshal(b, &info); err != nil { 56 | return data.AddrInfo{}, fmt.Errorf("failed to decode addr info: %w", err) 57 | } 58 | return info, nil 59 | } 60 | 61 | func (db *Database) UpdateAddrInfo(ctx context.Context, targetAddr address.Address, info data.AddrInfo) error { 62 | bytes, err := json.Marshal(info) 63 | if err != nil { 64 | return err 65 | } 66 | 67 | err = db.store.Put(ctx, addrKey(targetAddr), bytes) 68 | if err != nil { 69 | return fmt.Errorf("failed to put addr info into db: %w", err) 70 | } 71 | 72 | return nil 73 | } 74 | 75 | func (db *Database) UpdateTotalInfo(ctx context.Context, info data.TotalInfo) error { 76 | bytes, err := json.Marshal(info) 77 | if err != nil { 78 | return err 79 | } 80 | 81 | err = db.store.Put(ctx, totalInfoKey, bytes) 82 | if err != nil { 83 | return fmt.Errorf("failed to put total info into db: %w", err) 84 | } 85 | 86 | return nil 87 | } 88 | 89 | func addrKey(addr address.Address) datastore.Key { 90 | return datastore.NewKey(addr.String() + ":value") 91 | } 92 | -------------------------------------------------------------------------------- /faucet/internal/failure/failure.go: -------------------------------------------------------------------------------- 1 | package failure 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "sync" 7 | "time" 8 | 9 | logging "github.com/ipfs/go-log/v2" 10 | 11 | "github.com/filecoin-project/faucet/internal/platform/lotus" 12 | ) 13 | 14 | type Detector struct { 15 | m sync.Mutex 16 | log *logging.ZapEventLogger 17 | lastBlockHeight uint64 18 | lotus lotus.API 19 | lastBlockHeightUpdateTime time.Time 20 | threshold time.Duration 21 | checkInterval time.Duration 22 | stopChan chan bool 23 | ticker *time.Ticker 24 | } 25 | 26 | // NewDetector creates a new failure detector that checks height value each checkInterval 27 | // and triggers a failure if there is no block height update in threshold. 28 | func NewDetector(log *logging.ZapEventLogger, api lotus.API, checkInterval, threshold time.Duration) *Detector { 29 | d := Detector{ 30 | checkInterval: checkInterval, 31 | lotus: api, 32 | log: log, 33 | threshold: threshold, 34 | stopChan: make(chan bool), 35 | lastBlockHeightUpdateTime: time.Now(), 36 | ticker: time.NewTicker(checkInterval), 37 | } 38 | 39 | go d.run() 40 | 41 | return &d 42 | } 43 | 44 | func (d *Detector) run() { 45 | ctx, cancel := context.WithCancel(context.Background()) 46 | go func() { 47 | <-d.stopChan 48 | cancel() 49 | }() 50 | 51 | for { 52 | select { 53 | case <-d.stopChan: 54 | d.log.Infow("shutdown", "status", "detector stopped") 55 | return 56 | case <-d.ticker.C: 57 | status, err := d.lotus.NodeStatus(ctx, true) 58 | if err != nil { 59 | d.log.Errorw("error", "detector", "unable to get block", err) 60 | } else { 61 | height := status.SyncStatus.Epoch 62 | d.m.Lock() 63 | if d.lastBlockHeight != height { 64 | d.lastBlockHeight = height 65 | d.lastBlockHeightUpdateTime = time.Now() 66 | } 67 | d.m.Unlock() 68 | } 69 | } 70 | } 71 | 72 | } 73 | 74 | func (d *Detector) Stop() { 75 | d.ticker.Stop() 76 | close(d.stopChan) 77 | } 78 | 79 | func (d *Detector) GetLastBlockHeight() uint64 { 80 | d.m.Lock() 81 | defer d.m.Unlock() 82 | return d.lastBlockHeight 83 | } 84 | 85 | func (d *Detector) CheckProgress() error { 86 | d.m.Lock() 87 | defer d.m.Unlock() 88 | 89 | if time.Since(d.lastBlockHeightUpdateTime) > d.threshold { 90 | return fmt.Errorf("no blocks since block %d at %s", 91 | d.lastBlockHeight, d.lastBlockHeightUpdateTime.Format("2006-01-02 15:04:05")) 92 | } 93 | return nil 94 | } 95 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.yaml: -------------------------------------------------------------------------------- 1 | name: IPC Issue Template 2 | description: Use this template to report bugs and other issues 3 | labels: [bug] 4 | body: 5 | - type: dropdown 6 | id: issue-type 7 | attributes: 8 | label: Issue type 9 | description: What type of issue would you like to report? 10 | multiple: false 11 | options: 12 | - Bug 13 | - Build/Install 14 | - Performance 15 | - Support 16 | - Feature Request 17 | - Documentation Bug 18 | - Documentation Request 19 | - Others 20 | validations: 21 | required: true 22 | 23 | - type: dropdown 24 | id: latest 25 | attributes: 26 | label: Have you reproduced the bug with the latest dev version? 27 | description: We suggest attempting to reproducing the bug with the dev branch 28 | options: 29 | - "Yes" 30 | - "No" 31 | validations: 32 | required: true 33 | 34 | - type: input 35 | id: version 36 | attributes: 37 | label: Version 38 | placeholder: e.g. v0.4.0 39 | validations: 40 | required: true 41 | - type: dropdown 42 | id: Code 43 | attributes: 44 | label: Custom code 45 | options: 46 | - "Yes" 47 | - "No" 48 | validations: 49 | required: true 50 | - type: input 51 | id: OS 52 | attributes: 53 | label: OS platform and distribution 54 | placeholder: e.g., Linux Ubuntu 16.04 55 | - type: textarea 56 | id: what-happened 57 | attributes: 58 | label: Describe the issue 59 | description: Also tell us, what did you expect to happen? 60 | placeholder: | 61 | This is where you get to tell us what went wrong, when doing so, please try to provide a clear and concise description of the bug with all related information: 62 | * What you were doing when you experienced the bug? What are you trying to build? 63 | * Any *error* messages and logs you saw, *where* you saw them, and what you believe may have caused them (if you have any ideas). 64 | * What is the expected behaviour? Links to the code? 65 | validations: 66 | required: true 67 | - type: textarea 68 | id: repro-steps 69 | attributes: 70 | label: Repro steps 71 | description: Provide the minimum necessary steps to reproduce the problem. 72 | placeholder: Tell us what you see! 73 | validations: 74 | required: true 75 | - type: textarea 76 | id: logs 77 | attributes: 78 | label: Relevant log output 79 | description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. 80 | render: shell 81 | -------------------------------------------------------------------------------- /deployment/scripts/generate-membership.py: -------------------------------------------------------------------------------- 1 | # This script generates a membership ("validator set") understood by Eudico 2 | # from validator addresses in the format output by 3 | # 4 | # eudico net listen 5 | # 6 | # Input is read from stdin and output written to stdout. 7 | # Configuration number of the membership is always set to 0. 8 | # 9 | # Weight must be greater than 0. 10 | # 11 | # Example input: 12 | # t1dgw4345grpw53zdhu75dc6jj4qhrh4zoyrtq6di@/ip4/172.31.39.78/tcp/43077/p2p/12D3KooWNzTunrQtcoo4SLWNdQ4EdFWSZtah6mgU44Q5XWM61aan 13 | # t1a5gxsoogaofa5nzfdh66l6uynx4m6m4fiqvcx6y@/ip4/172.31.33.169/tcp/38257/p2p/12D3KooWABvxn3CHjz9r5TYGXGDqm8549VEuAyFpbkH8xWkNLSmr 14 | # t1q4j6esoqvfckm7zgqfjynuytjanbhirnbwfrsty@/ip4/172.31.42.15/tcp/44407/p2p/12D3KooWGdQGu1utYP6KD1Cq4iXTLV6hbZa8yQN34zwuHNP5YbCi 15 | # t16biatgyushsfcidabfy2lm5wo22ppe6r7ddir6y@/ip4/172.31.47.117/tcp/34355/p2p/12D3KooWEtfTyoWW7pFLsErAb6jPiQQCC3y3junHtLn9jYnFHei8 16 | # 17 | # Example output: 18 | # { 19 | # "configuration_number": 0, 20 | # "validators": [ 21 | # { 22 | # "addr": "t1dgw4345grpw53zdhu75dc6jj4qhrh4zoyrtq6di", 23 | # "net_addr": "/ip4/172.31.39.78/tcp/43077/p2p/12D3KooWNzTunrQtcoo4SLWNdQ4EdFWSZtah6mgU44Q5XWM61aan", 24 | # "weight": "1" 25 | # }, 26 | # { 27 | # "addr": "t1a5gxsoogaofa5nzfdh66l6uynx4m6m4fiqvcx6y", 28 | # "net_addr": "/ip4/172.31.33.169/tcp/38257/p2p/12D3KooWABvxn3CHjz9r5TYGXGDqm8549VEuAyFpbkH8xWkNLSmr", 29 | # "weight": "1" 30 | # }, 31 | # { 32 | # "addr": "t1q4j6esoqvfckm7zgqfjynuytjanbhirnbwfrsty", 33 | # "net_addr": "/ip4/172.31.42.15/tcp/44407/p2p/12D3KooWGdQGu1utYP6KD1Cq4iXTLV6hbZa8yQN34zwuHNP5YbCi", 34 | # "weight": "1" 35 | # }, 36 | # { 37 | # "addr": "t16biatgyushsfcidabfy2lm5wo22ppe6r7ddir6y", 38 | # "net_addr": "/ip4/172.31.47.117/tcp/34355/p2p/12D3KooWEtfTyoWW7pFLsErAb6jPiQQCC3y3junHtLn9jYnFHei8", 39 | # "weight": "1" 40 | # } 41 | # ] 42 | # } 43 | 44 | import sys 45 | import json 46 | 47 | membership = { 48 | "configuration_number": 0, 49 | "validators": [], 50 | } 51 | 52 | def parse_validator(line: str): 53 | tokens = line.split("@") 54 | membership["validators"].append({ 55 | "addr": tokens[0], 56 | "net_addr": tokens[1], 57 | "weight": "1", 58 | }) 59 | 60 | for line in sys.stdin.readlines(): 61 | line = line.strip() 62 | if line != "": # Skip empty lines 63 | parse_validator(line) 64 | 65 | # Printing the output of json.dumps instead of using directly json.dump to stdout, 66 | # since the latter seems to append extra characters to the output. 67 | print(json.dumps(membership, indent=4)) 68 | -------------------------------------------------------------------------------- /deployment/update-nodes.yaml: -------------------------------------------------------------------------------- 1 | # Updates a given set of validators by fetching the configured code, recompiling it, and restarting the validators. 2 | # After the update, waits until the nodes sync with the state of a bootstrap node and only then returns. 3 | # 4 | # For safety, does NOT default to restarting all validators 5 | # and the set of hosts to restart must be explicitly given using --extra-vars "nodes=..." 6 | # 7 | # Note that this playbook always affects all hosts, regardless of the value of the nodes variable. 8 | # This is due to the necessity of reconnecting all daemons to the restarted one. 9 | 10 | --- 11 | - name: Make sure nodes are specified explicitly 12 | hosts: "{{ nodes }}" 13 | gather_facts: False 14 | tasks: 15 | 16 | - import_playbook: setup.yaml 17 | 18 | - name: Restart Lotus daemons 19 | hosts: "{{ nodes }}" 20 | serial: 1 21 | gather_facts: False 22 | vars: 23 | bootstrap_identity: "{{ lookup('file', 'bootstrap-identity') }}" 24 | tasks: 25 | 26 | - name: Execute kill script 27 | ansible.builtin.script: 28 | cmd: scripts/kill.sh 29 | ignore_errors: True 30 | 31 | 32 | - name: Start Lotus daemon 33 | ansible.builtin.script: 34 | cmd: scripts/start-daemon.sh '{{ bootstrap_identity }}' '{{ log_file_lines }}' '{{ max_log_archive_size }}' 35 | 36 | 37 | - import_playbook: connect-daemons.yaml 38 | 39 | 40 | - name: Get the current block height from a bootstrap node 41 | hosts: bootstrap 42 | gather_facts: False 43 | tasks: 44 | - name: Get the current block height from the bootstrap node 45 | run_once: True 46 | shell: 'lotus/lotus chain get-block $(lotus/lotus chain head) | jq ".Height"' 47 | register: bootstrap_height 48 | 49 | 50 | - name: Start only the specified validators 51 | hosts: "{{ nodes }}" 52 | gather_facts: False 53 | tasks: 54 | 55 | - name: Show the block height at the bootstrap node 56 | ansible.builtin.debug: 57 | msg: "{{ hostvars['3.66.145.60'].bootstrap_height.stdout }}" 58 | 59 | 60 | # WARNING: Adjust this if checkpoint period changes. 61 | # TODO: Get rid of this altogether when the bug that requires us to wait here is fixed. 62 | - name: Wait until some new checkpoints are created 63 | ansible.builtin.wait_for: 64 | timeout: 20 65 | delegate_to: localhost 66 | 67 | 68 | - name: Start validators 69 | ansible.builtin.script: 70 | cmd: scripts/start-validator.sh '{{ log_file_lines }}' '{{ max_log_archive_size }}' 71 | 72 | 73 | - name: Wait until the validator catches up 74 | ansible.builtin.shell: 'lotus/lotus chain get-block $(lotus/lotus chain head) | jq ".Height"' 75 | register: validator_height 76 | until: validator_height.stdout | int > hostvars['3.66.145.60'].bootstrap_height.stdout | int 77 | delay: 10 78 | retries: 6 79 | 80 | 81 | - name: Show the block height at the restarted validator node 82 | ansible.builtin.debug: 83 | msg: "{{ validator_height.stdout }}" 84 | ... -------------------------------------------------------------------------------- /deployment/setup.yaml: -------------------------------------------------------------------------------- 1 | # Sets up the environment for running the Lotus daemon and validator. 2 | # This includes installing the necessary packages, fetching the Lotus code, and compiling it. 3 | # It does not start any nodes. See start-* and deploy-*.yaml for starting the nodes. 4 | # 5 | # Applies to all hosts by default, unless other nodes are specified using --extra-vars "nodes=..." 6 | 7 | --- 8 | - name: Initialize Spacenet VM 9 | hosts: "{{nodes | default('all')}}" 10 | gather_facts: False 11 | become: False 12 | environment: 13 | PATH: "{{ ansible_env.PATH }}:/home/{{ ansible_user }}/go/bin" 14 | tasks: 15 | 16 | - name: "Run apt update" 17 | become: True 18 | ansible.builtin.apt: 19 | update_cache: True 20 | 21 | 22 | - name: "Install apt packages" 23 | become: True 24 | ansible.builtin.apt: 25 | name: 26 | - mesa-opencl-icd 27 | - ocl-icd-opencl-dev 28 | - gcc 29 | - git 30 | - bzr 31 | - jq 32 | - pkg-config 33 | - curl 34 | - clang 35 | - build-essential 36 | - hwloc 37 | - libhwloc-dev 38 | - wget 39 | - make 40 | # - golang # The apt version of Go is outdated. We install it from source using th install-go.sh script. 41 | - tmux 42 | state: present 43 | 44 | - name: "Install Go" 45 | ansible.builtin.script: 46 | cmd: scripts/install-go.sh 47 | 48 | - name: "Work around upgrade issues on some Linux machines" 49 | become: True 50 | ansible.builtin.apt: 51 | name: 52 | - grub-efi-amd64-signed 53 | only_upgrade: True 54 | 55 | 56 | - name: "Run apt upgrade" 57 | become: True 58 | ansible.builtin.apt: 59 | upgrade: True 60 | 61 | 62 | - name: "Clone Lotus repo from GitHub" 63 | ansible.builtin.git: 64 | repo: "{{ lotus_git_repo }}" 65 | dest: ~/lotus 66 | force: True 67 | 68 | 69 | - name: "Check out the selected code version: {{ lotus_git_version }}" 70 | ansible.builtin.git: 71 | repo: "{{ lotus_git_repo }}" 72 | dest: ~/lotus 73 | single_branch: True 74 | version: "{{ lotus_git_version }}" 75 | force: True 76 | 77 | 78 | - name: "Replace mir library by a custom version ({{ replace_mir }})" 79 | shell: echo '\nreplace github.com/filecoin-project/mir => {{ replace_mir }}\n' >> lotus/go.mod && cd lotus && go mod tidy 80 | when: replace_mir is defined 81 | 82 | 83 | - name: "Clone Spacenet repo from GitHub" 84 | ansible.builtin.git: 85 | repo: "{{ spacenet_git_repo }}" 86 | dest: ~/spacenet 87 | force: True 88 | 89 | 90 | - name: "Check out the selected code version: {{ spacenet_git_version }}" 91 | ansible.builtin.git: 92 | repo: "{{ spacenet_git_repo }}" 93 | dest: ~/spacenet 94 | single_branch: True 95 | version: "{{ spacenet_git_version }}" 96 | force: True 97 | 98 | 99 | - name: "Upload log rotation utility" 100 | ansible.builtin.copy: 101 | src: rotate-logs.sh 102 | dest: lotus/rotate-logs.sh 103 | mode: u+x 104 | 105 | 106 | - name: "Compile Spacenet code" 107 | make: 108 | chdir: ~/lotus 109 | target: spacenet 110 | 111 | 112 | - name: "Run setup script" 113 | ansible.builtin.script: 114 | cmd: scripts/setup.sh 115 | ... 116 | -------------------------------------------------------------------------------- /faucet/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 39 | 40 |
41 |
42 |
43 |

🪐 Spacenet Faucet

44 |

Get some funds and start using the Filecoin Spacenet 🚀

45 |
46 |
47 |
48 | 49 | 50 |
51 |

52 |

53 | 54 |
55 |
56 |
57 |
58 | 59 |
60 |
61 | 62 |
63 |
64 |
65 | 66 | 67 | 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /faucet/internal/faucet/faucet.go: -------------------------------------------------------------------------------- 1 | package faucet 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/ipfs/go-cid" 9 | "github.com/ipfs/go-datastore" 10 | logging "github.com/ipfs/go-log/v2" 11 | 12 | "github.com/filecoin-project/faucet/internal/db" 13 | "github.com/filecoin-project/go-address" 14 | "github.com/filecoin-project/go-state-types/abi" 15 | "github.com/filecoin-project/lotus/api" 16 | "github.com/filecoin-project/lotus/build" 17 | "github.com/filecoin-project/lotus/chain/types" 18 | ) 19 | 20 | var ( 21 | ErrExceedTotalAllowedFunds = fmt.Errorf("transaction exceeds total allowed funds per day") 22 | ErrExceedAddrAllowedFunds = fmt.Errorf("transaction to exceeds daily allowed funds per address") 23 | ) 24 | 25 | type PushWaiter interface { 26 | MpoolPushMessage(ctx context.Context, msg *types.Message, spec *api.MessageSendSpec) (*types.SignedMessage, error) 27 | StateWaitMsg(ctx context.Context, cid cid.Cid, confidence uint64, limit abi.ChainEpoch, allowReplaced bool) (*api.MsgLookup, error) 28 | } 29 | 30 | type Config struct { 31 | FaucetAddress address.Address 32 | AllowedOrigins []string 33 | TotalWithdrawalLimit uint64 34 | AddressWithdrawalLimit uint64 35 | WithdrawalAmount uint64 36 | BackendAddress string 37 | } 38 | 39 | type Service struct { 40 | log *logging.ZapEventLogger 41 | lotus PushWaiter 42 | db *db.Database 43 | faucet address.Address 44 | cfg *Config 45 | } 46 | 47 | func NewService(log *logging.ZapEventLogger, lotus PushWaiter, store datastore.Datastore, cfg *Config) *Service { 48 | return &Service{ 49 | cfg: cfg, 50 | log: log, 51 | lotus: lotus, 52 | db: db.NewDatabase(store), 53 | faucet: cfg.FaucetAddress, 54 | } 55 | } 56 | 57 | func (s *Service) FundAddress(ctx context.Context, targetAddr address.Address) error { 58 | addrInfo, err := s.db.GetAddrInfo(ctx, targetAddr) 59 | if err != nil { 60 | return err 61 | } 62 | s.log.Infof("target address info: %v", addrInfo) 63 | 64 | totalInfo, err := s.db.GetTotalInfo(ctx) 65 | if err != nil { 66 | return err 67 | } 68 | s.log.Infof("total info: %v", totalInfo) 69 | 70 | if addrInfo.LatestWithdrawal.IsZero() || time.Since(addrInfo.LatestWithdrawal) >= 24*time.Hour { 71 | addrInfo.Amount = 0 72 | addrInfo.LatestWithdrawal = time.Now() 73 | } 74 | 75 | if totalInfo.LatestWithdrawal.IsZero() || time.Since(totalInfo.LatestWithdrawal) >= 24*time.Hour { 76 | totalInfo.Amount = 0 77 | totalInfo.LatestWithdrawal = time.Now() 78 | } 79 | 80 | if totalInfo.Amount >= s.cfg.TotalWithdrawalLimit { 81 | return ErrExceedTotalAllowedFunds 82 | } 83 | 84 | if addrInfo.Amount >= s.cfg.AddressWithdrawalLimit { 85 | return ErrExceedAddrAllowedFunds 86 | } 87 | 88 | s.log.Infof("funding %v is allowed", targetAddr) 89 | 90 | err = s.pushMessage(ctx, targetAddr) 91 | if err != nil { 92 | s.log.Errorw("Error waiting for message to be committed", "err", err) 93 | return fmt.Errorf("failt to push message: %w", err) 94 | } 95 | 96 | addrInfo.Amount += s.cfg.WithdrawalAmount 97 | totalInfo.Amount += s.cfg.WithdrawalAmount 98 | 99 | if err = s.db.UpdateAddrInfo(ctx, targetAddr, addrInfo); err != nil { 100 | return err 101 | } 102 | 103 | if err = s.db.UpdateTotalInfo(ctx, totalInfo); err != nil { 104 | return err 105 | } 106 | 107 | return nil 108 | } 109 | 110 | func (s *Service) pushMessage(ctx context.Context, addr address.Address) error { 111 | msg, err := s.lotus.MpoolPushMessage(ctx, &types.Message{ 112 | To: addr, 113 | From: s.faucet, 114 | Value: types.FromFil(s.cfg.WithdrawalAmount), 115 | Method: 0, // method Send 116 | Params: nil, 117 | }, nil) 118 | if err != nil { 119 | return err 120 | } 121 | 122 | if _, err = s.lotus.StateWaitMsg(ctx, msg.Cid(), build.MessageConfidence, abi.ChainEpoch(-1), true); err != nil { 123 | return err 124 | } 125 | 126 | s.log.Infof("Address %v funded successfully", addr) 127 | return nil 128 | } 129 | -------------------------------------------------------------------------------- /faucet/internal/itests/health_test.go: -------------------------------------------------------------------------------- 1 | package itests 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | "time" 10 | 11 | logging "github.com/ipfs/go-log/v2" 12 | "github.com/stretchr/testify/require" 13 | 14 | "github.com/filecoin-project/faucet/internal/data" 15 | "github.com/filecoin-project/faucet/internal/failure" 16 | handler "github.com/filecoin-project/faucet/internal/http" 17 | "github.com/filecoin-project/faucet/internal/itests/kit" 18 | ) 19 | 20 | type HealthTests struct { 21 | handler http.Handler 22 | } 23 | 24 | func TestValidatorHealth(t *testing.T) { 25 | log := logging.Logger("TEST-HEALTH") 26 | lotus := kit.NewFakeLotusNoCrash() 27 | d := failure.NewDetector(log, lotus, 100*time.Millisecond, time.Second) 28 | srv := handler.HealthHandler(log, lotus, d, "build") 29 | test := HealthTests{ 30 | handler: srv, 31 | } 32 | t.Run("validator-liveness", test.livenessForValidator) 33 | t.Run("validator-readiness", test.readinessForValidator) 34 | } 35 | 36 | func (ht *HealthTests) livenessForValidator(t *testing.T) { 37 | r := httptest.NewRequest(http.MethodGet, "/liveness", nil) 38 | w := httptest.NewRecorder() 39 | ht.handler.ServeHTTP(w, r) 40 | require.Equal(t, http.StatusOK, w.Code) 41 | var resp data.LivenessResponse 42 | err := json.Unmarshal(w.Body.Bytes(), &resp) 43 | require.NoError(t, err) 44 | require.Equal(t, 1, resp.PeerNumber) 45 | } 46 | 47 | func (ht *HealthTests) readinessForValidator(t *testing.T) { 48 | r := httptest.NewRequest(http.MethodGet, "/readiness", nil) 49 | w := httptest.NewRecorder() 50 | ht.handler.ServeHTTP(w, r) 51 | require.Equal(t, http.StatusInternalServerError, w.Code) 52 | } 53 | 54 | func TestBootstrapHealth(t *testing.T) { 55 | log := logging.Logger("TEST-HEALTH") 56 | lotus := kit.NewFakeLotusNoCrash() 57 | check := func() error { 58 | return fmt.Errorf("failed") 59 | } 60 | d := failure.NewDetector(log, lotus, 100*time.Millisecond, time.Second) 61 | srv := handler.HealthHandler(log, lotus, d, "build", check) 62 | test := HealthTests{ 63 | handler: srv, 64 | } 65 | t.Run("bootstrap-liveness", test.livenessForBootstrap) 66 | t.Run("bootstrap-readiness", test.readinessForBootstrap) 67 | } 68 | 69 | func (ht *HealthTests) livenessForBootstrap(t *testing.T) { 70 | r := httptest.NewRequest(http.MethodGet, "/liveness", nil) 71 | w := httptest.NewRecorder() 72 | ht.handler.ServeHTTP(w, r) 73 | require.Equal(t, http.StatusOK, w.Code) 74 | var resp data.LivenessResponse 75 | err := json.Unmarshal(w.Body.Bytes(), &resp) 76 | require.NoError(t, err) 77 | require.Equal(t, 1, resp.PeerNumber) 78 | } 79 | 80 | func (ht *HealthTests) readinessForBootstrap(t *testing.T) { 81 | r := httptest.NewRequest(http.MethodGet, "/readiness?bootstrap=true", nil) 82 | w := httptest.NewRecorder() 83 | ht.handler.ServeHTTP(w, r) 84 | require.Equal(t, http.StatusOK, w.Code) 85 | } 86 | 87 | func TestValidatorFailedHealth(t *testing.T) { 88 | log := logging.Logger("TEST-HEALTH") 89 | lotus := kit.NewFakeLotusNoCrash() 90 | check := func() error { 91 | return fmt.Errorf("failed") 92 | } 93 | d := failure.NewDetector(log, lotus, 100*time.Millisecond, time.Second) 94 | srv := handler.HealthHandler(log, lotus, d, "build", check) 95 | test := HealthTests{ 96 | handler: srv, 97 | } 98 | t.Run("failed-validator-readiness", test.failedReadinessForBootstrap) 99 | } 100 | 101 | func (ht *HealthTests) failedReadinessForBootstrap(t *testing.T) { 102 | r := httptest.NewRequest(http.MethodGet, "/readiness", nil) 103 | w := httptest.NewRecorder() 104 | ht.handler.ServeHTTP(w, r) 105 | require.Equal(t, http.StatusInternalServerError, w.Code) 106 | } 107 | 108 | func TestValidatorFailedHealthWithFailedLotus(t *testing.T) { 109 | log := logging.Logger("TEST-HEALTH") 110 | lotus := kit.NewFakeLotusWithFailedVersion() 111 | d := failure.NewDetector(log, lotus, 100*time.Millisecond, time.Second) 112 | srv := handler.HealthHandler(log, lotus, d, "build") 113 | test := HealthTests{ 114 | handler: srv, 115 | } 116 | t.Run("failed-validator-readiness-failed-lotus", test.failedReadinessForBootstrapWithFailedLotus) 117 | } 118 | 119 | func (ht *HealthTests) failedReadinessForBootstrapWithFailedLotus(t *testing.T) { 120 | r := httptest.NewRequest(http.MethodGet, "/readiness", nil) 121 | w := httptest.NewRecorder() 122 | ht.handler.ServeHTTP(w, r) 123 | require.Equal(t, http.StatusInternalServerError, w.Code) 124 | } 125 | -------------------------------------------------------------------------------- /faucet/internal/http/health.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net/http" 7 | "os" 8 | "os/exec" 9 | 10 | logging "github.com/ipfs/go-log/v2" 11 | 12 | "github.com/filecoin-project/faucet/internal/data" 13 | "github.com/filecoin-project/faucet/internal/failure" 14 | "github.com/filecoin-project/faucet/internal/platform/lotus" 15 | "github.com/filecoin-project/faucet/internal/platform/web" 16 | v "github.com/filecoin-project/faucet/pkg/version" 17 | ) 18 | 19 | type Health struct { 20 | log *logging.ZapEventLogger 21 | node lotus.API 22 | build string 23 | detector *failure.Detector 24 | check ValidatorHealthCheck 25 | } 26 | 27 | type ValidatorHealthCheck func() error 28 | 29 | func NewHealth(log *logging.ZapEventLogger, node lotus.API, d *failure.Detector, build string, check ...ValidatorHealthCheck) *Health { 30 | h := Health{ 31 | log: log, 32 | node: node, 33 | build: build, 34 | detector: d, 35 | } 36 | if check == nil { 37 | h.check = defaultValidatorHealthCheck 38 | } else { 39 | h.check = check[0] 40 | } 41 | return &h 42 | } 43 | 44 | // Liveness returns status info if the service is alive. 45 | func (h *Health) Liveness(w http.ResponseWriter, r *http.Request) { 46 | ctx := r.Context() 47 | 48 | host, err := os.Hostname() 49 | if err != nil { 50 | host = "unavailable" 51 | } 52 | 53 | statusCode := http.StatusOK 54 | 55 | if err := h.detector.CheckProgress(); err != nil { 56 | web.RespondError(w, http.StatusInternalServerError, err) 57 | return 58 | } 59 | 60 | status, err := h.node.NodeStatus(ctx, true) 61 | if err != nil { 62 | web.RespondError(w, http.StatusInternalServerError, err) 63 | return 64 | } 65 | 66 | version, err := h.node.Version(ctx) 67 | if err != nil { 68 | web.RespondError(w, http.StatusInternalServerError, err) 69 | return 70 | } 71 | 72 | h.log.Infow("liveness", "statusCode", statusCode, "method", r.Method, "path", r.URL.Path, "remoteaddr", r.RemoteAddr) 73 | 74 | p, err := h.node.NetPeers(ctx) 75 | if err != nil { 76 | web.RespondError(w, http.StatusInternalServerError, err) 77 | return 78 | } 79 | id, err := h.node.ID(r.Context()) 80 | if err != nil { 81 | web.RespondError(w, http.StatusInternalServerError, err) 82 | return 83 | } 84 | 85 | resp := data.LivenessResponse{ 86 | LotusVersion: version.String(), 87 | Epoch: status.SyncStatus.Epoch, 88 | Behind: status.SyncStatus.Behind, 89 | PeersToPublishMsgs: status.PeerStatus.PeersToPublishMsgs, 90 | PeersToPublishBlocks: status.PeerStatus.PeersToPublishBlocks, 91 | PeerNumber: len(p), 92 | Host: host, 93 | Build: h.build, 94 | PeerID: id.String(), 95 | ServiceVersion: v.Version(), 96 | } 97 | 98 | if err := web.Respond(r.Context(), w, resp, http.StatusOK); err != nil { 99 | web.RespondError(w, http.StatusInternalServerError, err) 100 | return 101 | } 102 | } 103 | 104 | // Readiness checks if the components are ready and if not will return a 500 status. 105 | func (h *Health) Readiness(w http.ResponseWriter, r *http.Request) { 106 | ctx := r.Context() 107 | 108 | h.log.Infow("readiness", "method", r.Method, "path", r.URL.Path, "remote", r.RemoteAddr) 109 | 110 | ready := true 111 | if _, err := h.node.Version(ctx); err != nil { 112 | h.log.Errorw("failed to connect to daemon", "source", "readiness", "ERROR", err) 113 | ready = false 114 | } 115 | 116 | // A node can be a bootstrap node or validator node. Bootstrap nodes run daemons only. 117 | // We signal that a node is a bootstrap node by accessing /readiness endpoint with "boostrap" parameter. 118 | isBootstrap := r.URL.Query().Get("bootstrap") != "" 119 | 120 | if !isBootstrap { 121 | if err := h.checkValidatorStatus(); err != nil { 122 | h.log.Errorw("failed to connect to validator", "source", "readiness", "ERROR", err) 123 | ready = false 124 | } 125 | } 126 | 127 | if !ready { 128 | http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 129 | return 130 | } 131 | 132 | resp := struct { 133 | Status string `json:"status"` 134 | }{ 135 | Status: "ok", 136 | } 137 | 138 | if err := web.Respond(ctx, w, resp, http.StatusOK); err != nil { 139 | web.RespondError(w, http.StatusInternalServerError, err) 140 | return 141 | } 142 | } 143 | 144 | func (h *Health) checkValidatorStatus() error { 145 | return h.check() 146 | } 147 | 148 | func defaultValidatorHealthCheck() error { 149 | grep := exec.Command("grep", "[e]udico mir validator") 150 | ps := exec.Command("ps", "ax") 151 | 152 | pipe, _ := ps.StdoutPipe() 153 | defer func(pipe io.ReadCloser) { 154 | pipe.Close() // nolint 155 | }(pipe) 156 | 157 | grep.Stdin = pipe 158 | if err := ps.Start(); err != nil { 159 | return err 160 | } 161 | 162 | // Run and get the output of grep. 163 | o, err := grep.Output() 164 | if err != nil { 165 | return err 166 | } 167 | if o == nil { 168 | return fmt.Errorf("validator not found") 169 | } 170 | return nil 171 | } 172 | -------------------------------------------------------------------------------- /deployment/spacenet_template.json: -------------------------------------------------------------------------------- 1 | { 2 | "NetworkVersion": 18, 3 | "Accounts": [ 4 | { 5 | "Type": "account", 6 | "Balance": "5000000000000", 7 | "Meta": { 8 | "Owner": "t3tat272hqg2h6fuokkun4xx742flp72iwvnsao5z4ba7c5qwhjlumyuklcnudg74phfvxaqd52ncb5vhutu2a" 9 | } 10 | }, 11 | { 12 | "Type": "account", 13 | "Balance": "5000000000000", 14 | "Meta": { 15 | "Owner": "t1cp4q4lqsdhob23ysywffg2tvbmar5cshia4rweq" 16 | } 17 | }, 18 | { 19 | "Type": "account", 20 | "Balance": "5000000000000", 21 | "Meta": { 22 | "Owner": "t1akaouty2buxxwb46l27pzrhl3te2lw5jem67xuy" 23 | } 24 | }, 25 | { 26 | "Type": "account", 27 | "Balance": "5000000000000", 28 | "Meta": { 29 | "Owner": "t1vfp7yzvwy7ftktnex2cfoz2gpm2jyxlebqpam4q" 30 | } 31 | }, 32 | { 33 | "Type": "account", 34 | "Balance": "5000000000000", 35 | "Meta": { 36 | "Owner": "t12zjpclnis2uytmcydrx7i5jcbvehs5ut3x6mvvq" 37 | } 38 | }, 39 | { 40 | "Type": "account", 41 | "Balance": "500000000000000000000000000", 42 | "Meta": { 43 | "Owner": "f1jlm55oqkdalh2l3akqfsaqmpjxgjd36pob34dqy" 44 | } 45 | } 46 | ], 47 | "Miners": [ 48 | { 49 | "ID": "t01000", 50 | "Owner": "t3tat272hqg2h6fuokkun4xx742flp72iwvnsao5z4ba7c5qwhjlumyuklcnudg74phfvxaqd52ncb5vhutu2a", 51 | "Worker": "t3tat272hqg2h6fuokkun4xx742flp72iwvnsao5z4ba7c5qwhjlumyuklcnudg74phfvxaqd52ncb5vhutu2a", 52 | "PeerId": "12D3KooWGnEaQeRxwJtvBQ7bNNYSP5XyFL2TZVyuswkbAJ3MakFB", 53 | "MarketBalance": "0", 54 | "PowerBalance": "0", 55 | "SectorSize": 2048, 56 | "Sectors": [ 57 | { 58 | "CommR": { 59 | "/": "bagboea4b5abcbf33kvktdrl4vipsv5n2uwo3cuhagx4ldcfra4a3d3c26hxzy4b4" 60 | }, 61 | "CommD": { 62 | "/": "baga6ea4seaqlikortzuo455iu3ggem62veye2kpeenftkvn4isxmgrkmxlotcma" 63 | }, 64 | "SectorID": 0, 65 | "Deal": { 66 | "PieceCID": { 67 | "/": "baga6ea4seaqlikortzuo455iu3ggem62veye2kpeenftkvn4isxmgrkmxlotcma" 68 | }, 69 | "PieceSize": 2048, 70 | "VerifiedDeal": false, 71 | "Client": "t3tat272hqg2h6fuokkun4xx742flp72iwvnsao5z4ba7c5qwhjlumyuklcnudg74phfvxaqd52ncb5vhutu2a", 72 | "Provider": "t01000", 73 | "Label": "0", 74 | "StartEpoch": 0, 75 | "EndEpoch": 9001, 76 | "StoragePricePerEpoch": "0", 77 | "ProviderCollateral": "0", 78 | "ClientCollateral": "0" 79 | }, 80 | "DealClientKey": { 81 | "Type": "bls", 82 | "PrivateKey": "oAQEUxvkaogDLfgCR69XseQjDra0KdDHjzrW8UDl5Vs=", 83 | "PublicKey": "mCev6PA2j+LRylUby9/80Vb/6RarZAd3PAg+LsLHSujMUUsTaDN/jzlrcEB900Qe", 84 | "Address": "t3tat272hqg2h6fuokkun4xx742flp72iwvnsao5z4ba7c5qwhjlumyuklcnudg74phfvxaqd52ncb5vhutu2a" 85 | }, 86 | "ProofType": 5 87 | }, 88 | { 89 | "CommR": { 90 | "/": "bagboea4b5abcbckyfnyxbq2vdk4bifzsiyygi3qd6cir6fxt76gyfjmroqrdhztd" 91 | }, 92 | "CommD": { 93 | "/": "baga6ea4seaqebwwnv3ltxshq6mvub2jtpb7ujwdae37y3i5tybnzs6plvtwo2bq" 94 | }, 95 | "SectorID": 1, 96 | "Deal": { 97 | "PieceCID": { 98 | "/": "baga6ea4seaqebwwnv3ltxshq6mvub2jtpb7ujwdae37y3i5tybnzs6plvtwo2bq" 99 | }, 100 | "PieceSize": 2048, 101 | "VerifiedDeal": false, 102 | "Client": "t3tat272hqg2h6fuokkun4xx742flp72iwvnsao5z4ba7c5qwhjlumyuklcnudg74phfvxaqd52ncb5vhutu2a", 103 | "Provider": "t01000", 104 | "Label": "1", 105 | "StartEpoch": 0, 106 | "EndEpoch": 9001, 107 | "StoragePricePerEpoch": "0", 108 | "ProviderCollateral": "0", 109 | "ClientCollateral": "0" 110 | }, 111 | "DealClientKey": { 112 | "Type": "bls", 113 | "PrivateKey": "oAQEUxvkaogDLfgCR69XseQjDra0KdDHjzrW8UDl5Vs=", 114 | "PublicKey": "mCev6PA2j+LRylUby9/80Vb/6RarZAd3PAg+LsLHSujMUUsTaDN/jzlrcEB900Qe", 115 | "Address": "t3tat272hqg2h6fuokkun4xx742flp72iwvnsao5z4ba7c5qwhjlumyuklcnudg74phfvxaqd52ncb5vhutu2a" 116 | }, 117 | "ProofType": 5 118 | } 119 | ] 120 | } 121 | ], 122 | "NetworkName": "spacenet", 123 | "VerifregRootKey": { 124 | "Type": "multisig", 125 | "Balance": "0", 126 | "Meta": { 127 | "Signers": [ 128 | "t1ceb34gnsc6qk5dt6n7xg6ycwzasjhbxm3iylkiy" 129 | ], 130 | "Threshold": 1, 131 | "VestingDuration": 0, 132 | "VestingStart": 0 133 | } 134 | }, 135 | "RemainderAccount": { 136 | "Type": "multisig", 137 | "Balance": "0", 138 | "Meta": { 139 | "Signers": [ 140 | "t1ceb34gnsc6qk5dt6n7xg6ycwzasjhbxm3iylkiy" 141 | ], 142 | "Threshold": 1, 143 | "VestingDuration": 0, 144 | "VestingStart": 0 145 | } 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /faucet/internal/itests/faucet_test.go: -------------------------------------------------------------------------------- 1 | package itests 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "net/http" 8 | "net/http/httptest" 9 | "os" 10 | "strings" 11 | "testing" 12 | "time" 13 | 14 | datastore "github.com/ipfs/go-ds-leveldb" 15 | logging "github.com/ipfs/go-log/v2" 16 | "github.com/stretchr/testify/require" 17 | ldbopts "github.com/syndtr/goleveldb/leveldb/opt" 18 | 19 | "github.com/filecoin-project/faucet/internal/data" 20 | faucetDB "github.com/filecoin-project/faucet/internal/db" 21 | "github.com/filecoin-project/faucet/internal/faucet" 22 | handler "github.com/filecoin-project/faucet/internal/http" 23 | "github.com/filecoin-project/faucet/internal/itests/kit" 24 | "github.com/filecoin-project/go-address" 25 | ) 26 | 27 | type FaucetTests struct { 28 | handler http.Handler 29 | store *datastore.Datastore 30 | db *faucetDB.Database 31 | faucetCfg *faucet.Config 32 | } 33 | 34 | const ( 35 | FaucetAddr = "f1cp4q4lqsdhob23ysywffg2tvbmar5cshia4rweq" 36 | TestAddr1 = "f1akaouty2buxxwb46l27pzrhl3te2lw5jem67xuy" 37 | TestAddr2 = "f1vfp7yzvwy7ftktnex2cfoz2gpm2jyxlebqpam4q" 38 | storePath = "./_store" 39 | ) 40 | 41 | func Test_Faucet(t *testing.T) { 42 | store, err := datastore.NewDatastore(storePath, &datastore.Options{ 43 | Compression: ldbopts.NoCompression, 44 | NoSync: false, 45 | Strict: ldbopts.StrictAll, 46 | ReadOnly: false, 47 | }) 48 | require.NoError(t, err) 49 | 50 | defer func() { 51 | err = store.Close() 52 | require.NoError(t, err) 53 | err = os.RemoveAll(storePath) 54 | require.NoError(t, err) 55 | }() 56 | 57 | log := logging.Logger("TEST-FAUCET") 58 | 59 | lotus := kit.NewFakeLotusNoCrash() 60 | 61 | addr, err := address.NewFromString(FaucetAddr) 62 | require.NoError(t, err) 63 | 64 | cfg := faucet.Config{ 65 | FaucetAddress: addr, 66 | TotalWithdrawalLimit: 1000, 67 | AddressWithdrawalLimit: 20, 68 | WithdrawalAmount: 10, 69 | } 70 | 71 | srv := handler.FaucetHandler(log, lotus, store, &cfg) 72 | 73 | db := faucetDB.NewDatabase(store) 74 | 75 | tests := FaucetTests{ 76 | handler: srv, 77 | store: store, 78 | db: db, 79 | faucetCfg: &cfg, 80 | } 81 | 82 | t.Run("fundEmptyAddress", tests.emptyAddress) 83 | t.Run("fundAddress201", tests.fundAddress201) 84 | t.Run("fundAddressWithMoreThanAllowed", tests.fundAddressWithMoreThanAllowed) 85 | t.Run("fundAddressWithMoreThanTotal", tests.fundAddressWithMoreThanTotal) 86 | } 87 | 88 | func (ft *FaucetTests) emptyAddress(t *testing.T) { 89 | req := data.FundRequest{Address: ""} 90 | 91 | body, err := json.Marshal(&req) 92 | if err != nil { 93 | t.Fatal(err) 94 | } 95 | 96 | r := httptest.NewRequest(http.MethodPost, "/fund", bytes.NewBuffer(body)) 97 | w := httptest.NewRecorder() 98 | 99 | ft.handler.ServeHTTP(w, r) 100 | 101 | require.Equal(t, http.StatusBadRequest, w.Code) 102 | } 103 | 104 | func (ft *FaucetTests) fundAddress201(t *testing.T) { 105 | req := data.FundRequest{Address: FaucetAddr} 106 | 107 | body, err := json.Marshal(&req) 108 | if err != nil { 109 | t.Fatal(err) 110 | } 111 | 112 | r := httptest.NewRequest(http.MethodPost, "/fund", bytes.NewBuffer(body)) 113 | w := httptest.NewRecorder() 114 | 115 | ft.handler.ServeHTTP(w, r) 116 | 117 | require.Equal(t, http.StatusCreated, w.Code) 118 | } 119 | 120 | // fundAddressWithMoreThanAllowed tests that exceeding daily allowed funds per address is not allowed. 121 | func (ft *FaucetTests) fundAddressWithMoreThanAllowed(t *testing.T) { 122 | targetAddr, err := address.NewFromString(TestAddr1) 123 | require.NoError(t, err) 124 | 125 | err = ft.db.UpdateAddrInfo(context.Background(), targetAddr, data.AddrInfo{ 126 | Amount: ft.faucetCfg.AddressWithdrawalLimit, 127 | LatestWithdrawal: time.Now(), 128 | }) 129 | require.NoError(t, err) 130 | 131 | req := data.FundRequest{Address: TestAddr1} 132 | 133 | body, err := json.Marshal(&req) 134 | if err != nil { 135 | t.Fatal(err) 136 | } 137 | 138 | r := httptest.NewRequest(http.MethodPost, "/fund", bytes.NewBuffer(body)) 139 | w := httptest.NewRecorder() 140 | 141 | ft.handler.ServeHTTP(w, r) 142 | 143 | require.Equal(t, http.StatusInternalServerError, w.Code) 144 | 145 | got := w.Body.String() 146 | exp := faucet.ErrExceedAddrAllowedFunds.Error() 147 | if !strings.Contains(got, exp) { 148 | t.Logf("\t\tTest %s:\tGot : %v", t.Name(), got) 149 | t.Logf("\t\tTest %s:\tExp: %v", t.Name(), exp) 150 | t.Fatalf("\t\tTest %s:\tShould get the expected result.", t.Name()) 151 | } 152 | } 153 | 154 | // fundAddressWithMoreThanAllowed tests that exceeding daily allowed funds per address is not allowed. 155 | func (ft *FaucetTests) fundAddressWithMoreThanTotal(t *testing.T) { 156 | err := ft.db.UpdateTotalInfo(context.Background(), data.TotalInfo{ 157 | Amount: ft.faucetCfg.TotalWithdrawalLimit, 158 | LatestWithdrawal: time.Now(), 159 | }) 160 | require.NoError(t, err) 161 | 162 | req := data.FundRequest{Address: TestAddr2} 163 | 164 | body, err := json.Marshal(&req) 165 | if err != nil { 166 | t.Fatal(err) 167 | } 168 | 169 | r := httptest.NewRequest(http.MethodPost, "/fund", bytes.NewBuffer(body)) 170 | w := httptest.NewRecorder() 171 | 172 | ft.handler.ServeHTTP(w, r) 173 | 174 | require.Equal(t, http.StatusInternalServerError, w.Code) 175 | 176 | got := w.Body.String() 177 | exp := faucet.ErrExceedTotalAllowedFunds.Error() 178 | if !strings.Contains(got, exp) { 179 | t.Logf("\t\tTest %s:\tGot : %v", t.Name(), got) 180 | t.Logf("\t\tTest %s:\tExp: %v", t.Name(), exp) 181 | t.Fatalf("\t\tTest %s:\tShould get the expected result.", t.Name()) 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /faucet/cmd/health/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "expvar" 6 | "fmt" 7 | "net/http" 8 | "os" 9 | "os/signal" 10 | "syscall" 11 | "time" 12 | 13 | "github.com/ardanlabs/conf/v3" 14 | "github.com/gorilla/handlers" 15 | logging "github.com/ipfs/go-log/v2" 16 | "github.com/pkg/errors" 17 | "go.uber.org/zap" 18 | 19 | "github.com/filecoin-project/faucet/internal/failure" 20 | app "github.com/filecoin-project/faucet/internal/http" 21 | "github.com/filecoin-project/faucet/internal/platform/lotus" 22 | "github.com/filecoin-project/faucet/pkg/version" 23 | "github.com/filecoin-project/lotus/api/client" 24 | ) 25 | 26 | func main() { 27 | logger := logging.Logger("SPACENET-HEALTH") 28 | 29 | lvl, err := logging.LevelFromString("info") 30 | if err != nil { 31 | panic(err) 32 | } 33 | logging.SetAllLoggers(lvl) 34 | 35 | if err := run(logger); err != nil { 36 | logger.Fatalln("main: error:", err) 37 | } 38 | } 39 | 40 | func run(log *logging.ZapEventLogger) error { 41 | // ========================================================================= 42 | // Configuration 43 | 44 | cfg := struct { 45 | conf.Version 46 | Web struct { 47 | ReadTimeout time.Duration `conf:"default:5s"` 48 | WriteTimeout time.Duration `conf:"default:60s"` 49 | IdleTimeout time.Duration `conf:"default:120s"` 50 | ShutdownTimeout time.Duration `conf:"default:20s"` 51 | Host string `conf:"default:0.0.0.0:9000"` 52 | } 53 | TLS struct { 54 | Disable bool `conf:"default:true"` 55 | CertFile string `conf:"default:nocert.pem"` 56 | KeyFile string `conf:"default:nokey.pem"` 57 | } 58 | Lotus struct { 59 | APIHost string `conf:"default:127.0.0.1:1230"` 60 | AuthToken string 61 | } 62 | }{ 63 | Version: conf.Version{ 64 | Build: version.Version(), 65 | Desc: "Spacenet Health Service", 66 | }, 67 | } 68 | 69 | help, err := conf.Parse("HEALTH", &cfg) 70 | if err != nil { 71 | if errors.Is(err, conf.ErrHelpWanted) { 72 | fmt.Println(help) 73 | return nil 74 | } 75 | return err 76 | } 77 | 78 | // ========================================================================= 79 | // App Starting 80 | 81 | ctx := context.Background() 82 | 83 | log.Infow("starting service", "version", version.Version()) 84 | defer log.Infow("shutdown complete") 85 | 86 | out, err := conf.String(&cfg) 87 | if err != nil { 88 | return fmt.Errorf("generating config for output: %w", err) 89 | } 90 | log.Infow("startup", "config", out) 91 | 92 | expvar.NewString("build").Set(version.Version()) 93 | 94 | // ========================================================================= 95 | // Initialize authentication support 96 | 97 | log.Infow("startup", "status", "initializing authentication support") 98 | 99 | var authToken string 100 | 101 | if cfg.Lotus.AuthToken == "" { 102 | authToken, err = lotus.GetToken() 103 | if err != nil { 104 | return fmt.Errorf("error getting authentication token: %w", err) 105 | } 106 | } else { 107 | authToken = cfg.Lotus.AuthToken 108 | } 109 | header := http.Header{"Authorization": []string{"Bearer " + authToken}} 110 | 111 | // ========================================================================= 112 | // Start Lotus client 113 | 114 | log.Infow("startup", "status", "initializing Lotus support", "host", cfg.Lotus.APIHost) 115 | 116 | lotusClient, lotusCloser, err := client.NewFullNodeRPCV1(ctx, "ws://"+cfg.Lotus.APIHost+"/rpc/v1", header) 117 | if err != nil { 118 | return fmt.Errorf("connecting to Lotus failed: %w", err) 119 | } 120 | defer func() { 121 | log.Infow("shutdown", "status", "stopping Lotus client support") 122 | lotusCloser() 123 | }() 124 | 125 | log.Infow("Successfully connected to Lotus node") 126 | 127 | // ========================================================================= 128 | // Start Detector Service 129 | 130 | d := failure.NewDetector(log, lotusClient, time.Minute, 3*time.Minute) 131 | 132 | // ========================================================================= 133 | // Start API Service 134 | 135 | log.Infow("startup", "status", "initializing HTTP API support") 136 | 137 | shutdown := make(chan os.Signal, 1) 138 | signal.Notify(shutdown, syscall.SIGINT, syscall.SIGTERM) 139 | 140 | api := http.Server{ 141 | Addr: cfg.Web.Host, 142 | Handler: handlers.RecoveryHandler()(app.HealthHandler(log, lotusClient, d, version.Version())), 143 | ReadTimeout: cfg.Web.ReadTimeout, 144 | WriteTimeout: cfg.Web.WriteTimeout, 145 | IdleTimeout: cfg.Web.IdleTimeout, 146 | ErrorLog: zap.NewStdLog(log.Desugar()), 147 | } 148 | 149 | serverErrors := make(chan error, 1) 150 | 151 | go func() { 152 | log.Infow("startup", "status", "api router started", "host", api.Addr) 153 | switch cfg.TLS.Disable { 154 | case true: 155 | serverErrors <- api.ListenAndServe() 156 | case false: 157 | serverErrors <- api.ListenAndServeTLS(cfg.TLS.CertFile, cfg.TLS.KeyFile) 158 | } 159 | }() 160 | 161 | // ========================================================================= 162 | // Shutdown 163 | 164 | select { 165 | case err := <-serverErrors: 166 | return fmt.Errorf("server error: %w", err) 167 | 168 | case sig := <-shutdown: 169 | log.Infow("shutdown", "status", "shutdown started", "signal", sig) 170 | defer log.Infow("shutdown", "status", "shutdown complete", "signal", sig) 171 | 172 | ctx, cancel := context.WithTimeout(ctx, cfg.Web.ShutdownTimeout) 173 | defer cancel() 174 | 175 | d.Stop() 176 | 177 | if err := api.Shutdown(ctx); err != nil { 178 | api.Close() // nolint 179 | return fmt.Errorf("could not stop server gracefully: %w", err) 180 | } 181 | } 182 | return nil 183 | } 184 | -------------------------------------------------------------------------------- /faucet/cmd/faucet/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "expvar" 7 | "fmt" 8 | "net/http" 9 | "os" 10 | "os/signal" 11 | "syscall" 12 | "time" 13 | 14 | "github.com/ardanlabs/conf/v3" 15 | "github.com/gorilla/handlers" 16 | datastore "github.com/ipfs/go-ds-leveldb" 17 | logging "github.com/ipfs/go-log/v2" 18 | "github.com/pkg/errors" 19 | ldbopts "github.com/syndtr/goleveldb/leveldb/opt" 20 | "go.uber.org/zap" 21 | 22 | "github.com/filecoin-project/faucet/internal/faucet" 23 | app "github.com/filecoin-project/faucet/internal/http" 24 | "github.com/filecoin-project/faucet/internal/platform/lotus" 25 | "github.com/filecoin-project/go-address" 26 | "github.com/filecoin-project/lotus/api/client" 27 | ) 28 | 29 | var build = "develop" 30 | 31 | func main() { 32 | logger := logging.Logger("SPACENET-FAUCET") 33 | 34 | lvl, err := logging.LevelFromString("info") 35 | if err != nil { 36 | panic(err) 37 | } 38 | logging.SetAllLoggers(lvl) 39 | 40 | if err := run(logger); err != nil { 41 | logger.Fatalln("main: error:", err) 42 | } 43 | } 44 | 45 | func run(log *logging.ZapEventLogger) error { 46 | // ========================================================================= 47 | // Configuration 48 | 49 | cfg := struct { 50 | conf.Version 51 | Web struct { 52 | ReadTimeout time.Duration `conf:"default:5s"` 53 | WriteTimeout time.Duration `conf:"default:60s"` 54 | IdleTimeout time.Duration `conf:"default:120s"` 55 | ShutdownTimeout time.Duration `conf:"default:20s"` 56 | Host string `conf:"default:0.0.0.0:8000"` 57 | BackendHost string `conf:"required"` 58 | AllowedOrigins []string `conf:"required"` 59 | } 60 | TLS struct { 61 | Disable bool `conf:"default:true"` 62 | CertFile string `conf:"default:nocert.pem"` 63 | KeyFile string `conf:"default:nokey.pem"` 64 | } 65 | Filecoin struct { 66 | Address string `conf:"default:t1jlm55oqkdalh2l3akqfsaqmpjxgjd36pob34dqy"` 67 | // Amount of tokens that below is in FIL. 68 | TotalWithdrawalLimit uint64 `conf:"default:10000"` 69 | AddressWithdrawalLimit uint64 `conf:"default:20"` 70 | WithdrawalAmount uint64 `conf:"default:10"` 71 | } 72 | Lotus struct { 73 | APIHost string `conf:"default:127.0.0.1:1230"` 74 | AuthToken string 75 | } 76 | DB struct { 77 | Path string `conf:"default:./_db_data"` 78 | Readonly bool `conf:"default:false"` 79 | } 80 | }{ 81 | Version: conf.Version{ 82 | Build: build, 83 | Desc: "Spacenet Faucet Service", 84 | }, 85 | } 86 | 87 | const prefix = "FAUCET" 88 | help, err := conf.Parse(prefix, &cfg) 89 | if err != nil { 90 | if errors.Is(err, conf.ErrHelpWanted) { 91 | fmt.Println(help) 92 | return nil 93 | } 94 | return err 95 | } 96 | 97 | // ========================================================================= 98 | // App Starting 99 | 100 | ctx := context.Background() 101 | 102 | log.Infow("starting service", "version", build) 103 | defer log.Infow("shutdown complete") 104 | 105 | out, err := conf.String(&cfg) 106 | if err != nil { 107 | return fmt.Errorf("generating config for output: %w", err) 108 | } 109 | log.Infow("startup", "config", out) 110 | 111 | expvar.NewString("build").Set(build) 112 | 113 | // ========================================================================= 114 | // Database Support 115 | 116 | log.Infow("startup", "status", "initializing database support", "path", cfg.DB.Path) 117 | 118 | db, err := datastore.NewDatastore(cfg.DB.Path, &datastore.Options{ 119 | Compression: ldbopts.NoCompression, 120 | NoSync: false, 121 | Strict: ldbopts.StrictAll, 122 | ReadOnly: cfg.DB.Readonly, 123 | }) 124 | if err != nil { 125 | return fmt.Errorf("couldn't initialize leveldb database: %w", err) 126 | } 127 | 128 | defer func() { 129 | log.Infow("shutdown", "status", "stopping leveldb") 130 | err = db.Close() 131 | if err != nil { 132 | log.Errorf("closing DB error: %s", err) 133 | } 134 | }() 135 | 136 | // ========================================================================= 137 | // Initialize authentication support 138 | 139 | log.Infow("startup", "status", "initializing authentication support") 140 | 141 | var authToken string 142 | 143 | if cfg.Lotus.AuthToken == "" { 144 | authToken, err = lotus.GetToken() 145 | if err != nil { 146 | return fmt.Errorf("error getting authentication token: %w", err) 147 | } 148 | } else { 149 | authToken = cfg.Lotus.AuthToken 150 | } 151 | header := http.Header{"Authorization": []string{"Bearer " + authToken}} 152 | 153 | // ========================================================================= 154 | // Start Lotus client 155 | 156 | faucetAddr, err := address.NewFromString(cfg.Filecoin.Address) 157 | if err != nil { 158 | return fmt.Errorf("failed to parse Faucet address: %w", err) 159 | } 160 | 161 | log.Infow("startup", "status", "initializing Lotus support", "host", cfg.Lotus.APIHost) 162 | 163 | lotusNode, lotusCloser, err := client.NewFullNodeRPCV1(ctx, "ws://"+cfg.Lotus.APIHost+"/rpc/v1", header) 164 | if err != nil { 165 | return fmt.Errorf("connecting to Lotus failed: %w", err) 166 | } 167 | defer func() { 168 | log.Infow("shutdown", "status", "stopping Lotus client support") 169 | lotusCloser() 170 | }() 171 | 172 | log.Infow("Successfully connected to Lotus node") 173 | 174 | // sanity-check to see if the node owns the key. 175 | if err := lotus.VerifyWallet(ctx, lotusNode, faucetAddr); err != nil { 176 | return fmt.Errorf("faucet wallet sanity-check failed: %w", err) 177 | } 178 | 179 | // ========================================================================= 180 | // Start API Service 181 | 182 | log.Infow("startup", "status", "initializing HTTP API support") 183 | 184 | shutdown := make(chan os.Signal, 1) 185 | signal.Notify(shutdown, syscall.SIGINT, syscall.SIGTERM) 186 | 187 | var tlsConfig *tls.Config 188 | if !cfg.TLS.Disable { 189 | cert, err := tls.LoadX509KeyPair(cfg.TLS.CertFile, cfg.TLS.KeyFile) 190 | if err != nil { 191 | return fmt.Errorf("failed to load TLS key pair: %w", err) 192 | } 193 | tlsConfig = &tls.Config{ 194 | MinVersion: tls.VersionTLS12, 195 | Certificates: []tls.Certificate{cert}, 196 | } 197 | } 198 | 199 | api := http.Server{ 200 | TLSConfig: tlsConfig, 201 | Addr: cfg.Web.Host, 202 | Handler: handlers.RecoveryHandler()(app.FaucetHandler(log, lotusNode, db, &faucet.Config{ 203 | FaucetAddress: faucetAddr, 204 | AllowedOrigins: cfg.Web.AllowedOrigins, 205 | BackendAddress: cfg.Web.BackendHost, 206 | TotalWithdrawalLimit: cfg.Filecoin.TotalWithdrawalLimit, 207 | AddressWithdrawalLimit: cfg.Filecoin.AddressWithdrawalLimit, 208 | WithdrawalAmount: cfg.Filecoin.WithdrawalAmount, 209 | })), 210 | ReadTimeout: cfg.Web.ReadTimeout, 211 | WriteTimeout: cfg.Web.WriteTimeout, 212 | IdleTimeout: cfg.Web.IdleTimeout, 213 | ErrorLog: zap.NewStdLog(log.Desugar()), 214 | } 215 | 216 | serverErrors := make(chan error, 1) 217 | 218 | go func() { 219 | log.Infow("startup", "status", "api router started", "host", api.Addr) 220 | switch cfg.TLS.Disable { 221 | case true: 222 | serverErrors <- api.ListenAndServe() 223 | case false: 224 | serverErrors <- api.ListenAndServeTLS(cfg.TLS.CertFile, cfg.TLS.KeyFile) 225 | } 226 | }() 227 | 228 | // ========================================================================= 229 | // Shutdown 230 | 231 | select { 232 | case err := <-serverErrors: 233 | return fmt.Errorf("server error: %w", err) 234 | 235 | case sig := <-shutdown: 236 | log.Infow("shutdown", "status", "shutdown started", "signal", sig) 237 | defer log.Infow("shutdown", "status", "shutdown complete", "signal", sig) 238 | 239 | ctx, cancel := context.WithTimeout(ctx, cfg.Web.ShutdownTimeout) 240 | defer cancel() 241 | 242 | if err := api.Shutdown(ctx); err != nil { 243 | api.Close() // nolint 244 | return fmt.Errorf("could not stop server gracefully: %w", err) 245 | } 246 | } 247 | return nil 248 | } 249 | -------------------------------------------------------------------------------- /faucet/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/filecoin-project/faucet 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/ardanlabs/conf/v3 v3.1.2 7 | github.com/filecoin-project/go-address v1.1.0 8 | github.com/filecoin-project/go-state-types v0.10.0 9 | github.com/filecoin-project/lotus v1.20.1 10 | github.com/gorilla/handlers v1.5.1 11 | github.com/gorilla/mux v1.8.0 12 | github.com/ipfs/go-cid v0.3.2 13 | github.com/ipfs/go-datastore v0.6.0 14 | github.com/ipfs/go-ds-leveldb v0.5.0 15 | github.com/ipfs/go-log/v2 v2.5.1 16 | github.com/libp2p/go-libp2p v0.23.4 17 | github.com/multiformats/go-multiaddr v0.8.0 18 | github.com/pkg/errors v0.9.1 19 | github.com/rs/cors v1.7.0 20 | github.com/stretchr/testify v1.8.1 21 | github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 22 | go.uber.org/zap v1.23.0 23 | ) 24 | 25 | require ( 26 | github.com/DataDog/zstd v1.4.5 // indirect 27 | github.com/GeertJohan/go.incremental v1.0.0 // indirect 28 | github.com/GeertJohan/go.rice v1.0.3 // indirect 29 | github.com/StackExchange/wmi v1.2.1 // indirect 30 | github.com/akavel/rsrc v0.8.0 // indirect 31 | github.com/benbjohnson/clock v1.3.0 // indirect 32 | github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect 33 | github.com/crackcomm/go-gitignore v0.0.0-20170627025303-887ab5e44cc3 // indirect 34 | github.com/daaku/go.zipexe v1.0.2 // indirect 35 | github.com/davecgh/go-spew v1.1.1 // indirect 36 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect 37 | github.com/felixge/httpsnoop v1.0.1 // indirect 38 | github.com/filecoin-project/go-amt-ipld/v2 v2.1.0 // indirect 39 | github.com/filecoin-project/go-amt-ipld/v3 v3.1.0 // indirect 40 | github.com/filecoin-project/go-amt-ipld/v4 v4.0.0 // indirect 41 | github.com/filecoin-project/go-bitfield v0.2.4 // indirect 42 | github.com/filecoin-project/go-cbor-util v0.0.1 // indirect 43 | github.com/filecoin-project/go-crypto v0.0.1 // indirect 44 | github.com/filecoin-project/go-data-transfer v1.15.2 // indirect 45 | github.com/filecoin-project/go-fil-markets v1.25.2 // indirect 46 | github.com/filecoin-project/go-hamt-ipld v0.1.5 // indirect 47 | github.com/filecoin-project/go-hamt-ipld/v2 v2.0.0 // indirect 48 | github.com/filecoin-project/go-hamt-ipld/v3 v3.1.0 // indirect 49 | github.com/filecoin-project/go-jsonrpc v0.2.1 // indirect 50 | github.com/filecoin-project/go-padreader v0.0.1 // indirect 51 | github.com/filecoin-project/go-statestore v0.2.0 // indirect 52 | github.com/filecoin-project/specs-actors v0.9.15 // indirect 53 | github.com/filecoin-project/specs-actors/v2 v2.3.6 // indirect 54 | github.com/filecoin-project/specs-actors/v3 v3.1.2 // indirect 55 | github.com/filecoin-project/specs-actors/v4 v4.0.2 // indirect 56 | github.com/filecoin-project/specs-actors/v5 v5.0.6 // indirect 57 | github.com/filecoin-project/specs-actors/v6 v6.0.2 // indirect 58 | github.com/filecoin-project/specs-actors/v7 v7.0.1 // indirect 59 | github.com/gbrlsnchs/jwt/v3 v3.0.1 // indirect 60 | github.com/go-logr/logr v1.2.3 // indirect 61 | github.com/go-logr/stdr v1.2.2 // indirect 62 | github.com/go-ole/go-ole v1.2.6 // indirect 63 | github.com/gogo/protobuf v1.3.2 // indirect 64 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 65 | github.com/golang/mock v1.6.0 // indirect 66 | github.com/golang/snappy v0.0.4 // indirect 67 | github.com/google/uuid v1.3.0 // indirect 68 | github.com/gorilla/websocket v1.5.0 // indirect 69 | github.com/hannahhoward/cbor-gen-for v0.0.0-20200817222906-ea96cece81f1 // indirect 70 | github.com/hannahhoward/go-pubsub v0.0.0-20200423002714-8d62886cc36e // indirect 71 | github.com/hashicorp/golang-lru v0.5.4 // indirect 72 | github.com/icza/backscanner v0.0.0-20210726202459-ac2ffc679f94 // indirect 73 | github.com/ipfs/bbloom v0.0.4 // indirect 74 | github.com/ipfs/go-block-format v0.1.1 // indirect 75 | github.com/ipfs/go-blockservice v0.4.0 // indirect 76 | github.com/ipfs/go-graphsync v0.13.2 // indirect 77 | github.com/ipfs/go-ipfs-blockstore v1.2.0 // indirect 78 | github.com/ipfs/go-ipfs-cmds v0.7.0 // indirect 79 | github.com/ipfs/go-ipfs-ds-help v1.1.0 // indirect 80 | github.com/ipfs/go-ipfs-exchange-interface v0.2.0 // indirect 81 | github.com/ipfs/go-ipfs-files v0.1.1 // indirect 82 | github.com/ipfs/go-ipfs-http-client v0.4.0 // indirect 83 | github.com/ipfs/go-ipfs-util v0.0.2 // indirect 84 | github.com/ipfs/go-ipld-cbor v0.0.6 // indirect 85 | github.com/ipfs/go-ipld-format v0.4.0 // indirect 86 | github.com/ipfs/go-ipld-legacy v0.1.1 // indirect 87 | github.com/ipfs/go-libipfs v0.4.1 // indirect 88 | github.com/ipfs/go-log v1.0.5 // indirect 89 | github.com/ipfs/go-merkledag v0.8.1 // indirect 90 | github.com/ipfs/go-metrics-interface v0.0.1 // indirect 91 | github.com/ipfs/go-path v0.3.0 // indirect 92 | github.com/ipfs/go-unixfs v0.4.0 // indirect 93 | github.com/ipfs/go-verifcid v0.0.2 // indirect 94 | github.com/ipfs/interface-go-ipfs-core v0.7.0 // indirect 95 | github.com/ipld/go-car v0.4.0 // indirect 96 | github.com/ipld/go-codec-dagpb v1.5.0 // indirect 97 | github.com/ipld/go-ipld-prime v0.20.0 // indirect 98 | github.com/ipld/go-ipld-selector-text-lite v0.0.1 // indirect 99 | github.com/ipsn/go-secp256k1 v0.0.0-20180726113642-9d62b9f0bc52 // indirect 100 | github.com/jbenet/goprocess v0.1.4 // indirect 101 | github.com/jessevdk/go-flags v1.4.0 // indirect 102 | github.com/jpillora/backoff v1.0.0 // indirect 103 | github.com/klauspost/cpuid/v2 v2.1.1 // indirect 104 | github.com/libp2p/go-buffer-pool v0.1.0 // indirect 105 | github.com/libp2p/go-flow-metrics v0.1.0 // indirect 106 | github.com/libp2p/go-libp2p-core v0.20.1 // indirect 107 | github.com/libp2p/go-libp2p-pubsub v0.8.2 // indirect 108 | github.com/libp2p/go-msgio v0.2.0 // indirect 109 | github.com/libp2p/go-openssl v0.1.0 // indirect 110 | github.com/magefile/mage v1.9.0 // indirect 111 | github.com/mattn/go-isatty v0.0.16 // indirect 112 | github.com/mattn/go-pointer v0.0.1 // indirect 113 | github.com/miekg/dns v1.1.50 // indirect 114 | github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1 // indirect 115 | github.com/minio/sha256-simd v1.0.0 // indirect 116 | github.com/mitchellh/go-homedir v1.1.0 // indirect 117 | github.com/mr-tron/base58 v1.2.0 // indirect 118 | github.com/multiformats/go-base32 v0.1.0 // indirect 119 | github.com/multiformats/go-base36 v0.1.0 // indirect 120 | github.com/multiformats/go-multiaddr-dns v0.3.1 // indirect 121 | github.com/multiformats/go-multibase v0.1.1 // indirect 122 | github.com/multiformats/go-multicodec v0.8.0 // indirect 123 | github.com/multiformats/go-multihash v0.2.1 // indirect 124 | github.com/multiformats/go-varint v0.0.6 // indirect 125 | github.com/nkovacs/streamquote v1.0.0 // indirect 126 | github.com/opentracing/opentracing-go v1.2.0 // indirect 127 | github.com/pmezard/go-difflib v1.0.0 // indirect 128 | github.com/polydawn/refmt v0.0.0-20201211092308-30ac6d18308e // indirect 129 | github.com/raulk/clock v1.1.0 // indirect 130 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 131 | github.com/shirou/gopsutil v2.18.12+incompatible // indirect 132 | github.com/spacemonkeygo/spacelog v0.0.0-20180420211403-2296661a0572 // indirect 133 | github.com/spaolacci/murmur3 v1.1.0 // indirect 134 | github.com/urfave/cli/v2 v2.16.3 // indirect 135 | github.com/valyala/bytebufferpool v1.0.0 // indirect 136 | github.com/valyala/fasttemplate v1.0.1 // indirect 137 | github.com/whyrusleeping/bencher v0.0.0-20190829221104-bb6607aa8bba // indirect 138 | github.com/whyrusleeping/cbor-gen v0.0.0-20221021053955-c138aae13722 // indirect 139 | github.com/whyrusleeping/timecache v0.0.0-20160911033111-cfcb2f1abfee // indirect 140 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect 141 | go.opencensus.io v0.23.0 // indirect 142 | go.opentelemetry.io/otel v1.11.1 // indirect 143 | go.opentelemetry.io/otel/trace v1.11.1 // indirect 144 | go.uber.org/atomic v1.10.0 // indirect 145 | go.uber.org/multierr v1.8.0 // indirect 146 | golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 // indirect 147 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect 148 | golang.org/x/net v0.0.0-20220920183852-bf014ff85ad5 // indirect 149 | golang.org/x/sys v0.5.0 // indirect 150 | golang.org/x/tools v0.1.12 // indirect 151 | golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect 152 | google.golang.org/protobuf v1.28.1 // indirect 153 | gopkg.in/yaml.v3 v3.0.1 // indirect 154 | lukechampine.com/blake3 v1.1.7 // indirect 155 | ) 156 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](./assets/spacenet-header.png) 2 | 3 | > ‼️ Spacenet has been deprecated in favour of Calibration as the recommended root network. This repo is now archived, its content stale. 4 | 5 | # Spacenet 6 | > A new-generation Filecoin IPC testnet. 7 | > 8 | > Made with ❤ by [ConsensusLab](https://consensuslab.world/) 9 | 10 | - [Spacenet Faucet](https://faucet.spacenet.ipc.space) 11 | - [Spacenet Genesis](https://github.com/consensus-shipyard/lotus/blob/spacenet/build/genesis/spacenet.car) 12 | - [Spacenet Bootstraps](https://github.com/consensus-shipyard/lotus/blob/spacenet/build/bootstrap/spacenet.pi) 13 | - [Spacenet status page](https://spacenet.statuspage.io/) 14 | 15 | [![Tests][tests-badge]][tests-url] 16 | [![Linting][lint-badge]][lint-url] 17 | 18 | > Check out the `spacenet` branch to connect to Spacenet. 19 | 20 | ## Why Spacenet? 21 | Spacenet is not _yet another_ Filecoin testnet. Its consensus layer has been modified to integrate [Mir](https://github.com/filecoin-project/mir), a distributed protocol implementation framework. The current version of Spacenet runs an implementation of the [Trantor BFT consensus](https://hackmd.io/P59lk4hnSBKN5ki5OblSFg) over Mir. And did we forget to mention Spacenet comes with [Interplanetary Consensus (IPC)](https://ipc.space/) support? 22 | 23 | Spacenet aims to provide developers with a testbed to deploy their FVM use cases and innovate with new Web3 applications that can benefit from operating in subnets. It is also a way for us to test our consensus innovations with real applications and real users. Developers will be able to deploy their own subnets from Spacenet while maintaining the ability to seamlessly interact with state and applications in the original network, from which they have otherwise become independent. 24 | 25 | > In the meantime, to learn more about IPC you can read [the design reference](https://github.com/consensus-shipyard/IPC-design-reference-spec/raw/main/main.pdf) and/or [watch this (slightly outdated) talk](https://www.youtube.com/watch?v=bD1LDVc2lMQ&list=PLhuBigpl7lqu0bsMQ8K7aLfmUFrkMw52K&index=3): 26 | 27 | [![Watch the video](https://img.youtube.com/vi/bD1LDVc2lMQ/hqdefault.jpg)](https://youtu.be/bD1LDVc2lMQ) 28 | 29 | ## SLA of the network 30 | Spacenet is an experimental network. We aim to have it constantly running, but some hiccups may appear along the way. If you are looking to rely on Spacenet for your applications, you should expect: 31 | - Unplanned (and potentially long-lasting) downtime while we investigate bugs. 32 | - Complete restarts of the network and loss of part or all stored state. In case of serious issues, it may be necessary to restart the network from a previous checkpoint, or completely restart from genesis. 33 | - Bugs and rough edges to be fixed and polished along the way. 34 | 35 | Announcements about new releases and status updates about the network are given in the **#spacenet** channel of the [Filecoin Slack](https://filecoin.io/slack) and through this repo. You can also ping us there or open an issue in this repo if you encounter a bug or some other issue with the network. You can also direct your requests through [this form](https://docs.google.com/forms/d/1O3_kHb2WJhil9sqXOxgGGGsqkAA61J1rKMfnb5os5yo/edit). 36 | 37 | ## Getting started for users 38 | Spacenet is a Filecoin testnet, and as such it is supposed to do (almost) everything that the [Filecoin network supports](https://lotus.filecoin.io/tutorials/lotus/store-and-retrieve/set-up/): 39 | - Send FIL between addresses 40 | - Create multisig accounts 41 | - Create [payment channels](https://lotus.filecoin.io/tutorials/lotus/payment-channels/) 42 | - Deploy [FVM compatible contracts](https://docs.filecoin.io/smart-contracts/fundamentals/the-filecoin-virtual-machine/) 43 | - Deploy [IPC subnets](https://github.com/consensus-shipyard/ipc-agent) 44 | 45 | That being said, as the consensus layer is no longer storage-dependent, Spacenet has limited support for storage-related features. In particular, we have stripped out some of the functionalities of the lotus miner. While you deploy a lotus-miner over Spacenet to onboard storage to the network and perform deals, lotus-miners are not allowed to propose and validate blocks anymore (this is handled by Mir-Trantor validators). 46 | 47 | > ⚠️ Support for storage-specific features in Spacenet is limited. 48 | 49 | ### Getting Spacenet FIL 50 | In order to fund your account with Spacenet FIL we provide a faucet at [https://spacenet.consensus.ninja](https://spacenet.consensus.ninja). Getting FIL is as simple as inputting your address in the textbox and clicking the button. 51 | - The per-request allowance given by the faucet is of 10 FIL. 52 | - There is a daily maximum of 20 FIL per address. 53 | - And we have also limited the maximum amount of funds that the faucet can withdraw daily. 54 | If, for some reason, you require more Spacenet FIL for your application, feel free to drop us a message in the #spacenet Slack channel, via consensuslab@protocol.ai to increase your allowance, or fill-in a request in [this form](https://docs.google.com/forms/d/1O3_kHb2WJhil9sqXOxgGGGsqkAA61J1rKMfnb5os5yo/edit). 55 | ![](./assets/spacenet-faucet.png) 56 | 57 | ## Getting started for developers 58 | You can run a full-node and connect it to Spacenet by running eudico (a fork of lotus that is able to run several consensus algorithms): 59 | - Cloning the modified lotus implementation (eudico) for Spacenet 60 | ``` 61 | git clone --branch spacenet https://github.com/consensus-shipyard/lotus 62 | ``` 63 | - Installing lotus and running all dependencies as described in the `README` of the [repo](https://github.com/consensus-shipyard/lotus) 64 | - Once you have all `lotus` dependencies installed you can run the following command to compile `eudico` with Spacenet support. 65 | ``` 66 | make spacenet 67 | ``` 68 | - With that, you are ready to run your spacenet daemon and connect to the network by connecting to any its bootstrap nodes. 69 | ``` 70 | ./eudico mir daemon --bootstrap=true 71 | ``` 72 | Eudico in Spacenet supports every lotus command supported in mainnet, so you'll be able to configure your Spacenet full-node at will (by exposing a different API port, running Lotus lite, etc.). More info available in [Lotus' docs](https://lotus.filecoin.io/lotus/get-started/what-is-lotus/). 73 | 74 | ### Using eudico Lite for Spacenet 75 | The Spacenet blockchain is growing fast in size! If you are looking to tinker a bit with the network and get some Spacenet FIL, but you are not planning to extensively use the network to the extent of running your own full-node, we have provided a read endpoint so you can interace with Spacenet through an Eudico Lite node. 76 | 77 | To connect to Spacenet through a Eudico Lite you need to configure `FULLNODE_API_INFO` to point to the following peer with the following `API_KEY`: 78 | ``` 79 | FULLNODE_API_INFO=/dns4/api.spacenet.ipc.space/tcp/1234/http ./eudico mir daemon --lite 80 | ``` 81 | To test that the connection has been successful you can try to create a new wallet and send some funds from the faucet. More info about Lotus/Eudico Lite can be found [here](https://lotus.filecoin.io/lotus/install/lotus-lite/) 82 | 83 | > 📓 We are only providing read access through our current Eudico Lite endpoint, if you would like to have write or admin access to a Spacenet full-node to test the network without having to sync your own node get in touch in FIL Slack's #spacenet. 84 | 85 | In future versions of Spacenet, we will provide periodic snapshots to help developers sync their full-nodes in a tractable amount of time. You can follow the progress of this feature in the [following issue](https://github.com/consensus-shipyard/spacenet/issues/18) 86 | 87 | ## Getting started with IPC 88 | 89 | Spacenet now features support for IPC subnets. You're able to create, join, and leave subnets, to operate as a subnet validator, and to issue transactions and deploy smart contracts in subnets. 90 | 91 | The instuctions in the [IPC Agent](https://github.com/consensus-shipyard/ipc-agent) repository will guide you through the deployment of an IPC subnet under Spacenet. 92 | 93 | ## Getting started for validators 94 | 95 | > Support for external validators coming soon! Track the work in [the following issue](https://github.com/consensus-shipyard/lotus/issues/21). If you are interested in becoming a validator let us know via `ipc@protocol.ai`. 96 | 97 | Spacenet is currently run by a committee of 4 validators owned by CL. We don't accept externally owned validators during this initial testing phase, until the network deployment is stabilized, but support for reconfiguration and external validators will be added soon. 98 | 99 | 100 | [lint-url]: https://github.com/consensus-shipyard/spacenet/actions/workflows/lint.yml 101 | [lint-badge]: https://github.com/consensus-shipyard/spacenet/actions/workflows/lint.yml/badge.svg?branch=main 102 | 103 | [tests-url]: https://github.com/consensus-shipyard/spacenet/actions/workflows/test.yml 104 | [tests-badge]: https://github.com/consensus-shipyard/spacenet/actions/workflows/test.yml/badge.svg?branch=main 105 | -------------------------------------------------------------------------------- /deployment/README.md: -------------------------------------------------------------------------------- 1 | # Deploying Spacenet 2 | 3 | We use [Ansible](https://www.ansible.com/) to deploy Spacenet nodes (and whole networks). 4 | The set of machines on which to deploy Spacenet must be defined in an Ansible inventory file 5 | (we use `hosts` as an example inventory file name in this document, but any other file name is also allowed). 6 | The inventory file must contain 2 host groups: `bootstrap` (with 1 host) and `validators` (with all validator hosts). 7 | An example host file looks as follows. 8 | ``` 9 | [bootstrap] 10 | 198.51.100.0 11 | 12 | [validators] 13 | 198.51.100.1 14 | 198.51.100.2 15 | 198.51.100.3 16 | 198.51.100.4 17 | ``` 18 | 19 | The Spacenet deployment can be managed using the provided Ansible playbooks. 20 | To run a playbook, install Ansible and execute the following command 21 | ```shell 22 | ansible-playbook -i hosts ... 23 | ``` 24 | with `hosts` being the Ansible inventory and `` one of the provided playbooks. 25 | Additional playbooks can be specified in the same command and will be executed in the given sequence. 26 | A reference of the provided deployment playbooks is given at the end of this document. 27 | 28 | ## Choosing deployment targets 29 | 30 | Running the command above applies the playbooks to their default targets, 31 | assuming all nodes in the inventory are part of Spacenet. 32 | To target specific nodes from the inventory, the `nodes` Ansible variable can be used 33 | through specifying an additional parameter `--extra-vars "nodes=''"`. 34 | For example the following commands, respectively, 35 | only set up the bootstrap node and only kill the validators `198.51.100.3` and `198.51.100.4`. 36 | ```shell 37 | ansible-playbook -i hosts setup.yaml --extra-vars "nodes=bootstrap" 38 | ansible-playbook -i hosts kill.yaml --extra-vars "nodes='198.51.100.3 198.51.100.4'" 39 | ``` 40 | 41 | ## Ansible parallelism 42 | 43 | By default, ansible communicates with 5 remote nodes at a time. 44 | This is fine for, say 4 validators and 1 bootstrap, but as soon as more nodes are involved, 45 | it slows down the deployment significantly. 46 | To increase the number of parallel ansible connections, use the `--forks` command-line argument. 47 | 48 | ```shell 49 | ansible-playbook -i hosts --forks 10 ... 50 | ``` 51 | 52 | ## System requirements and configuration 53 | 54 | - Ansible installed on the local machine. 55 | - Python 3 (command `python3`) installed on the local machine. 56 | - Ubuntu 22.04 on all remote machines (might easily work with other systems, but was tested with this one). 57 | - Sudo access without password on remote machines. 58 | - SSH access to remote machines without password 59 | 60 | The file [group_vars/all.yaml](group_vars/all.yaml) contains some configuration parameters 61 | (e.g. the location of the SSH key to use for accessing remote machines) documented therein. 62 | 63 | ### Potential issue on Ubuntu 22.04 64 | 65 | While testing the deployment on Amazon EC2 virtual machines, 66 | we noticed that installing dependencies on remote machines (performed by the `setup.yaml` playbook) sometimes failed. 67 | The issue and solution has been 68 | [described here](https://askubuntu.com/questions/1431786/grub-efi-amd64-signed-dependency-issue-in-ubuntu-22-04lts). 69 | To apply the work-around, the `custom-script.yaml` playbook can be used. 70 | If necessary, copy the following line 71 | ```shell 72 | sudo apt --only-upgrade install grub-efi-amd64-signed 73 | ``` 74 | in the [scripts/custom.sh](scripts/custom.sh) file run 75 | ```shell 76 | ansible-playbook -i hosts custom-script.yaml 77 | ``` 78 | 79 | ## Deploying a fresh instance of Spacenet 80 | 81 | To deploy an instance of Spacenet, first create an inventory file (called `hosts` in this example) 82 | and populate it with IP addresses of machines that should run Spacenet as described above. 83 | 84 | The following steps must be executed to deploy Spacenet: 85 | 1. Install necessary packages, 86 | clone the Spacenet client (Lotus) code and compile it on the remote machines (`setup.yaml`). 87 | 2. Start the bootstrap node (`start-bootstrap.yaml`) 88 | 3. Start the Lotus daemons on validator nodes (`start-daemons.yaml`) 89 | 4. Start the Mir validator processes on validator nodes (`start-validators.yaml`) 90 | 91 | These steps are automated for convenience in the `deploy-new.yaml` playbook. 92 | Thus, to deploy a fresh instance of Spacenet, simply run 93 | ```shell 94 | ansible-playbook -i hosts deploy-new.yaml 95 | ``` 96 | 97 | ## Rolling updates 98 | 99 | When the Lotus code, the validator code, or the Mir code are updated, 100 | the update can be rolled out to the running deployment, as long as the protocol remains the same. 101 | For this, we provide the `rolling-update.sh` script. 102 | This script performs a rolling update of selected nodes from an Ansible inventory and is invoked as follows. 103 | 104 | ```shell 105 | ./rolling-update hosts 198.51.100.1 [198.51.100.2 [...]] 106 | ``` 107 | 108 | The first argument must be an Ansible inventory file that contains all the node arguments that follow. 109 | This script updates (fetches the code, recompiles it, and restarts the node, using the update-nodes.yaml) 110 | the nodes one by one, always waiting for a node to catch up with the others 111 | and only then proceeding to updating the next one. 112 | 113 | ## Provided deployment playbooks 114 | 115 | ### `clean.yaml` 116 | 117 | Kills running Lotus daemon and Mir validator and deletes their associated state. 118 | Does not touch the code and binaries. 119 | 120 | Applies to all hosts by default, unless other nodes are specified using --extra-vars "nodes=..." 121 | 122 | ### `connect-daemons.yaml` 123 | 124 | Connects all Lotus daemons to each other. This is required for the nodes to be able to sync their state. 125 | It assumes the daemons and the bootstrap are up and running (but not necessarily the validators) 126 | 127 | Applies to all hosts (including bootstrap). 128 | 129 | ### `custom-script.yaml` 130 | 131 | Runs the scripts/custom.sh script. This is meant as a convenience tool for executing ad-hoc scripts. 132 | 133 | Applies to all hosts by default, unless other nodes are specified using --extra-vars "nodes=..." 134 | 135 | ### `deep-clean.yaml` 136 | 137 | Performs deep cleaning of the host machines. 138 | Runs clean.yaml and, in addition, deletes the cloned repository with the lotus code and binaries. 139 | 140 | Applies to all hosts by default, unless other nodes are specified using --extra-vars "nodes=..." 141 | 142 | ### `deploy-current.yaml` 143 | 144 | Deploys the bootstrap and the validators using existing binaries. 145 | This playbook still cleans the lotus daemon state, but neither updates nor recompiles the code. 146 | Performs the state cleanup by running clean.yaml, 147 | potentially producing and ignoring some errors, if nothing is running on the hosts - this is normal. 148 | 149 | The nodes variable must not be set, as this playbook must distinguish between different kinds of nodes 150 | (such as bootstrap and validators). 151 | 152 | ### `deploy-new.yaml` 153 | 154 | Deploys the whole system from scratch. 155 | Performs a deep clean by running deep-clean.yaml 156 | (potentially producing and ignoring some errors, if nothing is running on the hosts - this is normal) 157 | and sets up a new Spacenet deployment. 158 | 159 | The nodes variable must not be set, as this playbook must distinguish between different kinds of nodes 160 | (such as bootstrap and validators). 161 | 162 | ### `fetch-logs.yaml` 163 | 164 | Fetches logs from all hosts and stores them in the `fetched-logs` directory (one sub-directory per host). 165 | 166 | Applies to all hosts by default, unless other nodes are specified using --extra-vars "nodes=..." 167 | 168 | ### `kill.yaml` 169 | 170 | Kills running Lotus daemon and Mir validator. 171 | Does not touch their persisted state or the code and binaries. 172 | Reports but ignores errors, so it can be used even if the processes to be killed are not running. 173 | 174 | Applies to all hosts by default, unless other nodes are specified using --extra-vars "nodes=..." 175 | 176 | ### `restart-validators.yaml` 177 | 178 | Restarts a given set of validators. 179 | For safety, does NOT default to restarting all validators 180 | and the set of hosts to restart must be explicitly given using --extra-vars "nodes=..." 181 | 182 | Note that this playbook always affects all hosts, regardless of the value of the nodes variable. 183 | This is due to the necessity of reconnecting all daemons to the restarted one. 184 | 185 | ### `setup.yaml` 186 | 187 | Sets up the environment for running the Lotus daemon and validator. 188 | This includes installing the necessary packages, fetching the Lotus code, and compiling it. 189 | It does not start any nodes. See start-* and deploy-*.yaml for starting the nodes. 190 | 191 | Applies to all hosts by default, unless other nodes are specified using --extra-vars "nodes=..." 192 | 193 | ### `start-bootstrap.yaml` 194 | 195 | Starts the bootstrap node and downloads its identity to localhost (using setup.yaml). 196 | 197 | Applies to the bootstrap host by default, unless other nodes are specified using --extra-vars "nodes=..." 198 | 199 | ### `start-faucet.yaml` 200 | 201 | Starts the faucet service that can be used to distribute coins. 202 | Assumes that the bootstrap node is up and running (see start-bootstrap.yaml). 203 | 204 | Applies to the first bootstrap host by default, unless other nodes are specified using --extra-vars "nodes=..." 205 | 206 | ### `start-daemons.yaml` 207 | 208 | Starts the Lotus daemons and creates connections among them and to the bootstrap node. 209 | Assumes that the bootstrap node is up and running (see start-bootstrap.yaml). 210 | 211 | Applies to the validator host by default, unless other nodes are specified using --extra-vars "nodes=..." 212 | 213 | ### `start-monitoring.yaml` 214 | 215 | Starts the health monitoring service that can be used to check the status of the system. 216 | Assumes that all the nodes (bootstraps, daemons, and validators) are up and running. 217 | 218 | Applies to the validator host by default, unless other nodes are specified using --extra-vars "nodes=..." 219 | 220 | ### `start-validators.yaml` 221 | 222 | Starts the Mir validators. 223 | Assumes that the Lotus daemons are up and running (see start-daemons.yaml). 224 | 225 | Applies to the validator host by default, unless other nodes are specified using --extra-vars "nodes=..." 226 | 227 | ### `stop-monitoring.yaml` 228 | 229 | Stops the health monitoring services. 230 | Assumes that all the health services are running on the target nodes (bootstraps, daemons, and validators). 231 | 232 | ### `update-faucet.yaml` 233 | 234 | Updates the faucet services by pulling the actual code from the repository. 235 | It doesn't recompile or start services. 236 | 237 | ### `update-nodes.yaml` 238 | 239 | Updates a given set of validators by fetching the configured code, recompiling it, and restarting the validators. 240 | After the update, waits until the nodes sync with the state of a bootstrap node and only then returns. 241 | 242 | For safety, does NOT default to restarting all validators 243 | and the set of hosts to restart must be explicitly given using --extra-vars "nodes=..." 244 | 245 | Note that this playbook always affects all hosts, regardless of the value of the nodes variable. 246 | This is due to the necessity of reconnecting all daemons to the restarted one. 247 | 248 | ### `status.yaml` 249 | 250 | Gets the status of the Eudico daemons and the chain head. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------