OSDN Git Service

add access token function (#111)
authoryahtoo <yahtoo.ma@gmail.com>
Fri, 17 Nov 2017 07:30:37 +0000 (15:30 +0800)
committerGuanghua Guo <1536310027@qq.com>
Fri, 17 Nov 2017 07:30:37 +0000 (15:30 +0800)
* add access token function

* add access token function

* add access token function

blockchain/accesstoken/accesstoken.go
blockchain/accesstoken/accesstoken_test.go [new file with mode: 0644]
blockchain/accesstokens.go
blockchain/reactor.go
cmd/bytomcli/main.go
node/node.go

index 63c1814..2d448bf 100644 (file)
@@ -2,23 +2,22 @@
 // credentials.
 package accesstoken
 
-// TODO(tessr): merge this package into chain/net/http/authn
-
 import (
        "context"
        "crypto/rand"
+       "encoding/json"
        "fmt"
        "regexp"
+       "strings"
        "time"
 
+       dbm "github.com/tendermint/tmlibs/db"
+
        "github.com/bytom/crypto/sha3pool"
        "github.com/bytom/errors"
 )
 
-const (
-       tokenSize    = 32
-       defaultLimit = 100
-)
+const tokenSize = 32
 
 var (
        // ErrBadID is returned when Create is called on an invalid id string.
@@ -27,147 +26,132 @@ var (
        ErrDuplicateID = errors.New("duplicate access token ID")
        // ErrBadType is returned when Create is called with a bad type.
        ErrBadType = errors.New("type must be client or network")
+       // ErrNoMatchID is returned when Delete is called on nonexisting ID.
+       ErrNoMatchID = errors.New("nonexisting access token ID")
 
        // validIDRegexp checks that all characters are alphumeric, _ or -.
        // It also must have a length of at least 1.
        validIDRegexp = regexp.MustCompile(`^[\w-]+$`)
 )
 
+// Token describe the access token.
 type Token struct {
        ID      string    `json:"id"`
        Token   string    `json:"token,omitempty"`
-       Type    string    `json:"type,omitempty"` // deprecated in 1.2
+       Type    string    `json:"type,omitempty"`
        Created time.Time `json:"created_at"`
-       sortID  string
 }
 
+// CredentialStore store user access credential.
 type CredentialStore struct {
-       //      DB pg.DB
+       DB dbm.DB
+}
+
+// NewStore creates and returns a new Store object.
+func NewStore(db dbm.DB) *CredentialStore {
+       return &CredentialStore{
+               DB: db,
+       }
 }
 
 // Create generates a new access token with the given ID.
-func (cs *Token) Create(ctx context.Context, id, typ string) (*Token, error) {
+func (cs *CredentialStore) Create(ctx context.Context, id, typ string) (*string, error) {
        if !validIDRegexp.MatchString(id) {
                return nil, errors.WithDetailf(ErrBadID, "invalid id %q", id)
        }
-
+       k, err := json.Marshal(id)
+       if v := cs.DB.Get(k); v != nil {
+               return nil, errors.WithDetailf(ErrDuplicateID, "id %q already in use", id)
+       }
        var secret [tokenSize]byte
-       _, err := rand.Read(secret[:])
-       if err != nil {
+       v, err := rand.Read(secret[:])
+       if err != nil || v != tokenSize {
                return nil, err
        }
-       var hashedSecret [32]byte
+
+       var hashedSecret [tokenSize]byte
        sha3pool.Sum256(hashedSecret[:], secret[:])
+       created := time.Now()
 
-       /*      const q = `
-                       INSERT INTO access_tokens (id, type, hashed_secret)
-                       VALUES($1, $2, $3)
-                       RETURNING created, sort_id
-               `
-       */var (
-               created time.Time
-               sortID  string
-       //      maybeType = sql.NullString{String: typ, Valid: typ != ""}
-       )
-       /*      err = cs.DB.QueryRowContext(ctx, q, id, maybeType, hashedSecret[:]).Scan(&created, &sortID)
-               if pg.IsUniqueViolation(err) {
-                       return nil, errors.WithDetailf(ErrDuplicateID, "id %q already in use", id)
-               }
-               if err != nil {
-                       return nil, errors.Wrap(err)
-               }
-       */
-       return &Token{
+       token := &Token{
                ID:      id,
-               Token:   fmt.Sprintf("%s:%x", id, secret),
+               Token:   fmt.Sprintf("%s:%x", id, hashedSecret),
                Type:    typ,
                Created: created,
-               sortID:  sortID,
-       }, nil
+       }
+
+       key, err := json.Marshal(id)
+       if err != nil {
+               return nil, err
+       }
+       value, err := json.Marshal(token)
+       if err != nil {
+               return nil, err
+       }
+       cs.DB.Set(key, value)
+       hexsec := fmt.Sprintf("%s:%x", id, secret)
+       return &hexsec, nil
 }
 
 // Check returns whether or not an id-secret pair is a valid access token.
-func (cs *Token) Check(ctx context.Context, id string, secret []byte) (bool, error) {
-       var (
-               toHash [tokenSize]byte
-               hashed [32]byte
-       )
+func (cs *CredentialStore) Check(ctx context.Context, id string, secret []byte) (bool, error) {
+       if !validIDRegexp.MatchString(id) {
+               return false, errors.WithDetailf(ErrBadID, "invalid id %q", id)
+       }
+
+       var toHash [tokenSize]byte
+       var hashed [tokenSize]byte
        copy(toHash[:], secret)
        sha3pool.Sum256(hashed[:], toHash[:])
+       inToken := fmt.Sprintf("%s:%x", id, hashed[:])
 
-       /*const q = `SELECT EXISTS(SELECT 1 FROM access_tokens WHERE id=$1 AND hashed_secret=$2)`
-       var valid bool
-       err := cs.DB.QueryRowContext(ctx, q, id, hashed[:]).Scan(&valid)
+       var value []byte
+       token := &Token{}
+       k, err := json.Marshal(id)
        if err != nil {
                return false, err
        }
-       */
-       //      return valid, nil
-       return true, nil
-}
 
-// Exists returns whether an id is part of a valid access token. It does not validate a secret.
-func (cs *Token) Exists(ctx context.Context, id string) bool {
-       /*      const q = `SELECT EXISTS(SELECT 1 FROM access_tokens WHERE id=$1)`
-               var valid bool
-               err := cs.DB.QueryRowContext(ctx, q, id).Scan(&valid)
-               if err != nil {
-                       return false
-               }
-               return valid
-       */
-       return true
+       if value = cs.DB.Get(k); value == nil {
+               return false, errors.WithDetailf(ErrNoMatchID, "check id %q nonexisting", id)
+       }
+       if err := json.Unmarshal(value, token); err != nil {
+               return false, err
+       }
+
+       if strings.Compare(token.Token, inToken) == 0 {
+               return true, nil
+       }
+
+       return false, nil
 }
 
 // List lists all access tokens.
-func (cs *Token) List(ctx context.Context, typ, after string, limit int) ([]*Token, string, error) {
-       if limit == 0 {
-               limit = defaultLimit
-       }
-       /*      const q = `
-                               SELECT id, type, sort_id, created FROM access_tokens
-                               WHERE ($1='' OR type=$1::access_token_type) AND ($2='' OR sort_id<$2)
-                               ORDER BY sort_id DESC
-                               LIMIT $3
-                       `
-               var tokens []*Token
-               //      err := pg.ForQueryRows(ctx, cs.DB, q, typ, after, limit, func(id string, maybeType sql.NullString, sortID string, created time.Time) {
-               t := Token{
-                       ID:      id,
-                       Created: created,
-                       Type:    maybeType.String,
-                       sortID:  sortID,
-               }
-               tokens = append(tokens, &t)
-                       })
-               if err != nil {
-                       return nil, "", errors.Wrap(err)
+func (cs *CredentialStore) List(ctx context.Context) ([]*Token, error) {
+       tokens := make([]*Token, 0)
+       iter := cs.DB.Iterator()
+       defer iter.Release()
+
+       for iter.Next() {
+               token := &Token{}
+               if err := json.Unmarshal(iter.Value(), token); err != nil {
+                       return nil, err
                }
-       */
-       var tokens []*Token
-       var next string
-       if len(tokens) > 0 {
-               next = tokens[len(tokens)-1].sortID
+               tokens = append(tokens, token)
        }
-
-       return tokens, next, nil
+       return tokens, nil
 }
 
 // Delete deletes an access token by id.
-func (cs *Token) Delete(ctx context.Context, id string) error {
-       /*      const q = `DELETE FROM access_tokens WHERE id=$1`
-               res, err := cs.DB.ExecContext(ctx, q, id)
-               if err != nil {
-                       return errors.Wrap(err)
-               }
-       */
-       /*      deleted, err := res.RowsAffected()
-               if err != nil {
-                       return errors.Wrap(err)
-               }
+func (cs *CredentialStore) Delete(ctx context.Context, id string) error {
+       if !validIDRegexp.MatchString(id) {
+               return errors.WithDetailf(ErrBadID, "invalid id %q", id)
+       }
+       k, err := json.Marshal(id)
+       if err != nil {
+               return err
+       }
+       cs.DB.Delete(k)
 
-               if deleted == 0 {
-                       return errors.WithDetailf(pg.ErrUserInputNotFound, "acccess token id %s", id)
-               }
-       */return nil
+       return nil
 }
diff --git a/blockchain/accesstoken/accesstoken_test.go b/blockchain/accesstoken/accesstoken_test.go
new file mode 100644 (file)
index 0000000..c81d97c
--- /dev/null
@@ -0,0 +1,116 @@
+package accesstoken
+
+import (
+       "context"
+       "encoding/hex"
+       "strings"
+       "testing"
+
+       dbm "github.com/tendermint/tmlibs/db"
+
+       "github.com/bytom/errors"
+)
+
+func TestCreate(t *testing.T) {
+       testDB := dbm.NewDB("testdb1", "leveldb", ".data")
+       cs := NewStore(testDB)
+       ctx := context.Background()
+
+       cases := []struct {
+               id, typ string
+               want    error
+       }{
+               {"a", "client", nil},
+               {"b", "network", nil},
+               {"", "client", ErrBadID},
+               {"bad:id", "client", ErrBadID},
+               {"a", "network", ErrDuplicateID}, // this aborts the transaction, so no tests can follow
+       }
+
+       for _, c := range cases {
+               _, err := cs.Create(ctx, c.id, c.typ)
+               if errors.Root(err) != c.want {
+                       t.Errorf("Create(%s, %s) error = %s want %s", c.id, c.typ, err, c.want)
+               }
+       }
+}
+
+func TestList(t *testing.T) {
+       ctx := context.Background()
+       testDB := dbm.NewDB("testdb2", "leveldb", ".data")
+       cs := NewStore(testDB)
+
+       mustCreateToken(ctx, t, cs, "ab", "test")
+       mustCreateToken(ctx, t, cs, "bc", "test")
+       mustCreateToken(ctx, t, cs, "cd", "test")
+
+       cases := struct {
+               want []string
+       }{
+               want: []string{"ab", "bc", "cd"},
+       }
+
+       got, err := cs.List(ctx)
+       if err != nil {
+               t.Errorf("List errored: get list error")
+       }
+       for i, v := range got {
+               if m := strings.Compare(v.ID, cases.want[i]); m != 0 {
+                       t.Errorf("List errored: %s %s", v.ID, cases.want[i])
+               }
+               continue
+       }
+}
+
+func TestCheck(t *testing.T) {
+       ctx := context.Background()
+       testDB := dbm.NewDB("testdb3", "leveldb", ".data")
+       cs := NewStore(testDB)
+
+       token := mustCreateToken(ctx, t, cs, "x", "client")
+       tokenParts := strings.Split(*token, ":")
+       tokenID := tokenParts[0]
+       tokenSecret, err := hex.DecodeString(tokenParts[1])
+       if err != nil {
+               t.Fatal("bad token secret")
+       }
+
+       valid, err := cs.Check(ctx, tokenID, tokenSecret)
+       if err != nil {
+               t.Fatal(err)
+       }
+       if !valid {
+               t.Fatal("expected token and secret to be valid")
+       }
+
+       valid, err = cs.Check(ctx, "x", []byte("badsecret"))
+       if err != nil {
+               t.Fatal(err)
+       }
+       if valid {
+               t.Fatal("expected bad secret to not be valid")
+       }
+}
+
+func TestDelete(t *testing.T) {
+       ctx := context.Background()
+       testDB := dbm.NewDB("testdb4", "leveldb", ".data")
+       cs := NewStore(testDB)
+
+       token := mustCreateToken(ctx, t, cs, "Y", "client")
+       tokenParts := strings.Split(*token, ":")
+       tokenID := tokenParts[0]
+
+       err := cs.Delete(ctx, tokenID)
+       if err != nil {
+               t.Fatal(err)
+       }
+}
+
+func mustCreateToken(ctx context.Context, t *testing.T, cs *CredentialStore, id, typ string) *string {
+       token, err := cs.Create(ctx, id, typ)
+       if err != nil {
+               t.Fatal(err)
+       }
+       return token
+}
index e2014e9..ff985d0 100644 (file)
@@ -2,111 +2,46 @@ package blockchain
 
 import (
        "context"
-       "encoding/json"
+       "encoding/hex"
 
-       "github.com/bytom/blockchain/accesstoken"
        "github.com/bytom/errors"
-       //      "github.com/bytom/net/http/authz"
-       "github.com/bytom/net/http/httpjson"
 )
 
-/*
-const (
-       defGenericPageSize = 100
-)
-*/
 var errCurrentToken = errors.New("token cannot delete itself")
 
-func (bcr *BlockchainReactor) createAccessToken(ctx context.Context, x struct{ ID, Type string }) (*accesstoken.Token, error) {
-       token, err := bcr.accesstoken.Create(ctx, x.ID, x.Type)
+func (a *BlockchainReactor) createAccessToken(ctx context.Context, x struct{ ID, Type string }) interface{} {
+       token, err := a.accessTokens.Create(ctx, x.ID, x.Type)
        if err != nil {
-               return nil, errors.Wrap(err)
+               return err.Error()
        }
 
+       return token
+}
 
-       if x.Type == "" {
-               return token, nil
-       }
-
-       data := map[string]interface{}{
-               "id": token.ID,
-       }
-       _, err = json.Marshal(data)
-       //      guardData, err := json.Marshal(data)
+func (a *BlockchainReactor) listAccessTokens(ctx context.Context) interface{} {
+       tokens, err := a.accessTokens.List(ctx)
        if err != nil {
-               return nil, errors.Wrap(err)
+               return err.Error()
        }
-       /*
-               var grant *authz.Grant
-
-               // Type is deprecated; however, for backward compatibility, using the
-               // Type field will create a grant associated with this new token.
-               switch x.Type {
-               case "client":
-                       grant = &authz.Grant{
-                               GuardType: "access_token",
-                               GuardData: guardData,
-                               Policy:    "client-readwrite",
-                       }
-               case "network":
-                       grant = &authz.Grant{
-                               GuardType: "access_token",
-                               GuardData: guardData,
-                               Policy:    "crosscore",
-                       }
-               default:
-                       // We've already returned if x.Type wasn't specified, so this must be a bad type.
-                       return nil, accesstoken.ErrBadType
-               }
-               err = a.sdb.Exec(ctx, a.grants.Save(ctx, grant))
-               if err != nil {
-                       return nil, errors.Wrap(err)
-               }
-       */
-       token.Type = x.Type // deprecated
-
-       return token, nil
+       return tokens
 }
 
-func (bcr *BlockchainReactor) listAccessTokens(ctx context.Context, x requestQuery) (*page, error) {
-       limit := x.PageSize
-       if limit == 0 {
-               limit = 100
-       }
-
-       tokens, next, err := bcr.accesstoken.List(ctx, x.Type, x.After, limit)
-       if err != nil {
-               return nil, err
+func (a *BlockchainReactor) deleteAccessToken(ctx context.Context, x struct{ ID, Token string }) interface{} {
+       //TODO Add delete permission verify.
+       if err := a.accessTokens.Delete(ctx, x.ID); err != nil {
+               return err.Error()
        }
-
-
-       outQuery := x
-       outQuery.After = next
-
-       return &page{
-               Items:    httpjson.Array(tokens),
-               LastPage: len(tokens) < limit,
-               Next:     outQuery,
-       }, nil
+       return "SUCCESS!"
 }
 
-func (bcr *BlockchainReactor) deleteAccessToken(ctx context.Context, x struct{ ID string }) error {
-       currentID, _, _ := httpjson.Request(ctx).BasicAuth()
-       if currentID == x.ID {
-               return errCurrentToken
+func (a *BlockchainReactor) checkAccessToken(ctx context.Context, x struct{ ID, Secret string }) interface{} {
+       secret, err := hex.DecodeString(x.Secret)
+       if err != nil {
+               return err.Error()
        }
-       err := bcr.accesstoken.Delete(ctx, x.ID)
+       result, err := a.accessTokens.Check(ctx, x.ID, secret)
        if err != nil {
-               return err
+               return err.Error()
        }
-
-
-       /*      err = a.sdb.Exec(ctx, a.deleteGrantsByAccessToken(x.ID))
-               if err != nil {
-                       // well, technically we did delete the access token, so don't return the error
-                       // TODO(tessr): make this whole operation atomic, such that we either delete
-                       // both the access token and its grants, or we return a failure.
-                       log.Printkv(ctx, log.KeyError, err, "at", "revoking grants for access token", "token", x.ID)
-               }
-       */return nil
+       return result
 }
index d5e4866..ccd0ae2 100755 (executable)
@@ -49,7 +49,7 @@ type BlockchainReactor struct {
        wallet        *account.Wallet
        accounts      *account.Manager
        assets        *asset.Registry
-       accesstoken   *accesstoken.Token
+       accessTokens  *accesstoken.CredentialStore
        txFeedTracker *txfeed.Tracker
        blockKeeper   *blockKeeper
        txPool        *protocol.TxPool
@@ -153,8 +153,10 @@ func (bcr *BlockchainReactor) BuildHander() {
        m.Handle("/info", jsonHandler(bcr.info))
        m.Handle("/submit-transaction", jsonHandler(bcr.submit))
        m.Handle("/create-access-token", jsonHandler(bcr.createAccessToken))
-       m.Handle("/list-access-tokens", jsonHandler(bcr.listAccessTokens))
+       m.Handle("/list-access-token", jsonHandler(bcr.listAccessTokens))
        m.Handle("/delete-access-token", jsonHandler(bcr.deleteAccessToken))
+       m.Handle("/check-access-token", jsonHandler(bcr.checkAccessToken))
+
        //hsm api
        m.Handle("/create-key", jsonHandler(bcr.pseudohsmCreateKey))
        m.Handle("/list-keys", jsonHandler(bcr.pseudohsmListKeys))
@@ -217,7 +219,7 @@ type page struct {
        LastPage bool         `json:"last_page"`
 }
 
-func NewBlockchainReactor(chain *protocol.Chain, txPool *protocol.TxPool, accounts *account.Manager, assets *asset.Registry, sw *p2p.Switch, hsm *pseudohsm.HSM, wallet *account.Wallet, txfeeds *txfeed.Tracker) *BlockchainReactor {
+func NewBlockchainReactor(chain *protocol.Chain, txPool *protocol.TxPool, accounts *account.Manager, assets *asset.Registry, sw *p2p.Switch, hsm *pseudohsm.HSM, wallet *account.Wallet, txfeeds *txfeed.Tracker, accessTokens *accesstoken.CredentialStore) *BlockchainReactor {
        mining := cpuminer.NewCPUMiner(chain, accounts, txPool)
        bcR := &BlockchainReactor{
                chain:         chain,
@@ -231,6 +233,7 @@ func NewBlockchainReactor(chain *protocol.Chain, txPool *protocol.TxPool, accoun
                sw:            sw,
                hsm:           hsm,
                txFeedTracker: txfeeds,
+               accessTokens:  accessTokens,
        }
        bcR.BaseReactor = *p2p.NewBaseReactor("BlockchainReactor", bcR)
        return bcR
index 58f42d2..27e5842 100755 (executable)
@@ -81,6 +81,7 @@ var commands = map[string]*command{
        "create-access-token":      {createAccessToken},
        "list-access-token":        {listAccessTokens},
        "delete-access-token":      {deleteAccessToken},
+       "check-access-token":       {checkAccessToken},
        "create-key":               {createKey},
        "list-keys":                {listKeys},
        "delete-key":               {deleteKey},
@@ -915,56 +916,61 @@ func listUnspentOutputs(client *rpc.Client, args []string) {
 }
 
 func createAccessToken(client *rpc.Client, args []string) {
-       if len(args) != 0 {
-               fatalln("error:createAccessToken not use args")
+       if len(args) != 1 {
+               fatalln("error:createAccessToken use args id")
        }
        type Token struct {
-               ID      string    `json:"id"`
-               Token   string    `json:"token,omitempty"`
-               Type    string    `json:"type,omitempty"` // deprecated in 1.2
-               Created time.Time `json:"created_at"`
-               sortID  string
+               ID   string `json:"id"`
+               Type string `json:"type"`
        }
        var token Token
-       token.ID = "Alice"
-       token.Token = "token"
+       token.ID = args[0]
 
-       client.Call(context.Background(), "/create-access-token", &[]Token{token}, nil)
+       var response interface{}
+
+       client.Call(context.Background(), "/create-access-token", &token, &response)
+       fmt.Println(response)
 }
 
 func listAccessTokens(client *rpc.Client, args []string) {
        if len(args) != 0 {
                fatalln("error:listAccessTokens not use args")
        }
+       var response interface{}
+       client.Call(context.Background(), "/list-access-token", nil, &response)
+       fmt.Println(response)
+}
+
+func deleteAccessToken(client *rpc.Client, args []string) {
+       if len(args) != 1 {
+               fatalln("error:deleteAccessToken use args id")
+       }
        type Token struct {
-               ID      string    `json:"id"`
-               Token   string    `json:"token,omitempty"`
-               Type    string    `json:"type,omitempty"` // deprecated in 1.2
-               Created time.Time `json:"created_at"`
-               sortID  string
+               ID     string `json:"id"`
+               Secert string `json:"secert,omitempty"`
        }
        var token Token
-       token.ID = "Alice"
-       token.Token = "token"
-
-       client.Call(context.Background(), "/list-access-token", &[]Token{token}, nil)
+       token.ID = args[0]
+       var response interface{}
+       client.Call(context.Background(), "/delete-access-token", &token, &response)
+       fmt.Println(response)
 }
-func deleteAccessToken(client *rpc.Client, args []string) {
-       if len(args) != 0 {
-               fatalln("error:deleteAccessToken not use args")
+
+func checkAccessToken(client *rpc.Client, args []string) {
+       if len(args) != 1 {
+               fatalln("error:deleteAccessToken use args token")
        }
        type Token struct {
-               ID      string    `json:"id"`
-               Token   string    `json:"token,omitempty"`
-               Type    string    `json:"type,omitempty"` // deprecated in 1.2
-               Created time.Time `json:"created_at"`
-               sortID  string
+               ID     string `json:"id"`
+               Secret string `json:"secret,omitempty"`
        }
        var token Token
-       token.ID = "Alice"
-       token.Token = "token"
-
-       client.Call(context.Background(), "/delete-access-token", &[]Token{token}, nil)
+       inputs := strings.Split(args[0], ":")
+       token.ID = inputs[0]
+       token.Secret = inputs[1]
+       var response interface{}
+       client.Call(context.Background(), "/check-access-token", &token, &response)
+       fmt.Println(response)
 }
 
 func createKey(client *rpc.Client, args []string) {
index 5774041..3d06457 100755 (executable)
@@ -18,6 +18,7 @@ import (
        dbm "github.com/tendermint/tmlibs/db"
 
        bc "github.com/bytom/blockchain"
+       "github.com/bytom/blockchain/accesstoken"
        "github.com/bytom/blockchain/account"
        "github.com/bytom/blockchain/asset"
        "github.com/bytom/blockchain/pseudohsm"
@@ -136,6 +137,9 @@ func NewNode(config *cfg.Config) *Node {
        txDB := dbm.NewDB("txdb", config.DBBackend, config.DBDir())
        store := txdb.NewStore(txDB)
 
+       tokenDB := dbm.NewDB("accesstoken", config.DBBackend, config.DBDir())
+       accessTokens := accesstoken.NewStore(tokenDB)
+
        privKey := crypto.GenPrivKeyEd25519()
 
        // Make event switch
@@ -211,8 +215,7 @@ func NewNode(config *cfg.Config) *Node {
        if err != nil {
                cmn.Exit(cmn.Fmt("initialize HSM failed: %v", err))
        }
-
-       bcReactor := bc.NewBlockchainReactor(chain, txPool, accounts, assets, sw, hsm, wallet, txFeed)
+       bcReactor := bc.NewBlockchainReactor(chain, txPool, accounts, assets, sw, hsm, wallet, txFeed, accessTokens)
 
        sw.AddReactor("BLOCKCHAIN", bcReactor)
 
@@ -232,7 +235,7 @@ func NewNode(config *cfg.Config) *Node {
                // go tool pprof http://profileHose/debug/pprof/heap
                go func() {
                        http.ListenAndServe(profileHost, nil)
-               } ()
+               }()
        }
 
        node := &Node{