diff --git a/api/accountcreationauths.go b/api/accountcreationauths.go new file mode 100644 index 0000000..90e36ce --- /dev/null +++ b/api/accountcreationauths.go @@ -0,0 +1,68 @@ +package api + +import ( + "errors" + "net/http" + "time" + + ethCommon "github.com/ethereum/go-ethereum/common" + "github.com/gin-gonic/gin" + "github.com/hermeznetwork/hermez-node/apitypes" + "github.com/hermeznetwork/hermez-node/common" + "github.com/iden3/go-iden3-crypto/babyjub" +) + +func postAccountCreationAuth(c *gin.Context) { + // Parse body + var apiAuth receivedAuth + if err := c.ShouldBindJSON(&apiAuth); err != nil { + retBadReq(err, c) + return + } + // API to common + verify signature + commonAuth := accountCreationAuthAPIToCommon(&apiAuth) + if !commonAuth.VerifySignature() { + retBadReq(errors.New("invalid signature"), c) + return + } + // Insert to DB + if err := l2.AddAccountCreationAuth(commonAuth); err != nil { + retSQLErr(err, c) + return + } + // Return OK + c.Status(http.StatusOK) +} + +func getAccountCreationAuth(c *gin.Context) { + // Get hezEthereumAddress + addr, err := parseParamHezEthAddr(c) + if err != nil { + retBadReq(err, c) + return + } + // Fetch auth from l2DB + auth, err := l2.GetAccountCreationAuthAPI(*addr) + if err != nil { + retSQLErr(err, c) + return + } + // Build succesfull response + c.JSON(http.StatusOK, auth) +} + +type receivedAuth struct { + EthAddr apitypes.StrHezEthAddr `json:"hezEthereumAddress" binding:"required"` + BJJ apitypes.StrHezBJJ `json:"bjj" binding:"required"` + Signature apitypes.StrEthSignature `json:"signature" binding:"required"` + Timestamp time.Time `json:"timestamp"` +} + +func accountCreationAuthAPIToCommon(apiAuth *receivedAuth) *common.AccountCreationAuth { + return &common.AccountCreationAuth{ + EthAddr: ethCommon.Address(apiAuth.EthAddr), + BJJ: (*babyjub.PublicKey)(&apiAuth.BJJ), + Signature: []byte(apiAuth.Signature), + Timestamp: apiAuth.Timestamp, + } +} diff --git a/api/accountcreationauths_test.go b/api/accountcreationauths_test.go new file mode 100644 index 0000000..145ee44 --- /dev/null +++ b/api/accountcreationauths_test.go @@ -0,0 +1,99 @@ +package api + +import ( + "bytes" + "encoding/hex" + "encoding/json" + "fmt" + "math/big" + "testing" + "time" + + ethCommon "github.com/ethereum/go-ethereum/common" + "github.com/hermeznetwork/hermez-node/common" + "github.com/stretchr/testify/assert" +) + +type testAuth struct { + EthAddr string `json:"hezEthereumAddress" binding:"required"` + BJJ string `json:"bjj" binding:"required"` + Signature string `json:"signature" binding:"required"` + Timestamp time.Time `json:"timestamp"` +} + +func genTestAuths(auths []*common.AccountCreationAuth) []testAuth { + testAuths := []testAuth{} + for _, auth := range auths { + testAuths = append(testAuths, testAuth{ + EthAddr: ethAddrToHez(auth.EthAddr), + BJJ: bjjToString(auth.BJJ), + Signature: "0x" + hex.EncodeToString(auth.Signature), + Timestamp: auth.Timestamp, + }) + } + return testAuths +} + +func TestAccountCreationAuth(t *testing.T) { + // POST + endpoint := apiURL + "account-creation-authorization" + for _, auth := range tc.auths { + jsonAuthBytes, err := json.Marshal(auth) + assert.NoError(t, err) + jsonAuthReader := bytes.NewReader(jsonAuthBytes) + fmt.Println(string(jsonAuthBytes)) + assert.NoError( + t, doGoodReq( + "POST", + endpoint, + jsonAuthReader, nil, + ), + ) + } + // GET + endpoint += "/" + for _, auth := range tc.auths { + fetchedAuth := testAuth{} + assert.NoError( + t, doGoodReq( + "GET", + endpoint+auth.EthAddr, + nil, &fetchedAuth, + ), + ) + assertAuth(t, auth, fetchedAuth) + } + // POST + // 400 + // Wrong addr + badAuth := tc.auths[0] + badAuth.EthAddr = ethAddrToHez(ethCommon.BigToAddress(big.NewInt(1))) + jsonAuthBytes, err := json.Marshal(badAuth) + assert.NoError(t, err) + jsonAuthReader := bytes.NewReader(jsonAuthBytes) + err = doBadReq("POST", endpoint, jsonAuthReader, 400) + assert.NoError(t, err) + // Wrong signature + badAuth = tc.auths[0] + badAuth.Signature = badAuth.Signature[:len(badAuth.Signature)-1] + badAuth.Signature += "F" + jsonAuthBytes, err = json.Marshal(badAuth) + assert.NoError(t, err) + jsonAuthReader = bytes.NewReader(jsonAuthBytes) + err = doBadReq("POST", endpoint, jsonAuthReader, 400) + assert.NoError(t, err) + // GET + // 400 + err = doBadReq("GET", endpoint+"hez:0xFooBar", nil, 400) + assert.NoError(t, err) + // 404 + err = doBadReq("GET", endpoint+"hez:0x0000000000000000000000000000000000000001", nil, 404) + assert.NoError(t, err) +} + +func assertAuth(t *testing.T, expected, actual testAuth) { + // timestamp should be very close to now + assert.Less(t, time.Now().UTC().Unix()-3, actual.Timestamp.Unix()) + expected.Timestamp = actual.Timestamp + assert.Equal(t, expected, actual) +} diff --git a/api/api_test.go b/api/api_test.go index 49640fe..568c1de 100644 --- a/api/api_test.go +++ b/api/api_test.go @@ -1,7 +1,6 @@ package api import ( - "bytes" "context" "encoding/json" "errors" @@ -51,7 +50,7 @@ type testCommon struct { usrExits []exitAPI poolTxsToSend []testPoolTxSend poolTxsToReceive []testPoolTxReceive - auths []accountCreationAuthAPI + auths []testAuth router *swagger.Router bids []testBid } @@ -336,15 +335,6 @@ func TestMain(m *testing.M) { if err != nil { panic(err) } - // Account creation auth - const nAuths = 5 - auths := test.GenAuths(nAuths) - // Transform auths to API format - apiAuths := []accountCreationAuthAPI{} - for _, auth := range auths { - apiAuth := accountCreationAuthToAPI(auth) - apiAuths = append(apiAuths, *apiAuth) - } // Bids const nBids = 10 @@ -373,7 +363,7 @@ func TestMain(m *testing.M) { usrExits: usrExits, poolTxsToSend: poolTxsToSend, poolTxsToReceive: poolTxsToReceive, - auths: apiAuths, + auths: genTestAuths(test.GenAuths(5)), router: router, bids: genTestBids(blocks, coordinators, bids), } @@ -581,70 +571,6 @@ func TestGetConfig(t *testing.T) { assert.Equal(t, cg, &configTest) } -func TestAccountCreationAuth(t *testing.T) { - // POST - endpoint := apiURL + "account-creation-authorization" - for _, auth := range tc.auths { - jsonAuthBytes, err := json.Marshal(auth) - assert.NoError(t, err) - jsonAuthReader := bytes.NewReader(jsonAuthBytes) - fmt.Println(string(jsonAuthBytes)) - assert.NoError( - t, doGoodReq( - "POST", - endpoint, - jsonAuthReader, nil, - ), - ) - } - // GET - endpoint += "/" - for _, auth := range tc.auths { - fetchedAuth := accountCreationAuthAPI{} - assert.NoError( - t, doGoodReq( - "GET", - endpoint+auth.EthAddr, - nil, &fetchedAuth, - ), - ) - assertAuth(t, auth, fetchedAuth) - } - // POST - // 400 - // Wrong addr - badAuth := tc.auths[0] - badAuth.EthAddr = ethAddrToHez(ethCommon.BigToAddress(big.NewInt(1))) - jsonAuthBytes, err := json.Marshal(badAuth) - assert.NoError(t, err) - jsonAuthReader := bytes.NewReader(jsonAuthBytes) - err = doBadReq("POST", endpoint, jsonAuthReader, 400) - assert.NoError(t, err) - // Wrong signature - badAuth = tc.auths[0] - badAuth.Signature = badAuth.Signature[:len(badAuth.Signature)-1] - badAuth.Signature += "F" - jsonAuthBytes, err = json.Marshal(badAuth) - assert.NoError(t, err) - jsonAuthReader = bytes.NewReader(jsonAuthBytes) - err = doBadReq("POST", endpoint, jsonAuthReader, 400) - assert.NoError(t, err) - // GET - // 400 - err = doBadReq("GET", endpoint+"hez:0xFooBar", nil, 400) - assert.NoError(t, err) - // 404 - err = doBadReq("GET", endpoint+"hez:0x0000000000000000000000000000000000000001", nil, 404) - assert.NoError(t, err) -} - -func assertAuth(t *testing.T, expected, actual accountCreationAuthAPI) { - // timestamp should be very close to now - assert.Less(t, time.Now().UTC().Unix()-3, actual.Timestamp.Unix()) - expected.Timestamp = actual.Timestamp - assert.Equal(t, expected, actual) -} - func doGoodReqPaginated( path, order string, iterStruct db.Paginationer, diff --git a/api/dbtoapistructs.go b/api/dbtoapistructs.go index 1eee777..fc47bdd 100644 --- a/api/dbtoapistructs.go +++ b/api/dbtoapistructs.go @@ -2,12 +2,8 @@ package api import ( "encoding/base64" - "encoding/hex" - "errors" "math/big" "strconv" - "strings" - "time" ethCommon "github.com/ethereum/go-ethereum/common" "github.com/hermeznetwork/hermez-node/common" @@ -152,47 +148,3 @@ type configAPI struct { AuctionConstants eth.AuctionConstants `json:"auction"` WDelayerConstants eth.WDelayerConstants `json:"withdrawalDelayer"` } - -// AccountCreationAuth - -type accountCreationAuthAPI struct { - EthAddr string `json:"hezEthereumAddress" binding:"required"` - BJJ string `json:"bjj" binding:"required"` - Signature string `json:"signature" binding:"required"` - Timestamp time.Time `json:"timestamp"` -} - -func accountCreationAuthToAPI(dbAuth *common.AccountCreationAuth) *accountCreationAuthAPI { - return &accountCreationAuthAPI{ - EthAddr: ethAddrToHez(dbAuth.EthAddr), - BJJ: bjjToString(dbAuth.BJJ), - Signature: "0x" + hex.EncodeToString(dbAuth.Signature), - Timestamp: dbAuth.Timestamp, - } -} - -func accountCreationAuthAPIToCommon(apiAuth *accountCreationAuthAPI) (*common.AccountCreationAuth, error) { - ethAddr, err := hezStringToEthAddr(apiAuth.EthAddr, "hezEthereumAddress") - if err != nil { - return nil, err - } - bjj, err := hezStringToBJJ(apiAuth.BJJ, "bjj") - if err != nil { - return nil, err - } - without0x := strings.TrimPrefix(apiAuth.Signature, "0x") - s, err := hex.DecodeString(without0x) - if err != nil { - return nil, err - } - auth := &common.AccountCreationAuth{ - EthAddr: *ethAddr, - BJJ: bjj, - Signature: s, - Timestamp: apiAuth.Timestamp, - } - if !auth.VerifySignature() { - return nil, errors.New("invalid signature") - } - return auth, nil -} diff --git a/api/handlers.go b/api/handlers.go index d4da8ef..6415463 100644 --- a/api/handlers.go +++ b/api/handlers.go @@ -28,46 +28,6 @@ var ( ErrNillBidderAddr = errors.New("biderAddr can not be nil") ) -func postAccountCreationAuth(c *gin.Context) { - // Parse body - var apiAuth accountCreationAuthAPI - if err := c.ShouldBindJSON(&apiAuth); err != nil { - retBadReq(err, c) - return - } - // API to common + verify signature - dbAuth, err := accountCreationAuthAPIToCommon(&apiAuth) - if err != nil { - retBadReq(err, c) - return - } - // Insert to DB - if err := l2.AddAccountCreationAuth(dbAuth); err != nil { - retSQLErr(err, c) - return - } - // Return OK - c.Status(http.StatusOK) -} - -func getAccountCreationAuth(c *gin.Context) { - // Get hezEthereumAddress - addr, err := parseParamHezEthAddr(c) - if err != nil { - retBadReq(err, c) - return - } - // Fetch auth from l2DB - dbAuth, err := l2.GetAccountCreationAuth(*addr) - if err != nil { - retSQLErr(err, c) - return - } - apiAuth := accountCreationAuthToAPI(dbAuth) - // Build succesfull response - c.JSON(http.StatusOK, apiAuth) -} - func getAccounts(c *gin.Context) { } diff --git a/apitypes/apitypes.go b/apitypes/apitypes.go index a97ab98..6d90291 100644 --- a/apitypes/apitypes.go +++ b/apitypes/apitypes.go @@ -3,6 +3,7 @@ package apitypes import ( "database/sql/driver" "encoding/base64" + "encoding/hex" "encoding/json" "errors" "fmt" @@ -155,6 +156,20 @@ func (s *StrHezEthAddr) UnmarshalText(text []byte) error { return nil } +// StrEthSignature is used to unmarshal EthSignature directly into an alias of []byte +type StrEthSignature []byte + +// UnmarshalText unmarshals a StrEthSignature +func (s *StrEthSignature) UnmarshalText(text []byte) error { + without0x := strings.TrimPrefix(string(text), "0x") + signature, err := hex.DecodeString(without0x) + if err != nil { + return err + } + *s = signature + return nil +} + // HezBJJ is used to scan/value *babyjub.PublicKey directly into strings that follow the BJJ public key hez fotmat (^hez:[A-Za-z0-9_-]{44}$) from/to sql DBs. // It assumes that *babyjub.PublicKey are inserted/fetched to/from the DB using the default Scan/Value interface type HezBJJ string @@ -261,3 +276,38 @@ func (s *StrHezIdx) UnmarshalText(text []byte) error { *s = StrHezIdx(common.Idx(idxInt)) return nil } + +// EthSignature is used to scan/value []byte representing an Ethereum signatue directly into strings from/to sql DBs. +type EthSignature string + +// NewEthSignature creates a *EthSignature from []byte +// If the provided signature is nil the returned *EthSignature will also be nil +func NewEthSignature(signature []byte) *EthSignature { + if signature == nil { + return nil + } + ethSignature := EthSignature("0x" + hex.EncodeToString(signature)) + return ðSignature +} + +// Scan implements Scanner for database/sql +func (e *EthSignature) Scan(src interface{}) error { + if srcStr, ok := src.(string); ok { + // src is a string + *e = *(NewEthSignature([]byte(srcStr))) + return nil + } else if srcBytes, ok := src.([]byte); ok { + // src is []byte + *e = *(NewEthSignature(srcBytes)) + return nil + } else { + // unexpected src + return fmt.Errorf("can't scan %T into apitypes.EthSignature", src) + } +} + +// Value implements valuer for database/sql +func (e EthSignature) Value() (driver.Value, error) { + without0x := strings.TrimPrefix(string(e), "0x") + return hex.DecodeString(without0x) +} diff --git a/apitypes/apitypes_test.go b/apitypes/apitypes_test.go index 2ce058d..020fc70 100644 --- a/apitypes/apitypes_test.go +++ b/apitypes/apitypes_test.go @@ -332,3 +332,82 @@ func TestHezBJJ(t *testing.T) { assert.NoError(t, err) assert.Nil(t, toBJJNil.I) } + +func TestEthSignature(t *testing.T) { + // Clean DB + _, err := db.Exec("delete from test") + assert.NoError(t, err) + // Example structs + type ethSignStruct struct { + I []byte `meddler:"i"` + } + type hezEthSignStruct struct { + I EthSignature `meddler:"i"` + } + type hezEthSignStructNil struct { + I *EthSignature `meddler:"i"` + } + + // Not nil case + // Insert into DB using []byte Scan/Value + s := "someRandomFooForYou" + fromEth := ethSignStruct{ + I: []byte(s), + } + err = meddler.Insert(db, "test", &fromEth) + assert.NoError(t, err) + // Read from DB using EthSignature Scan/Value + toHezEth := hezEthSignStruct{} + err = meddler.QueryRow(db, &toHezEth, "select * from test") + assert.NoError(t, err) + assert.Equal(t, NewEthSignature(fromEth.I), &toHezEth.I) + // Clean DB + _, err = db.Exec("delete from test") + assert.NoError(t, err) + // Insert into DB using EthSignature Scan/Value + fromHezEth := hezEthSignStruct{ + I: *NewEthSignature([]byte(s)), + } + err = meddler.Insert(db, "test", &fromHezEth) + assert.NoError(t, err) + // Read from DB using []byte Scan/Value + toEth := ethSignStruct{} + err = meddler.QueryRow(db, &toEth, "select * from test") + assert.NoError(t, err) + assert.Equal(t, &fromHezEth.I, NewEthSignature(toEth.I)) + + // Nil case + // Clean DB + _, err = db.Exec("delete from test") + assert.NoError(t, err) + // Insert into DB using []byte Scan/Value + fromEthNil := ethSignStruct{ + I: nil, + } + err = meddler.Insert(db, "test", &fromEthNil) + assert.NoError(t, err) + // Read from DB using EthSignature Scan/Value + foo := EthSignature("foo") + toHezEthNil := hezEthSignStructNil{ + I: &foo, // check that this will be set to nil, not because of not being initialized + } + err = meddler.QueryRow(db, &toHezEthNil, "select * from test") + assert.NoError(t, err) + assert.Nil(t, toHezEthNil.I) + // Clean DB + _, err = db.Exec("delete from test") + assert.NoError(t, err) + // Insert into DB using EthSignature Scan/Value + fromHezEthNil := hezEthSignStructNil{ + I: nil, + } + err = meddler.Insert(db, "test", &fromHezEthNil) + assert.NoError(t, err) + // Read from DB using []byte Scan/Value + toEthNil := ethSignStruct{ + I: []byte(s), // check that this will be set to nil, not because of not being initialized + } + err = meddler.QueryRow(db, &toEthNil, "select * from test") + assert.NoError(t, err) + assert.Nil(t, toEthNil.I) +} diff --git a/db/l2db/l2db.go b/db/l2db/l2db.go index 9891040..970bb9d 100644 --- a/db/l2db/l2db.go +++ b/db/l2db/l2db.go @@ -55,7 +55,7 @@ func (l2db *L2DB) AddAccountCreationAuth(auth *common.AccountCreationAuth) error return err } -// GetAccountCreationAuth returns an account creation authorization into the DB +// GetAccountCreationAuth returns an account creation authorization from the DB func (l2db *L2DB) GetAccountCreationAuth(addr ethCommon.Address) (*common.AccountCreationAuth, error) { auth := new(common.AccountCreationAuth) return auth, meddler.QueryRow( @@ -65,6 +65,16 @@ func (l2db *L2DB) GetAccountCreationAuth(addr ethCommon.Address) (*common.Accoun ) } +// GetAccountCreationAuthAPI returns an account creation authorization from the DB +func (l2db *L2DB) GetAccountCreationAuthAPI(addr ethCommon.Address) (*AccountCreationAuthAPI, error) { + auth := new(AccountCreationAuthAPI) + return auth, meddler.QueryRow( + l2db.db, auth, + "SELECT * FROM account_creation_auth WHERE eth_addr = $1;", + addr, + ) +} + // AddTx inserts a tx to the pool func (l2db *L2DB) AddTx(tx *PoolL2TxWrite) error { return meddler.Insert(l2db.db, "tx_pool", tx) diff --git a/db/l2db/views.go b/db/l2db/views.go index c398030..b86c34e 100644 --- a/db/l2db/views.go +++ b/db/l2db/views.go @@ -114,3 +114,10 @@ func (tx PoolTxAPI) MarshalJSON() ([]byte, error) { }, }) } + +type AccountCreationAuthAPI struct { + EthAddr apitypes.HezEthAddr `json:"hezEthereumAddress" meddler:"eth_addr" ` + BJJ apitypes.HezBJJ `json:"bjj" meddler:"bjj" ` + Signature apitypes.EthSignature `json:"signature" meddler:"signature" ` + Timestamp time.Time `json:"timestamp" meddler:"timestamp,utctime"` +}