diff --git a/README.md b/README.md index 3ca6946..22a667b 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ This repository contains the base chain implementation that Colinear's compute m | :-------- | :-------- | | Leasing & settlement module | [x/colinearcore](./x/colinearcore/README.md) | | In-memory bid database | [x/colinearcore/memdb](./x/colinearcore/memdb/README.md) | +| Hardware provider auditing | [x/colinearcore/audit](./x/colinearcore/audit/README.md) | ## Validators diff --git a/x/colinearcore/audit/README.md b/x/colinearcore/audit/README.md new file mode 100644 index 0000000..b44ff68 --- /dev/null +++ b/x/colinearcore/audit/README.md @@ -0,0 +1,17 @@ +# Peer Auditing + +The auditing mechanism contained in this folder ensures that hardware providers are running the hardware they say they are. The basic requirements will be as follows: + +| Requirement | Punishment | +| :-- | :-- | +| Must stake a set amount of CLR. | N/A | +| Must complete a GPU puzzle for each declared GPU. | N/A | +| Must be online for validator pings | Staked funds slashed | + +## GPU Puzzle + +Work in progress. + +## Validator Pings + +Attempts to perform an SSH handshake using Go's builtin `crypto/ssh`. If the handshake times out, this means that practically speaking, the provider is offline. diff --git a/x/colinearcore/audit/sshconn.go b/x/colinearcore/audit/sshconn.go new file mode 100644 index 0000000..6475467 --- /dev/null +++ b/x/colinearcore/audit/sshconn.go @@ -0,0 +1,109 @@ +package audit + +import ( + "errors" + "fmt" + "log" + "strconv" + "time" + + "github.com/dgraph-io/badger/v2" + "golang.org/x/crypto/ssh" +) + +// Maps SSH ping result channel to address that is being pinged +var pingDB *badger.DB + +func MountPingDB() { + var err error + pingDB, err = badger.Open(badger.DefaultOptions("").WithInMemory(true)) + if err != nil { + // must force crash here, since ping db is absolutely required + log.Fatalf("Failed to mount in-memory db: %s", err) + } +} + +// Begin ping to check if SSH is running on an open port on a host. +// +// NOTE: host should include a port number. +func BeginSshPing(host string) error { + + k := []byte(host) + + err := pingDB.Update(func(txn *badger.Txn) error { + _, err := txn.Get(k) + if err != nil { + if !errors.Is(err, badger.ErrKeyNotFound) { + // key-not-found condition required to write + return err + } else { + goto writePing + } + } + return fmt.Errorf("already pinging host %s", host) + + writePing: + if err := txn.Set(k, []byte(strconv.FormatBool(false))); err != nil { + return err + } + return nil + }) + + if err != nil { + return err + } + + sshConfig := &ssh.ClientConfig{ + User: "user", + Auth: []ssh.AuthMethod{ssh.Password("pass")}, + HostKeyCallback: ssh.InsecureIgnoreHostKey(), + Timeout: 100 * time.Second, + } + + go dialSshAsync(host, sshConfig) + + return nil +} + +// Check on ongoing SSH ping +func CheckSshPing(host string) (bool, error) { + k := []byte(host) + + var pingStatus bool + + err := pingDB.View(func(txn *badger.Txn) error { + resVal, err := txn.Get(k) + if err != nil { + if !errors.Is(err, badger.ErrKeyNotFound) { + return err + } else { + return fmt.Errorf("not pinging host %s", host) + } + } + + err = resVal.Value(func(val []byte) error { + pingStatus, err = strconv.ParseBool(string(val)) + if err != nil { + return fmt.Errorf("failed to decode ping status for host %s: %s", host, string(val)) + } + return nil + }) + return err + + }) + + if err != nil { + return false, err + } + + return pingStatus, nil +} + +func dialSshAsync(host string, conf *ssh.ClientConfig) { + ssh.Dial("tcp", host, conf) + pingDB.Update(func(txn *badger.Txn) error { + k := []byte(host) + return txn.Set(k, []byte(strconv.FormatBool(true))) + }) + // any potential resulting error is swallowed since this is run asynchronously +} diff --git a/x/colinearcore/audit/sshconn_test.go b/x/colinearcore/audit/sshconn_test.go new file mode 100644 index 0000000..5c8495e --- /dev/null +++ b/x/colinearcore/audit/sshconn_test.go @@ -0,0 +1,40 @@ +package audit + +import ( + "testing" + "time" +) + +func TestCheckSSH(t *testing.T) { + + MountPingDB() + + host1 := "102.1.41.3:22" // should evaluate to false + host2 := "127.0.0.1:22" // should evaluate to true + + if err := BeginSshPing(host1); err != nil { + panic(err) + } + + if err := BeginSshPing(host2); err != nil { + panic(err) + } + + time.Sleep(5 * time.Second) + + res, err := CheckSshPing(host1) + if err != nil { + panic(err) + } + if res { + panic("check host 1 - res should be false") + } + + res2, err := CheckSshPing(host2) + if err != nil { + panic(err) + } + if !res2 { + panic("check host 2 - res should be true") + } +}