`);
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 |
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 | 
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 | [](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 | 
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 |
--------------------------------------------------------------------------------