From 6afbda5302ce181434587a3feca02f411caefd26 Mon Sep 17 00:00:00 2001 From: Eduard S Date: Mon, 19 Oct 2020 13:53:39 +0200 Subject: [PATCH] Extend statedb and use prefixes, add debugapi In statedb: - Store all values using prefixes for keys to allow iteration - Add methods MTGetRoot, GetAccounts Implement debugapi, an http server with debugging endpoints: - debugAPI.GET("sdb/batchnum", a.handleCurrentBatch) - debugAPI.GET("sdb/mtroot", a.handleMTRoot) - debugAPI.GET("sdb/accounts", a.handleAccounts) - debugAPI.GET("sdb/accounts/:Idx", a.handleAccount) --- db/statedb/statedb.go | 75 ++++++++++++++++---- db/statedb/statedb_test.go | 54 +++++++++++---- db/statedb/txprocessors.go | 2 +- db/statedb/utils.go | 8 +-- go.mod | 4 +- go.sum | 11 +++ test/debugapi/debugapi.go | 121 +++++++++++++++++++++++++++++++++ test/debugapi/debugapi_test.go | 101 +++++++++++++++++++++++++++ 8 files changed, 344 insertions(+), 32 deletions(-) create mode 100644 test/debugapi/debugapi.go create mode 100644 test/debugapi/debugapi_test.go diff --git a/db/statedb/statedb.go b/db/statedb/statedb.go index 084ca43..bb7b279 100644 --- a/db/statedb/statedb.go +++ b/db/statedb/statedb.go @@ -3,6 +3,7 @@ package statedb import ( "errors" "fmt" + "math/big" "os" "os/exec" "strconv" @@ -29,7 +30,18 @@ var ( ErrToIdxNotFound = errors.New("ToIdx can not be found") // KeyCurrentBatch is used as key in the db to store the current BatchNum - KeyCurrentBatch = []byte("currentbatch") + KeyCurrentBatch = []byte("k:currentbatch") + + // PrefixKeyIdx is the key prefix for idx in the db + PrefixKeyIdx = []byte("i:") + // PrefixKeyAccHash is the key prefix for account hash in the db + PrefixKeyAccHash = []byte("h:") + // PrefixKeyMT is the key prefix for merkle tree in the db + PrefixKeyMT = []byte("m:") + // PrefixKeyAddr is the key prefix for address in the db + PrefixKeyAddr = []byte("a:") + // PrefixKeyAddrBJJ is the key prefix for address-babyjubjub in the db + PrefixKeyAddrBJJ = []byte("ab:") ) const ( @@ -80,7 +92,7 @@ func NewStateDB(path string, typ TypeStateDB, nLevels int) (*StateDB, error) { var mt *merkletree.MerkleTree = nil if typ == TypeSynchronizer || typ == TypeBatchBuilder { - mt, err = merkletree.NewMerkleTree(sto, nLevels) + mt, err = merkletree.NewMerkleTree(sto.WithPrefix(PrefixKeyMT), nLevels) if err != nil { return nil, err } @@ -236,7 +248,7 @@ func (s *StateDB) Reset(batchNum common.BatchNum) error { if s.mt != nil { // open the MT for the current s.db - mt, err := merkletree.NewMerkleTree(s.db, s.mt.MaxLevels()) + mt, err := merkletree.NewMerkleTree(s.db.WithPrefix(PrefixKeyMT), s.mt.MaxLevels()) if err != nil { return err } @@ -251,6 +263,35 @@ func (s *StateDB) GetAccount(idx common.Idx) (*common.Account, error) { return getAccountInTreeDB(s.db, idx) } +// GetAccounts returns all the accounts in the db. Use for debugging pruposes +// only. +func (s *StateDB) GetAccounts() ([]common.Account, error) { + idxDB := s.db.WithPrefix(PrefixKeyIdx) + idxs := []common.Idx{} + // NOTE: Current implementation of Iterate in the pebble interface is + // not efficient, as it iterates over all keys. Improve it following + // this example: https://github.com/cockroachdb/pebble/pull/923/files + if err := idxDB.Iterate(func(k []byte, v []byte) (bool, error) { + idx, err := common.IdxFromBytes(k) + if err != nil { + return false, err + } + idxs = append(idxs, idx) + return true, nil + }); err != nil { + return nil, err + } + accs := []common.Account{} + for i := range idxs { + acc, err := s.GetAccount(idxs[i]) + if err != nil { + return nil, err + } + accs = append(accs, *acc) + } + return accs, nil +} + // getAccountInTreeDB is abstracted from StateDB to be used from StateDB and // from ExitTree. GetAccount returns the account for the given Idx func getAccountInTreeDB(sto db.Storage, idx common.Idx) (*common.Account, error) { @@ -258,17 +299,22 @@ func getAccountInTreeDB(sto db.Storage, idx common.Idx) (*common.Account, error) if err != nil { return nil, err } - vBytes, err := sto.Get(idxBytes[:]) + vBytes, err := sto.Get(append(PrefixKeyIdx, idxBytes[:]...)) if err != nil { return nil, err } - accBytes, err := sto.Get(vBytes) + accBytes, err := sto.Get(append(PrefixKeyAccHash, vBytes...)) if err != nil { return nil, err } var b [32 * common.NLeafElems]byte copy(b[:], accBytes) - return common.AccountFromBytes(b) + account, err := common.AccountFromBytes(b) + if err != nil { + return nil, err + } + account.Idx = idx + return account, nil } // CreateAccount creates a new Account in the StateDB for the given Idx. If @@ -309,16 +355,16 @@ func createAccountInTreeDB(sto db.Storage, mt *merkletree.MerkleTree, idx common if err != nil { return nil, err } - _, err = tx.Get(idxBytes[:]) + _, err = tx.Get(append(PrefixKeyIdx, idxBytes[:]...)) if err != db.ErrNotFound { return nil, ErrAccountAlreadyExists } - err = tx.Put(v.Bytes(), accountBytes[:]) + err = tx.Put(append(PrefixKeyAccHash, v.Bytes()...), accountBytes[:]) if err != nil { return nil, err } - err = tx.Put(idxBytes[:], v.Bytes()) + err = tx.Put(append(PrefixKeyIdx, idxBytes[:]...), v.Bytes()) if err != nil { return nil, err } @@ -360,7 +406,7 @@ func updateAccountInTreeDB(sto db.Storage, mt *merkletree.MerkleTree, idx common if err != nil { return nil, err } - err = tx.Put(v.Bytes(), accountBytes[:]) + err = tx.Put(append(PrefixKeyAccHash, v.Bytes()...), accountBytes[:]) if err != nil { return nil, err } @@ -368,7 +414,7 @@ func updateAccountInTreeDB(sto db.Storage, mt *merkletree.MerkleTree, idx common if err != nil { return nil, err } - err = tx.Put(idxBytes[:], v.Bytes()) + err = tx.Put(append(PrefixKeyIdx, idxBytes[:]...), v.Bytes()) if err != nil { return nil, err } @@ -391,6 +437,11 @@ func (s *StateDB) MTGetProof(idx common.Idx) (*merkletree.CircomVerifierProof, e return s.mt.GenerateCircomVerifierProof(idx.BigInt(), s.mt.Root()) } +// MTGetRoot returns the current root of the underlying Merkle Tree +func (s *StateDB) MTGetRoot() *big.Int { + return s.mt.Root().BigInt() +} + // LocalStateDB represents the local StateDB which allows to make copies from // the synchronizer StateDB, and is used by the tx-selector and the // batch-builder. LocalStateDB is an in-memory storage. @@ -462,7 +513,7 @@ func (l *LocalStateDB) Reset(batchNum common.BatchNum, fromSynchronizer bool) er return err } // open the MT for the current s.db - mt, err := merkletree.NewMerkleTree(l.db, l.mt.MaxLevels()) + mt, err := merkletree.NewMerkleTree(l.db.WithPrefix(PrefixKeyMT), l.mt.MaxLevels()) if err != nil { return err } diff --git a/db/statedb/statedb_test.go b/db/statedb/statedb_test.go index 48a100d..0a92fff 100644 --- a/db/statedb/statedb_test.go +++ b/db/statedb/statedb_test.go @@ -26,6 +26,7 @@ func newAccount(t *testing.T, i int) *common.Account { address := ethCrypto.PubkeyToAddress(key.PublicKey) return &common.Account{ + Idx: common.Idx(256 + i), TokenID: common.TokenID(i), Nonce: common.Nonce(i), Balance: big.NewInt(1000), @@ -124,7 +125,7 @@ func TestStateDBWithoutMT(t *testing.T) { // create test accounts var accounts []*common.Account - for i := 0; i < 100; i++ { + for i := 0; i < 4; i++ { accounts = append(accounts, newAccount(t, i)) } @@ -136,22 +137,22 @@ func TestStateDBWithoutMT(t *testing.T) { // add test accounts for i := 0; i < len(accounts); i++ { - _, err = sdb.CreateAccount(common.Idx(i), accounts[i]) + _, err = sdb.CreateAccount(accounts[i].Idx, accounts[i]) assert.Nil(t, err) } for i := 0; i < len(accounts); i++ { - existingAccount := common.Idx(i) + existingAccount := accounts[i].Idx accGetted, err := sdb.GetAccount(existingAccount) assert.Nil(t, err) assert.Equal(t, accounts[i], accGetted) } // try already existing idx and get error - existingAccount := common.Idx(1) + existingAccount := common.Idx(256) _, err = sdb.GetAccount(existingAccount) // check that exist assert.Nil(t, err) - _, err = sdb.CreateAccount(common.Idx(1), accounts[1]) // check that can not be created twice + _, err = sdb.CreateAccount(common.Idx(256), accounts[1]) // check that can not be created twice assert.NotNil(t, err) assert.Equal(t, ErrAccountAlreadyExists, err) @@ -188,35 +189,35 @@ func TestStateDBWithMT(t *testing.T) { // add test accounts for i := 0; i < len(accounts); i++ { - _, err = sdb.CreateAccount(common.Idx(i), accounts[i]) + _, err = sdb.CreateAccount(accounts[i].Idx, accounts[i]) assert.Nil(t, err) } for i := 0; i < len(accounts); i++ { - accGetted, err := sdb.GetAccount(common.Idx(i)) + accGetted, err := sdb.GetAccount(accounts[i].Idx) assert.Nil(t, err) assert.Equal(t, accounts[i], accGetted) } // try already existing idx and get error - _, err = sdb.GetAccount(common.Idx(1)) // check that exist + _, err = sdb.GetAccount(common.Idx(256)) // check that exist assert.Nil(t, err) - _, err = sdb.CreateAccount(common.Idx(1), accounts[1]) // check that can not be created twice + _, err = sdb.CreateAccount(common.Idx(256), accounts[1]) // check that can not be created twice assert.NotNil(t, err) assert.Equal(t, ErrAccountAlreadyExists, err) - _, err = sdb.MTGetProof(common.Idx(1)) + _, err = sdb.MTGetProof(common.Idx(256)) assert.Nil(t, err) // update accounts for i := 0; i < len(accounts); i++ { accounts[i].Nonce = accounts[i].Nonce + 1 - _, err = sdb.UpdateAccount(common.Idx(i), accounts[i]) + _, err = sdb.UpdateAccount(accounts[i].Idx, accounts[i]) assert.Nil(t, err) } - a, err := sdb.GetAccount(common.Idx(1)) // check that account value has been updated + a, err := sdb.GetAccount(common.Idx(256)) // check that account value has been updated assert.Nil(t, err) - assert.Equal(t, accounts[1].Nonce, a.Nonce) + assert.Equal(t, accounts[0].Nonce, a.Nonce) } func TestCheckpoints(t *testing.T) { @@ -234,7 +235,7 @@ func TestCheckpoints(t *testing.T) { // add test accounts for i := 0; i < len(accounts); i++ { - _, err = sdb.CreateAccount(common.Idx(i), accounts[i]) + _, err = sdb.CreateAccount(accounts[i].Idx, accounts[i]) assert.Nil(t, err) } @@ -334,6 +335,31 @@ func TestCheckpoints(t *testing.T) { } } +func TestStateDBGetAccounts(t *testing.T) { + dir, err := ioutil.TempDir("", "tmpdb") + require.Nil(t, err) + + sdb, err := NewStateDB(dir, TypeTxSelector, 0) + assert.Nil(t, err) + + // create test accounts + var accounts []common.Account + for i := 0; i < 16; i++ { + account := newAccount(t, i) + accounts = append(accounts, *account) + } + + // add test accounts + for i := range accounts { + _, err = sdb.CreateAccount(accounts[i].Idx, &accounts[i]) + require.Nil(t, err) + } + + dbAccounts, err := sdb.GetAccounts() + require.Nil(t, err) + assert.Equal(t, accounts, dbAccounts) +} + func printCheckpoints(t *testing.T, path string) { files, err := ioutil.ReadDir(path) assert.Nil(t, err) diff --git a/db/statedb/txprocessors.go b/db/statedb/txprocessors.go index d747c7a..d63c51f 100644 --- a/db/statedb/txprocessors.go +++ b/db/statedb/txprocessors.go @@ -14,7 +14,7 @@ import ( var ( // keyidx is used as key in the db to store the current Idx - keyidx = []byte("idx") + keyidx = []byte("k:idx") ) func (s *StateDB) resetZKInputs() { diff --git a/db/statedb/utils.go b/db/statedb/utils.go index 0db064f..cce91eb 100644 --- a/db/statedb/utils.go +++ b/db/statedb/utils.go @@ -51,12 +51,12 @@ func (s *StateDB) setIdxByEthAddrBJJ(idx common.Idx, addr ethCommon.Address, pk if err != nil { return err } - err = tx.Put(k, idxBytes[:]) + err = tx.Put(append(PrefixKeyAddrBJJ, k...), idxBytes[:]) if err != nil { return err } // store Addr-idx - err = tx.Put(addr.Bytes(), idxBytes[:]) + err = tx.Put(append(PrefixKeyAddr, addr.Bytes()...), idxBytes[:]) if err != nil { return err } @@ -71,7 +71,7 @@ func (s *StateDB) setIdxByEthAddrBJJ(idx common.Idx, addr ethCommon.Address, pk // Ethereum Address. Will return common.Idx(0) and error in case that Idx is // not found in the StateDB. func (s *StateDB) GetIdxByEthAddr(addr ethCommon.Address) (common.Idx, error) { - b, err := s.db.Get(addr.Bytes()) + b, err := s.db.Get(append(PrefixKeyAddr, addr.Bytes()...)) if err != nil { return common.Idx(0), ErrToIdxNotFound } @@ -94,7 +94,7 @@ func (s *StateDB) GetIdxByEthAddrBJJ(addr ethCommon.Address, pk *babyjub.PublicK } else if !bytes.Equal(addr.Bytes(), common.EmptyAddr.Bytes()) && pk != nil { // case ToEthAddr!=0 && ToBJJ!=0 k := concatEthAddrBJJ(addr, pk) - b, err := s.db.Get(k) + b, err := s.db.Get(append(PrefixKeyAddrBJJ, k...)) if err != nil { return common.Idx(0), ErrToIdxNotFound } diff --git a/go.mod b/go.mod index c5582be..4ab5061 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,8 @@ require ( github.com/dghubble/sling v1.3.0 github.com/ethereum/go-ethereum v1.9.17 github.com/getkin/kin-openapi v0.22.0 - github.com/gin-gonic/gin v1.4.0 + github.com/gin-contrib/cors v1.3.1 + github.com/gin-gonic/gin v1.5.0 github.com/go-sql-driver/mysql v1.5.0 // indirect github.com/gobuffalo/packr/v2 v2.8.0 github.com/iden3/go-iden3-crypto v0.0.6-0.20200823174058-e04ca5764a15 @@ -24,6 +25,7 @@ require ( go.uber.org/multierr v1.6.0 // indirect go.uber.org/zap v1.16.0 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 + golang.org/x/net v0.0.0-20200822124328-c89045814202 golang.org/x/tools/gopls v0.5.0 // indirect gopkg.in/go-playground/validator.v9 v9.29.1 ) diff --git a/go.sum b/go.sum index 0e91769..02f34c9 100644 --- a/go.sum +++ b/go.sum @@ -165,10 +165,16 @@ github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49P github.com/ghemawat/stream v0.0.0-20171120220530-696b145b53b9/go.mod h1:106OIgooyS7OzLDOpUGgm9fA3bQENb/cFSyyBmMoJDs= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gin-contrib/cors v1.3.1 h1:doAsuITavI4IOcd0Y19U4B+O0dNWihRyX//nn4sEmgA= +github.com/gin-contrib/cors v1.3.1/go.mod h1:jjEJ4268OPZUcU7k9Pm653S7lXUGcqMADzFA61xsmDk= github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3 h1:t8FVkw33L+wilf2QiWkw0UV77qRpcH/JHPKGpKa2E8g= github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-gonic/gin v1.4.0 h1:3tMoCCfM7ppqsR0ptz/wi1impNpT7/9wQtMZ8lr1mCQ= github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM= +github.com/gin-gonic/gin v1.5.0 h1:fi+bqFAx/oLK54somfCtEZs9HeH1LHVoEPUgARpTqyc= +github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do= github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= @@ -378,6 +384,8 @@ github.com/mattn/go-isatty v0.0.5-0.20180830101745-3fb116b82035/go.mod h1:M+lRXT github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.9 h1:d5US/mDsogSGW37IV293h//ZFaeajb69h+EHFsv2xGg= +github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= github.com/mattn/go-oci8 v0.0.7/go.mod h1:wjDx6Xm9q7dFtHJvIlrI99JytznLw5wQ4R+9mNXJwGI= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= @@ -570,10 +578,12 @@ github.com/tyler-smith/go-bip39 v1.0.2 h1:+t3w+KwLXO6154GNJY+qUtIxLTmFjfUmpguQT1 github.com/tyler-smith/go-bip39 v1.0.2/go.mod h1:sJ5fKU0s6JVwZjjcUEX2zFOnvq0ASQ2K9Zr6cf67kNs= github.com/ugorji/go v1.1.4 h1:j4s+tAvLfL3bZyefP2SEWmhBzmuIlH/eqNuPdFPgngw= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= +github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go v1.1.8 h1:/D9x7IRpfMHDlizVOgxrag5Fh+/NY+LtI8bsr+AswRA= github.com/ugorji/go v1.1.8/go.mod h1:0lNM99SwWUIRhCXnigEMClngXBk/EmpTXa7mgiewYWA= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8 h1:3SVOIvH7Ae1KRYyQWRjXWJEA9sS/c/pjvH++55Gr648= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= github.com/ugorji/go/codec v1.1.8 h1:4dryPvxMP9OtkjIbuNeK2nb27M38XMHLGlfNSNph/5s= github.com/ugorji/go/codec v1.1.8/go.mod h1:X00B19HDtwvKbQY2DcYjvZxKQp8mzrJoQ6EgoIY/D2E= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= @@ -713,6 +723,7 @@ golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190515120540-06a5c4944438/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/test/debugapi/debugapi.go b/test/debugapi/debugapi.go new file mode 100644 index 0000000..4ebaba0 --- /dev/null +++ b/test/debugapi/debugapi.go @@ -0,0 +1,121 @@ +package debugapi + +import ( + "context" + "net/http" + "time" + + "github.com/gin-contrib/cors" + "github.com/gin-gonic/gin" + "github.com/hermeznetwork/hermez-node/common" + "github.com/hermeznetwork/hermez-node/db/statedb" + "github.com/hermeznetwork/hermez-node/log" +) + +func handleNoRoute(c *gin.Context) { + c.JSON(http.StatusNotFound, gin.H{ + "error": "404 page not found", + }) +} + +type errorMsg struct { + Message string +} + +func badReq(err error, c *gin.Context) { + log.Errorw("Bad request", "err", err) + c.JSON(http.StatusBadRequest, errorMsg{ + Message: err.Error(), + }) +} + +// DebugAPI is an http API with debugging endpoints +type DebugAPI struct { + addr string + stateDB *statedb.StateDB // synchronizer statedb +} + +// NewDebugAPI creates a new DebugAPI +func NewDebugAPI(addr string, stateDB *statedb.StateDB) *DebugAPI { + return &DebugAPI{ + stateDB: stateDB, + addr: addr, + } +} + +func (a *DebugAPI) handleAccount(c *gin.Context) { + uri := struct { + Idx uint32 + }{} + if err := c.ShouldBindUri(&uri); err != nil { + badReq(err, c) + return + } + account, err := a.stateDB.GetAccount(common.Idx(uri.Idx)) + if err != nil { + badReq(err, c) + return + } + c.JSON(http.StatusOK, account) +} + +func (a *DebugAPI) handleAccounts(c *gin.Context) { + accounts, err := a.stateDB.GetAccounts() + if err != nil { + badReq(err, c) + return + } + c.JSON(http.StatusOK, accounts) +} + +func (a *DebugAPI) handleCurrentBatch(c *gin.Context) { + batchNum, err := a.stateDB.GetCurrentBatch() + if err != nil { + badReq(err, c) + return + } + c.JSON(http.StatusOK, batchNum) +} + +func (a *DebugAPI) handleMTRoot(c *gin.Context) { + root := a.stateDB.MTGetRoot() + c.JSON(http.StatusOK, root) +} + +// Run starts the http server of the DebugAPI. To stop it, pass a context with +// cancelation (see `debugapi_test.go` for an example). +func (a *DebugAPI) Run(ctx context.Context) error { + api := gin.Default() + api.NoRoute(handleNoRoute) + api.Use(cors.Default()) + debugAPI := api.Group("/debug") + + debugAPI.GET("sdb/batchnum", a.handleCurrentBatch) + debugAPI.GET("sdb/mtroot", a.handleMTRoot) + debugAPI.GET("sdb/accounts", a.handleAccounts) + debugAPI.GET("sdb/accounts/:Idx", a.handleAccount) + + debugAPIServer := &http.Server{ + Addr: a.addr, + Handler: api, + // Use some hardcoded numberes that are suitable for testing + ReadTimeout: 30 * time.Second, //nolint:gomnd + WriteTimeout: 30 * time.Second, //nolint:gomnd + MaxHeaderBytes: 1 << 20, //nolint:gomnd + } + go func() { + log.Infof("Debug API is ready at %v", a.addr) + if err := debugAPIServer.ListenAndServe(); err != nil && + err != http.ErrServerClosed { + log.Fatalf("Listen: %s\n", err) + } + }() + + <-ctx.Done() + log.Info("Stopping Debug API...") + if err := debugAPIServer.Shutdown(context.Background()); err != nil { + return err + } + log.Info("Debug API stopped") + return nil +} diff --git a/test/debugapi/debugapi_test.go b/test/debugapi/debugapi_test.go new file mode 100644 index 0000000..183acc1 --- /dev/null +++ b/test/debugapi/debugapi_test.go @@ -0,0 +1,101 @@ +package debugapi + +import ( + "crypto/ecdsa" + "fmt" + "io/ioutil" + "math/big" + "net/http" + "strconv" + "testing" + + "github.com/dghubble/sling" + ethCrypto "github.com/ethereum/go-ethereum/crypto" + "github.com/hermeznetwork/hermez-node/common" + "github.com/hermeznetwork/hermez-node/db/statedb" + "github.com/iden3/go-iden3-crypto/babyjub" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/net/context" +) + +func newAccount(t *testing.T, i int) *common.Account { + var sk babyjub.PrivateKey + copy(sk[:], []byte(strconv.Itoa(i))) // only for testing + pk := sk.Public() + + var key ecdsa.PrivateKey + key.D = big.NewInt(int64(i + 1)) // only for testing + key.PublicKey.X, key.PublicKey.Y = ethCrypto.S256().ScalarBaseMult(key.D.Bytes()) + key.Curve = ethCrypto.S256() + address := ethCrypto.PubkeyToAddress(key.PublicKey) + + return &common.Account{ + Idx: common.Idx(256 + i), + TokenID: common.TokenID(i), + Nonce: common.Nonce(i), + Balance: big.NewInt(1000), + PublicKey: pk, + EthAddr: address, + } +} + +func TestDebugAPI(t *testing.T) { + dir, err := ioutil.TempDir("", "tmpdb") + require.Nil(t, err) + + sdb, err := statedb.NewStateDB(dir, statedb.TypeSynchronizer, 32) + require.Nil(t, err) + err = sdb.MakeCheckpoint() // Make a checkpoint to increment the batchNum + require.Nil(t, err) + + addr := "localhost:12345" + debugAPI := NewDebugAPI(addr, sdb) + + ctx, cancel := context.WithCancel(context.Background()) + go func() { + err := debugAPI.Run(ctx) + require.Nil(t, err) + }() + + var accounts []common.Account + for i := 0; i < 16; i++ { + account := newAccount(t, i) + accounts = append(accounts, *account) + _, err = sdb.CreateAccount(account.Idx, account) + require.Nil(t, err) + } + + url := fmt.Sprintf("http://%v/debug/", addr) + + var batchNum common.BatchNum + req, err := sling.New().Get(url).Path("sdb/batchnum").ReceiveSuccess(&batchNum) + require.Equal(t, http.StatusOK, req.StatusCode) + require.Nil(t, err) + assert.Equal(t, common.BatchNum(1), batchNum) + + var mtroot *big.Int + req, err = sling.New().Get(url).Path("sdb/mtroot").ReceiveSuccess(&mtroot) + require.Equal(t, http.StatusOK, req.StatusCode) + require.Nil(t, err) + // Testing against a hardcoded value obtained by running the test and + // printing the value previously. + assert.Equal(t, "8902613552504893273500019895709436294962812188236308621387152512232191202510", + mtroot.String()) + + var accountAPI common.Account + req, err = sling.New().Get(url). + Path(fmt.Sprintf("sdb/accounts/%v", accounts[0].Idx)). + ReceiveSuccess(&accountAPI) + require.Equal(t, http.StatusOK, req.StatusCode) + require.Nil(t, err) + assert.Equal(t, accounts[0], accountAPI) + + var accountsAPI []common.Account + req, err = sling.New().Get(url).Path("sdb/accounts").ReceiveSuccess(&accountsAPI) + require.Equal(t, http.StatusOK, req.StatusCode) + require.Nil(t, err) + assert.Equal(t, accounts, accountsAPI) + + cancel() +}