From 39bc4adef699a55d8f014568c8d3745bbd3358bc Mon Sep 17 00:00:00 2001 From: Yang Hau Date: Mon, 24 Jul 2023 17:46:29 +0300 Subject: [PATCH 01/26] refactor: Refactor authentication lib --- packages/authentication/auth_context.go | 19 ++ packages/authentication/basic_auth.go | 35 ---- packages/authentication/context.go | 44 ----- packages/authentication/ip_whitelist.go | 53 ------ packages/authentication/jwt_auth.go | 146 +++------------ packages/authentication/jwt_auth_test.go | 17 +- packages/authentication/jwt_handler.go | 106 ----------- packages/authentication/jwt_login.go | 67 +++++++ packages/authentication/routes.go | 108 +++++++++++ packages/authentication/shared/routes.go | 4 - packages/authentication/status.go | 33 ---- packages/authentication/strategy.go | 171 ------------------ .../authentication/validate_middleware.go | 114 ++++++++++++ .../authentication/validate_permissions.go | 4 - packages/webapi/api.go | 12 +- .../webapi/models/mock/AuthInfoModel.json | 4 + packages/webapi/models/mock/LoginRequest.json | 4 + .../webapi/models/mock/LoginResponse.json | 3 + tools/cluster/templates/waspconfig.go | 8 - 19 files changed, 355 insertions(+), 597 deletions(-) create mode 100644 packages/authentication/auth_context.go delete mode 100644 packages/authentication/basic_auth.go delete mode 100644 packages/authentication/context.go delete mode 100644 packages/authentication/ip_whitelist.go delete mode 100644 packages/authentication/jwt_handler.go create mode 100644 packages/authentication/jwt_login.go create mode 100644 packages/authentication/routes.go delete mode 100644 packages/authentication/status.go delete mode 100644 packages/authentication/strategy.go create mode 100644 packages/authentication/validate_middleware.go create mode 100644 packages/webapi/models/mock/AuthInfoModel.json create mode 100644 packages/webapi/models/mock/LoginRequest.json create mode 100644 packages/webapi/models/mock/LoginResponse.json diff --git a/packages/authentication/auth_context.go b/packages/authentication/auth_context.go new file mode 100644 index 0000000000..679a42d34c --- /dev/null +++ b/packages/authentication/auth_context.go @@ -0,0 +1,19 @@ +package authentication + +import "github.com/labstack/echo/v4" + +type AuthContext struct { + echo.Context + + scheme string + claims *WaspClaims + name string +} + +func (a *AuthContext) Name() string { + return a.name +} + +func (a *AuthContext) Scheme() string { + return a.scheme +} diff --git a/packages/authentication/basic_auth.go b/packages/authentication/basic_auth.go deleted file mode 100644 index 12338e6bd5..0000000000 --- a/packages/authentication/basic_auth.go +++ /dev/null @@ -1,35 +0,0 @@ -package authentication - -import ( - "fmt" - - "github.com/labstack/echo/v4" - "github.com/labstack/echo/v4/middleware" - - "github.com/iotaledger/hive.go/web/basicauth" - "github.com/iotaledger/wasp/packages/users" -) - -func AddBasicAuth(webAPI WebAPI, userManager *users.UserManager) { - webAPI.Use(middleware.BasicAuth(func(username, password string, c echo.Context) (bool, error) { - authContext := c.Get("auth").(*AuthContext) - - user, err := userManager.User(username) - if err != nil { - return false, err - } - - valid, err := basicauth.VerifyPassword([]byte(password), user.PasswordSalt, user.PasswordHash) - if err != nil { - return false, fmt.Errorf("failed to verify password: %w", err) - } - - if !valid { - return false, nil - } - - authContext.name = username - authContext.isAuthenticated = true - return true, nil - })) -} diff --git a/packages/authentication/context.go b/packages/authentication/context.go deleted file mode 100644 index 62496211cc..0000000000 --- a/packages/authentication/context.go +++ /dev/null @@ -1,44 +0,0 @@ -package authentication - -import ( - "github.com/labstack/echo/v4" -) - -type ( - ClaimValidator func(claims *WaspClaims) bool - AccessValidator func(validator ClaimValidator) bool -) - -type AuthContext struct { - echo.Context - - scheme string - isAuthenticated bool - claims *WaspClaims - name string -} - -func (a *AuthContext) Name() string { - return a.name -} - -func (a *AuthContext) IsAuthenticated() bool { - return a.isAuthenticated -} - -func (a *AuthContext) Scheme() string { - return a.scheme -} - -func (a *AuthContext) IsAllowedTo(validator ClaimValidator) bool { - if !a.isAuthenticated { - return false - } - - if a.scheme == AuthJWT { - return validator(a.claims) - } - - // IP Whitelist and Basic Auth will always give access to everything! - return true -} diff --git a/packages/authentication/ip_whitelist.go b/packages/authentication/ip_whitelist.go deleted file mode 100644 index e58944d44e..0000000000 --- a/packages/authentication/ip_whitelist.go +++ /dev/null @@ -1,53 +0,0 @@ -package authentication - -import ( - "net" - "strings" - - "github.com/labstack/echo/v4" -) - -func AddIPWhiteListAuth(webAPI WebAPI, config IPWhiteListAuthConfiguration) { - ipWhiteList := createIPWhiteList(config) - webAPI.Use(protected(ipWhiteList)) -} - -func createIPWhiteList(config IPWhiteListAuthConfiguration) []net.IP { - r := make([]net.IP, 0) - for _, ip := range config.Whitelist { - r = append(r, net.ParseIP(ip)) - } - return r -} - -func isAllowed(ip net.IP, whitelist []net.IP) bool { - if ip.IsLoopback() { - return true - } - for _, whitelistedIP := range whitelist { - if ip.Equal(whitelistedIP) { - return true - } - } - return false -} - -func protected(whitelist []net.IP) echo.MiddlewareFunc { - return func(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { - authContext := c.Get("auth").(*AuthContext) - - parts := strings.Split(c.Request().RemoteAddr, ":") - if len(parts) == 2 { - ip := net.ParseIP(parts[0]) - if ip != nil && isAllowed(ip, whitelist) { - authContext.isAuthenticated = true - return next(c) - } - } - - c.Logger().Infof("Blocking request from %s: %s %s", c.Request().RemoteAddr, c.Request().Method, c.Request().RequestURI) - return echo.ErrUnauthorized - } - } -} diff --git a/packages/authentication/jwt_auth.go b/packages/authentication/jwt_auth.go index c280125ca6..4f02baa94b 100644 --- a/packages/authentication/jwt_auth.go +++ b/packages/authentication/jwt_auth.go @@ -4,16 +4,12 @@ import ( "crypto/subtle" "fmt" "net/http" - "strings" "time" "github.com/golang-jwt/jwt/v5" - echojwt "github.com/labstack/echo-jwt/v4" "github.com/labstack/echo/v4" - "github.com/iotaledger/wasp/packages/authentication/shared" "github.com/iotaledger/wasp/packages/authentication/shared/permissions" - "github.com/iotaledger/wasp/packages/users" ) // Errors @@ -32,8 +28,6 @@ type JWTAuth struct { secret []byte } -type MiddlewareValidator = func(c echo.Context, authContext *AuthContext) bool - func NewJWTAuth(duration time.Duration, nodeID string, secret []byte) *JWTAuth { return &JWTAuth{ duration: duration, @@ -42,6 +36,32 @@ func NewJWTAuth(duration time.Duration, nodeID string, secret []byte) *JWTAuth { } } +func (j *JWTAuth) IssueJWT(username string, claims *WaspClaims) (string, error) { + now := time.Now() + + // Set claims + registeredClaims := jwt.RegisteredClaims{ + Subject: username, + Issuer: j.nodeID, + Audience: jwt.ClaimStrings{j.nodeID}, + ID: fmt.Sprintf("%d", now.Unix()), + IssuedAt: jwt.NewNumericDate(now), + NotBefore: jwt.NewNumericDate(now), + } + + if j.duration > 0 { + registeredClaims.ExpiresAt = jwt.NewNumericDate(now.Add(j.duration)) + } + + claims.RegisteredClaims = registeredClaims + + // Create token + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + + // Generate encoded token and send it as response. + return token.SignedString(j.secret) +} + type WaspClaims struct { jwt.RegisteredClaims Permissions map[string]struct{} `json:"permissions"` @@ -78,117 +98,3 @@ func (c *WaspClaims) compare(field, expected string) bool { func (c *WaspClaims) VerifySubject(expected string) bool { return c.compare(c.Subject, expected) } - -func (j *JWTAuth) IssueJWT(username string, authClaims *WaspClaims) (string, error) { - now := time.Now() - - // Set claims - registeredClaims := jwt.RegisteredClaims{ - Subject: username, - Issuer: j.nodeID, - Audience: jwt.ClaimStrings{j.nodeID}, - ID: fmt.Sprintf("%d", now.Unix()), - IssuedAt: jwt.NewNumericDate(now), - NotBefore: jwt.NewNumericDate(now), - } - - if j.duration > 0 { - registeredClaims.ExpiresAt = jwt.NewNumericDate(now.Add(j.duration)) - } - - authClaims.RegisteredClaims = registeredClaims - - // Create token - token := jwt.NewWithClaims(jwt.SigningMethodHS256, authClaims) - - // Generate encoded token and send it as response. - return token.SignedString(j.secret) -} - -var DefaultJWTDuration time.Duration - -func AddJWTAuth(config JWTAuthConfiguration, privateKey []byte, userManager *users.UserManager, claimValidator ClaimValidator) (*JWTAuth, func() echo.MiddlewareFunc) { - duration := config.Duration - - // If durationHours is 0, we set 24h as the default duration - if duration == 0 { - duration = DefaultJWTDuration - } - - // FIXME: replace "wasp" as nodeID - jwtAuth := NewJWTAuth(duration, "wasp", privateKey) - - authMiddleware := func() echo.MiddlewareFunc { - return echojwt.WithConfig(echojwt.Config{ - ContextKey: JWTContextKey, - NewClaimsFunc: func(c echo.Context) jwt.Claims { - return &WaspClaims{} - }, - Skipper: func(c echo.Context) bool { - path := c.Request().URL.Path - if path == "/" || - strings.HasSuffix(path, shared.AuthRoute()) || - strings.HasSuffix(path, shared.AuthInfoRoute()) || - strings.HasPrefix(path, "/doc") { - return true - } - - return false - }, - SigningKey: jwtAuth.secret, - TokenLookup: "header:Authorization:Bearer ,cookie:jwt", - ParseTokenFunc: func(c echo.Context, auth string) (interface{}, error) { - keyFunc := func(t *jwt.Token) (interface{}, error) { - if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok { - return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"]) - } - - return jwtAuth.secret, nil - } - - token, err := jwt.ParseWithClaims( - auth, - &WaspClaims{}, - keyFunc, - jwt.WithValidMethods([]string{"HS256"}), - ) - if err != nil { - return nil, err - } - if !token.Valid { - return nil, fmt.Errorf("invalid token") - } - - claims, ok := token.Claims.(*WaspClaims) - if !ok { - return nil, fmt.Errorf("wrong JWT claim type") - } - - audience, err := claims.GetAudience() - if err != nil { - return nil, err - } - b, err := audience.MarshalJSON() - if err != nil { - return nil, err - } - if subtle.ConstantTimeCompare(b, []byte(fmt.Sprintf("[%q]", jwtAuth.nodeID))) == 0 { - return nil, fmt.Errorf("not in audience") - } - - userMap := userManager.Users() - if _, ok := userMap[claims.Subject]; !ok { - return nil, fmt.Errorf("invalid subject") - } - - authContext := c.Get("auth").(*AuthContext) - authContext.isAuthenticated = true - authContext.claims = claims - - return token, nil - }, - }) - } - - return jwtAuth, authMiddleware -} diff --git a/packages/authentication/jwt_auth_test.go b/packages/authentication/jwt_auth_test.go index 35e6070742..80b3d22614 100644 --- a/packages/authentication/jwt_auth_test.go +++ b/packages/authentication/jwt_auth_test.go @@ -16,7 +16,7 @@ import ( "github.com/iotaledger/wasp/packages/users" ) -func TestAddJWTAuth(t *testing.T) { +func TestGetJWTAuthMiddleware(t *testing.T) { t.Run("normal", func(t *testing.T) { e := echo.New() e.GET("/test-route", func(c echo.Context) error { @@ -32,11 +32,10 @@ func TestAddJWTAuth(t *testing.T) { Name: "wasp", }) - _, middleware := authentication.AddJWTAuth( + _, middleware := authentication.GetJWTAuthMiddleware( authentication.JWTAuthConfiguration{}, []byte("abc"), userManager, - nil, // remove claim validator ) e.Use(func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { @@ -45,7 +44,7 @@ func TestAddJWTAuth(t *testing.T) { return next(c) } }) - e.Use(middleware()) + e.Use(middleware) req := httptest.NewRequest(http.MethodGet, "/test-route", http.NoBody) req.Header.Set(echo.HeaderAuthorization, "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ3YXNwIiwic3ViIjoid2FzcCIsImF1ZCI6WyJ3YXNwIl0sImV4cCI6NDg0NTUwNjQ5MiwibmJmIjoxNjg5ODYxNDM2LCJpYXQiOjE2ODk4NjE0MzYsImp0aSI6IjE2ODk4NjE0MzYiLCJwZXJtaXNzaW9ucyI6eyJ3cml0ZSI6e319fQ.VP--725H3xO2Spz6L9twB6Tsm37a26IXVU87cSqRoOM") @@ -73,13 +72,12 @@ func TestAddJWTAuth(t *testing.T) { }) } - _, middleware := authentication.AddJWTAuth( + _, middleware := authentication.GetJWTAuthMiddleware( authentication.JWTAuthConfiguration{}, []byte(""), &users.UserManager{}, - nil, // remove claim validator ) - e.Use(middleware()) + e.Use(middleware) for _, path := range skipPaths { req := httptest.NewRequest(http.MethodGet, path, http.NoBody) @@ -119,11 +117,10 @@ func TestJWTAuthIssueAndVerify(t *testing.T) { Name: username, }) - _, middleware := authentication.AddJWTAuth( + _, middleware := authentication.GetJWTAuthMiddleware( authentication.JWTAuthConfiguration{Duration: duration}, privateKey, userManager, - nil, // remove claim validator ) e.Use(func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { @@ -132,7 +129,7 @@ func TestJWTAuthIssueAndVerify(t *testing.T) { return next(c) } }) - e.Use(middleware()) + e.Use(middleware) req := httptest.NewRequest(http.MethodGet, "/test-route", http.NoBody) req.Header.Set(echo.HeaderAuthorization, fmt.Sprintf("Bearer %s", jwtString)) diff --git a/packages/authentication/jwt_handler.go b/packages/authentication/jwt_handler.go deleted file mode 100644 index cb7618f2c0..0000000000 --- a/packages/authentication/jwt_handler.go +++ /dev/null @@ -1,106 +0,0 @@ -package authentication - -import ( - "errors" - "fmt" - "net/http" - "time" - - "github.com/labstack/echo/v4" - - "github.com/iotaledger/hive.go/web/basicauth" - "github.com/iotaledger/wasp/packages/authentication/shared" - "github.com/iotaledger/wasp/packages/users" -) - -const headerXForwardedPrefix = "X-Forwarded-Prefix" - -type AuthHandler struct { - Jwt *JWTAuth - UserManager *users.UserManager -} - -func (a *AuthHandler) validateLogin(user *users.User, password string) bool { - valid, err := basicauth.VerifyPassword([]byte(password), user.PasswordSalt, user.PasswordHash) - if err != nil { - return false - } - - return valid -} - -func (a *AuthHandler) stageAuthRequest(c echo.Context) (string, error) { - request := &shared.LoginRequest{} - - if err := c.Bind(request); err != nil { - return "", errors.New("invalid form data") - } - - user, err := a.UserManager.User(request.Username) - if err != nil { - return "", errors.New("invalid credentials") - } - - if !a.validateLogin(user, request.Password) { - return "", errors.New("invalid credentials") - } - - claims := &WaspClaims{ - Permissions: user.Permissions, - } - - token, err := a.Jwt.IssueJWT(request.Username, claims) - if err != nil { - return "", errors.New("unable to login") - } - - return token, nil -} - -func (a *AuthHandler) handleJSONAuthRequest(c echo.Context, token string, errorResult error) error { - if errorResult != nil { - return c.JSON(http.StatusUnauthorized, shared.LoginResponse{Error: errorResult}) - } - - return c.JSON(http.StatusOK, shared.LoginResponse{JWT: token}) -} - -func (a *AuthHandler) redirect(c echo.Context, uri string) error { - return c.Redirect(http.StatusFound, c.Request().Header.Get(headerXForwardedPrefix)+uri) -} - -func (a *AuthHandler) handleFormAuthRequest(c echo.Context, token string, errorResult error) error { - if errorResult != nil { - // TODO: Add sessions to get rid of the query parameter? - return a.redirect(c, fmt.Sprintf("%s?error=%s", shared.AuthRoute(), errorResult)) - } - - cookie := http.Cookie{ - Name: "jwt", - Value: token, - HttpOnly: true, // JWT Token will be stored in a http only cookie, this is important to mitigate XSS/XSRF attacks - Expires: time.Now().Add(a.Jwt.duration), - Path: "/", - SameSite: http.SameSiteStrictMode, - } - - c.SetCookie(&cookie) - - return a.redirect(c, shared.AuthRouteSuccess()) -} - -func (a *AuthHandler) CrossAPIAuthHandler(c echo.Context) error { - token, errorResult := a.stageAuthRequest(c) - - contentType := c.Request().Header.Get(echo.HeaderContentType) - - if contentType == echo.MIMEApplicationJSON { - return a.handleJSONAuthRequest(c, token, errorResult) - } - - if contentType == echo.MIMEApplicationForm { - return a.handleFormAuthRequest(c, token, errorResult) - } - - return errors.New("invalid login request") -} diff --git a/packages/authentication/jwt_login.go b/packages/authentication/jwt_login.go new file mode 100644 index 0000000000..700de01cf7 --- /dev/null +++ b/packages/authentication/jwt_login.go @@ -0,0 +1,67 @@ +package authentication + +import ( + "errors" + "fmt" + "net/http" + + "github.com/labstack/echo/v4" + + "github.com/iotaledger/hive.go/web/basicauth" + "github.com/iotaledger/wasp/packages/authentication/shared" + "github.com/iotaledger/wasp/packages/users" +) + +type AuthHandler struct { + Jwt *JWTAuth + UserManager *users.UserManager +} + +func (a *AuthHandler) JWTLoginHandler(c echo.Context) error { + if c.Request().Header.Get(echo.HeaderContentType) != echo.MIMEApplicationJSON { + return errors.New("invalid login request") + } + + req, user, err := a.parseAuthRequest(c) + if err != nil { + return c.JSON(http.StatusUnauthorized, shared.LoginResponse{Error: err}) + } + + claims := &WaspClaims{ + Permissions: user.Permissions, + } + token, err := a.Jwt.IssueJWT(req.Username, claims) + if err != nil { + return c.JSON(http.StatusUnauthorized, shared.LoginResponse{Error: fmt.Errorf("unable to login")}) + } + + return c.JSON(http.StatusOK, shared.LoginResponse{JWT: token}) +} + +func (a *AuthHandler) parseAuthRequest(c echo.Context) (*shared.LoginRequest, *users.User, error) { + request := &shared.LoginRequest{} + + if err := c.Bind(request); err != nil { + return nil, nil, fmt.Errorf("invalid form data") + } + + user, err := a.UserManager.User(request.Username) + if err != nil { + return nil, nil, fmt.Errorf("invalid credentials") + } + + if !validatePassword(user, request.Password) { + return nil, nil, fmt.Errorf("invalid credentials") + } + + return request, user, nil +} + +func validatePassword(user *users.User, password string) bool { + valid, err := basicauth.VerifyPassword([]byte(password), user.PasswordSalt, user.PasswordHash) + if err != nil { + return false + } + + return valid +} diff --git a/packages/authentication/routes.go b/packages/authentication/routes.go new file mode 100644 index 0000000000..d509f340e2 --- /dev/null +++ b/packages/authentication/routes.go @@ -0,0 +1,108 @@ +package authentication + +import ( + "fmt" + "net/http" + "time" + + "github.com/labstack/echo/v4" + "github.com/pangpanglabs/echoswagger/v2" + + "github.com/iotaledger/wasp/packages/authentication/shared" + "github.com/iotaledger/wasp/packages/registry" + "github.com/iotaledger/wasp/packages/users" + "github.com/iotaledger/wasp/packages/webapi/interfaces" +) + +const ( + AuthNone = "none" + AuthJWT = "jwt" +) + +type JWTAuthConfiguration struct { + Duration time.Duration `default:"24h" usage:"jwt token lifetime"` +} + +type AuthConfiguration struct { + Scheme string `default:"ip" usage:"selects which authentication to choose"` + + JWTConfig JWTAuthConfiguration `name:"jwt" usage:"defines the jwt configuration"` +} + +type WebAPI interface { + GET(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route + POST(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route + Use(middleware ...echo.MiddlewareFunc) +} + +func AddAuthentication( + apiRoot echoswagger.ApiRoot, + userManager *users.UserManager, + nodeIdentityProvider registry.NodeIdentityProvider, + authConfig AuthConfiguration, + mocker interfaces.Mocker, +) echo.MiddlewareFunc { + echoRoot := apiRoot.Echo() + authGroup := apiRoot.Group("auth", "") + + // initialize AuthContext obj as var in echo.Context + echoRoot.Use(func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + c.Set("auth", &AuthContext{ + scheme: authConfig.Scheme, + }) + + return next(c) + } + }) + + // set AuthInfo route + authGroup.GET(shared.AuthInfoRoute(), authInfoHandler(authConfig)). + AddResponse(http.StatusOK, "Login was successful", mocker.Get(shared.AuthInfoModel{}), nil). + SetOperationId("authInfo"). + SetSummary("Get information about the current authentication mode") + + // set Auth route + var middleware echo.MiddlewareFunc + var handler echo.HandlerFunc + switch authConfig.Scheme { + case AuthJWT: + var jwtAuth *JWTAuth + privateKey := nodeIdentityProvider.NodeIdentity().GetPrivateKey().AsBytes() + + // The primary claim is the one mandatory claim that gives access to api/webapi/alike + jwtAuth, middleware = GetJWTAuthMiddleware(authConfig.JWTConfig, privateKey, userManager) + authHandler := &AuthHandler{Jwt: jwtAuth, UserManager: userManager} + handler = authHandler.JWTLoginHandler + + case AuthNone: + middleware = GetNoneAuthMiddleware() + handler = nil + + default: + panic(fmt.Sprintf("Unknown auth scheme %s", authConfig.Scheme)) + } + + authGroup.POST(shared.AuthRoute(), handler). + AddParamBody(mocker.Get(shared.LoginRequest{}), "", "The login request", true). + AddResponse(http.StatusUnauthorized, "Unauthorized (Wrong permissions, missing token)", nil, nil). + AddResponse(http.StatusMethodNotAllowed, "auth type: none", nil, nil). + AddResponse(http.StatusOK, "Login was successful", mocker.Get(shared.LoginResponse{}), nil). + SetOperationId("authenticate"). + SetSummary("Authenticate towards the node") + return middleware +} + +func authInfoHandler(authConfig AuthConfiguration) func(c echo.Context) error { + return func(c echo.Context) error { + model := shared.AuthInfoModel{ + Scheme: authConfig.Scheme, + } + + if model.Scheme == AuthJWT { + model.AuthURL = shared.AuthRoute() + } + + return c.JSON(http.StatusOK, model) + } +} diff --git a/packages/authentication/shared/routes.go b/packages/authentication/shared/routes.go index 25dffdbb62..f71811051d 100644 --- a/packages/authentication/shared/routes.go +++ b/packages/authentication/shared/routes.go @@ -4,10 +4,6 @@ func AuthRoute() string { return "/auth" } -func AuthRouteSuccess() string { - return "/auth/success" -} - func AuthInfoRoute() string { return "/auth/info" } diff --git a/packages/authentication/status.go b/packages/authentication/status.go deleted file mode 100644 index 046269882a..0000000000 --- a/packages/authentication/status.go +++ /dev/null @@ -1,33 +0,0 @@ -package authentication - -import ( - "net/http" - - "github.com/labstack/echo/v4" - - "github.com/iotaledger/wasp/packages/authentication/shared" -) - -type StatusWebAPIModel struct { - config AuthConfiguration -} - -func (a *StatusWebAPIModel) handleAuthenticationStatus(c echo.Context) error { - model := shared.AuthInfoModel{ - Scheme: a.config.Scheme, - } - - if model.Scheme == AuthJWT { - model.AuthURL = shared.AuthRoute() - } - - return c.JSON(http.StatusOK, model) -} - -func addAuthenticationStatus(webAPI WebAPI, config AuthConfiguration) { - c := &StatusWebAPIModel{ - config: config, - } - - webAPI.GET(shared.AuthInfoRoute(), c.handleAuthenticationStatus) -} diff --git a/packages/authentication/strategy.go b/packages/authentication/strategy.go deleted file mode 100644 index 0f61e0fea9..0000000000 --- a/packages/authentication/strategy.go +++ /dev/null @@ -1,171 +0,0 @@ -package authentication - -import ( - "fmt" - "net/http" - "time" - - "github.com/labstack/echo/v4" - "github.com/pangpanglabs/echoswagger/v2" - - "github.com/iotaledger/wasp/packages/authentication/shared" - "github.com/iotaledger/wasp/packages/registry" - "github.com/iotaledger/wasp/packages/users" -) - -const ( - AuthJWT = "jwt" - AuthBasic = "basic" - AuthIPWhitelist = "ip" - AuthNone = "none" -) - -type JWTAuthConfiguration struct { - Duration time.Duration `default:"24h" usage:"jwt token lifetime"` -} - -type BasicAuthConfiguration struct { - Username string `default:"wasp" usage:"the username which grants access to the service"` -} - -type IPWhiteListAuthConfiguration struct { - Whitelist []string `default:"0.0.0.0" usage:"a list of ips that are allowed to access the service"` -} - -type AuthConfiguration struct { - Scheme string `default:"ip" usage:"selects which authentication to choose"` - - JWTConfig JWTAuthConfiguration `name:"jwt" usage:"defines the jwt configuration"` - BasicAuthConfig BasicAuthConfiguration `name:"basic" usage:"defines the basic auth configuration"` - IPWhitelistConfig IPWhiteListAuthConfiguration `name:"ip" usage:"defines the whitelist configuration"` -} - -type WebAPI interface { - GET(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route - POST(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route - Use(middleware ...echo.MiddlewareFunc) -} - -func AddNoneAuth(webAPI WebAPI) { - // Adds a middleware to set the authContext to authenticated. - // All routes will be open to everyone, so use it in private environments only. - // Handle with care! - noneFunc := func(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { - authContext := c.Get("auth").(*AuthContext) - - authContext.isAuthenticated = true - - return next(c) - } - } - - webAPI.Use(noneFunc) -} - -func AddV1Authentication( - webAPI WebAPI, - userManager *users.UserManager, - nodeIdentityProvider registry.NodeIdentityProvider, - authConfig AuthConfiguration, - claimValidator ClaimValidator, -) { - addAuthContext(webAPI, authConfig) - - switch authConfig.Scheme { - case AuthBasic: - AddBasicAuth(webAPI, userManager) - case AuthJWT: - nodeIdentity := nodeIdentityProvider.NodeIdentity() - privateKey := nodeIdentity.GetPrivateKey().AsBytes() - - // The primary claim is the one mandatory claim that gives access to api/webapi/alike - jwtAuth, authMiddleware := AddJWTAuth(authConfig.JWTConfig, privateKey, userManager, claimValidator) - - authHandler := &AuthHandler{Jwt: jwtAuth, UserManager: userManager} - webAPI.POST(shared.AuthRoute(), authHandler.CrossAPIAuthHandler) - webAPI.Use(authMiddleware()) - - case AuthIPWhitelist: - AddIPWhiteListAuth(webAPI, authConfig.IPWhitelistConfig) - - case AuthNone: - AddNoneAuth(webAPI) - - default: - panic(fmt.Sprintf("Unknown auth scheme %s", authConfig.Scheme)) - } - - addAuthenticationStatus(webAPI, authConfig) -} - -// TODO: After deprecating V1 we can slim down this whole strategy handler. -// It is currently needed as the current authentication scheme does not support echoSwagger, -// which leaves authentication out of the client code generator. -// After v1 gets removed: -// * Get rid off basic/ip auth and only keeping 'none' and 'JWT' -// * Properly document the routes with echoSwagger -// * Keep only one AddAuthentication method - -func AddV2Authentication(apiRoot echoswagger.ApiRoot, - userManager *users.UserManager, - nodeIdentityProvider registry.NodeIdentityProvider, - authConfig AuthConfiguration, - claimValidator ClaimValidator, -) func() echo.MiddlewareFunc { - echoRoot := apiRoot.Echo() - authGroup := apiRoot.Group("auth", "") - - addAuthContext(echoRoot, authConfig) - - c := &StatusWebAPIModel{ - config: authConfig, - } - - authGroup.GET(shared.AuthInfoRoute(), c.handleAuthenticationStatus). - AddResponse(http.StatusOK, "Login was successful", shared.AuthInfoModel{}, nil). - SetOperationId("authInfo"). - SetSummary("Get information about the current authentication mode") - - switch authConfig.Scheme { - case AuthJWT: - nodeIdentity := nodeIdentityProvider.NodeIdentity() - privateKey := nodeIdentity.GetPrivateKey().AsBytes() - - // The primary claim is the one mandatory claim that gives access to api/webapi/alike - jwtAuth, jwtMiddleware := AddJWTAuth(authConfig.JWTConfig, privateKey, userManager, claimValidator) - - authHandler := &AuthHandler{Jwt: jwtAuth, UserManager: userManager} - authGroup.POST(shared.AuthRoute(), authHandler.CrossAPIAuthHandler). - AddParamBody(shared.LoginRequest{}, "", "The login request", true). - AddResponse(http.StatusUnauthorized, "Unauthorized (Wrong permissions, missing token)", nil, nil). - AddResponse(http.StatusOK, "Login was successful", shared.LoginResponse{}, nil). - SetOperationId("authenticate"). - SetSummary("Authenticate towards the node") - - return jwtMiddleware - - case AuthNone: - AddNoneAuth(echoRoot) - authGroup.POST(shared.AuthRoute(), nil). - AddResponse(http.StatusMethodNotAllowed, "auth type: none", nil, nil) - return nil - - default: - panic(fmt.Sprintf("Unknown auth scheme %s", authConfig.Scheme)) - } -} - -func addAuthContext(webAPI WebAPI, config AuthConfiguration) { - webAPI.Use(func(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { - cc := &AuthContext{ - scheme: config.Scheme, - } - - c.Set("auth", cc) - - return next(c) - } - }) -} diff --git a/packages/authentication/validate_middleware.go b/packages/authentication/validate_middleware.go new file mode 100644 index 0000000000..47ef38be51 --- /dev/null +++ b/packages/authentication/validate_middleware.go @@ -0,0 +1,114 @@ +package authentication + +import ( + "crypto/subtle" + "fmt" + "strings" + "time" + + "github.com/golang-jwt/jwt/v5" + echojwt "github.com/labstack/echo-jwt/v4" + "github.com/labstack/echo/v4" + + "github.com/iotaledger/wasp/packages/authentication/shared" + "github.com/iotaledger/wasp/packages/users" +) + +var DefaultJWTDuration time.Duration + +func GetJWTAuthMiddleware( + config JWTAuthConfiguration, + privateKey []byte, + userManager *users.UserManager, +) (*JWTAuth, echo.MiddlewareFunc) { + duration := config.Duration + // If durationHours is 0, we set 24h as the default duration + if duration == 0 { + duration = DefaultJWTDuration + } + + // FIXME: replace "wasp" as nodeID + jwtAuth := NewJWTAuth(duration, "wasp", privateKey) + + authMiddleware := echojwt.WithConfig(echojwt.Config{ + ContextKey: JWTContextKey, + NewClaimsFunc: func(c echo.Context) jwt.Claims { + return &WaspClaims{} + }, + Skipper: func(c echo.Context) bool { + path := c.Request().URL.Path + if path == "/" || + strings.HasSuffix(path, shared.AuthRoute()) || + strings.HasSuffix(path, shared.AuthInfoRoute()) || + strings.HasPrefix(path, "/doc") { + return true + } + + return false + }, + SigningKey: jwtAuth.secret, + TokenLookup: "header:Authorization:Bearer ,cookie:jwt", + ParseTokenFunc: func(c echo.Context, auth string) (interface{}, error) { + keyFunc := func(t *jwt.Token) (interface{}, error) { + if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"]) + } + + return jwtAuth.secret, nil + } + + token, err := jwt.ParseWithClaims( + auth, + &WaspClaims{}, + keyFunc, + jwt.WithValidMethods([]string{"HS256"}), + ) + if err != nil { + return nil, err + } + if !token.Valid { + return nil, fmt.Errorf("invalid token") + } + + claims, ok := token.Claims.(*WaspClaims) + if !ok { + return nil, fmt.Errorf("wrong JWT claim type") + } + + audience, err := claims.GetAudience() + if err != nil { + return nil, err + } + b, err := audience.MarshalJSON() + if err != nil { + return nil, err + } + if subtle.ConstantTimeCompare(b, []byte(fmt.Sprintf("[%q]", jwtAuth.nodeID))) == 0 { + return nil, fmt.Errorf("not in audience") + } + + userMap := userManager.Users() + if _, ok := userMap[claims.Subject]; !ok { + return nil, fmt.Errorf("invalid subject") + } + + authContext := c.Get("auth").(*AuthContext) + authContext.claims = claims + + return token, nil + }, + }) + + return jwtAuth, authMiddleware +} + +func GetNoneAuthMiddleware() echo.MiddlewareFunc { + // Adds a middleware to set the authContext to authenticated. + // All routes will be open to everyone, so use it in private environments only. + // Handle with care! + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + return next(c) + } + } +} diff --git a/packages/authentication/validate_permissions.go b/packages/authentication/validate_permissions.go index 2c5364b1fa..88c8fe4d3b 100644 --- a/packages/authentication/validate_permissions.go +++ b/packages/authentication/validate_permissions.go @@ -28,10 +28,6 @@ func ValidatePermissions(permissions []string) func(next echo.HandlerFunc) echo. return next(e) } - if !authContext.IsAuthenticated() { - return e.JSON(http.StatusUnauthorized, ValidationError{Error: "Invalid token"}) - } - for _, permission := range permissions { if !authContext.claims.HasPermission(permission) { return e.JSON(http.StatusUnauthorized, ValidationError{MissingPermission: permission, Error: "Missing permission"}) diff --git a/packages/webapi/api.go b/packages/webapi/api.go index 526a672625..b6e4b37e9b 100644 --- a/packages/webapi/api.go +++ b/packages/webapi/api.go @@ -48,7 +48,7 @@ func AddHealthEndpoint(server echoswagger.ApiRoot, chainService interfaces.Chain SetSummary("Returns 200 if the node is healthy.") } -func loadControllers(server echoswagger.ApiRoot, mocker *Mocker, controllersToLoad []interfaces.APIController, authMiddleware func() echo.MiddlewareFunc) { +func loadControllers(server echoswagger.ApiRoot, mocker *Mocker, controllersToLoad []interfaces.APIController, authMiddleware echo.MiddlewareFunc) { for _, controller := range controllersToLoad { group := server.Group(controller.Name(), fmt.Sprintf("/v%d/", APIVersion)) controller.RegisterPublic(group, mocker) @@ -66,7 +66,7 @@ func loadControllers(server echoswagger.ApiRoot, mocker *Mocker, controllersToLo } if authMiddleware != nil { - group.EchoGroup().Use(authMiddleware()) + group.EchoGroup().Use(authMiddleware) } controller.RegisterAdmin(adminGroup, mocker) @@ -110,13 +110,7 @@ func Init( userService := services.NewUserService(userManager) // -- - claimValidator := func(claims *authentication.WaspClaims) bool { - // The v2 api uses another way of permission handling, so we can always return true here. - // Permissions are now validated at the route level. See the webapi/v2/controllers/*/controller.go routes. - return true - } - - authMiddleware := authentication.AddV2Authentication(server, userManager, nodeIdentityProvider, authConfig, claimValidator) + authMiddleware := authentication.AddAuthentication(server, userManager, nodeIdentityProvider, authConfig, mocker) controllersToLoad := []interfaces.APIController{ chain.NewChainController(logger, chainService, committeeService, evmService, nodeService, offLedgerService, registryService), diff --git a/packages/webapi/models/mock/AuthInfoModel.json b/packages/webapi/models/mock/AuthInfoModel.json new file mode 100644 index 0000000000..8aca4cda59 --- /dev/null +++ b/packages/webapi/models/mock/AuthInfoModel.json @@ -0,0 +1,4 @@ +{ + "scheme": "jwt", + "authURL": "/auth" +} \ No newline at end of file diff --git a/packages/webapi/models/mock/LoginRequest.json b/packages/webapi/models/mock/LoginRequest.json new file mode 100644 index 0000000000..74d65e01e8 --- /dev/null +++ b/packages/webapi/models/mock/LoginRequest.json @@ -0,0 +1,4 @@ +{ + "username":"wasp", + "password":"wasp" +} \ No newline at end of file diff --git a/packages/webapi/models/mock/LoginResponse.json b/packages/webapi/models/mock/LoginResponse.json new file mode 100644 index 0000000000..02fe1cf4aa --- /dev/null +++ b/packages/webapi/models/mock/LoginResponse.json @@ -0,0 +1,3 @@ +{ + "jwt": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ3YXNwIiwic3ViIjoid2FzcCIsImF1ZCI6WyJ3YXNwIl0sImV4cCI6MTY4OTk1MTAyNCwibmJmIjoxNjg5ODY0NjI0LCJpYXQiOjE2ODk4NjQ2MjQsImp0aSI6IjE2ODk4NjQ2MjQiLCJwZXJtaXNzaW9ucyI6eyJ3cml0ZSI6e319fQ.LNUuTaoRjEPQyD2nQ00O6NeadiG7nmOEyVIQmGNb1a0" +} \ No newline at end of file diff --git a/tools/cluster/templates/waspconfig.go b/tools/cluster/templates/waspconfig.go index fae6698a82..6de466792b 100644 --- a/tools/cluster/templates/waspconfig.go +++ b/tools/cluster/templates/waspconfig.go @@ -118,14 +118,6 @@ var WaspConfig = ` "scheme": "none", "jwt": { "duration": "24h" - }, - "basic": { - "username": "wasp" - }, - "ip": { - "whitelist": [ - "0.0.0.0" - ] } }, "limits": { From 32a009343627b458d36bf5416f04d85a9371301c Mon Sep 17 00:00:00 2001 From: Yang Hau Date: Thu, 27 Jul 2023 13:28:16 +0300 Subject: [PATCH 02/26] feat: Allow jwt in cluster tests --- tools/cluster/cluster.go | 39 +++++++++++++++++++++++---- tools/cluster/config.go | 1 + tools/cluster/templates/waspconfig.go | 3 ++- tools/cluster/tests/wasp-cli_test.go | 15 +++++++++++ 4 files changed, 52 insertions(+), 6 deletions(-) diff --git a/tools/cluster/cluster.go b/tools/cluster/cluster.go index f114bf2d24..2c159e4207 100644 --- a/tools/cluster/cluster.go +++ b/tools/cluster/cluster.go @@ -124,13 +124,35 @@ func (clu *Cluster) AddTrustedNode(peerInfo apiclient.PeeringTrustRequest, onNod return nil } -func (clu *Cluster) TrustAll() error { +func (clu *Cluster) Login() ([]string, error) { + allNodes := clu.Config.AllNodes() + jwtTokens := make([]string, len(allNodes)) + for ni := range allNodes { + res, _, err := clu.WaspClient(allNodes[ni]).AuthApi.Authenticate(context.Background()). + LoginRequest(*apiclient.NewLoginRequest("wasp", "wasp")). + Execute() //nolint:bodyclose // false positive + if err != nil { + return nil, err + } + jwtTokens[ni] = "Bearer " + res.Jwt + } + return jwtTokens, nil +} + +func (clu *Cluster) TrustAll(jwtTokens ...string) error { allNodes := clu.Config.AllNodes() allPeers := make([]*apiclient.PeeringNodeIdentityResponse, len(allNodes)) + clients := make([]*apiclient.APIClient, len(allNodes)) + for ni := range allNodes { + clients[ni] = clu.WaspClient(allNodes[ni]) + if jwtTokens != nil { + clients[ni].GetConfig().AddDefaultHeader("Authorization", jwtTokens[ni]) + } + } for ni := range allNodes { var err error //nolint:bodyclose // false positive - if allPeers[ni], _, err = clu.WaspClient(allNodes[ni]).NodeApi.GetPeeringIdentity(context.Background()).Execute(); err != nil { + if allPeers[ni], _, err = clients[ni].NodeApi.GetPeeringIdentity(context.Background()).Execute(); err != nil { return err } } @@ -140,7 +162,7 @@ func (clu *Cluster) TrustAll() error { if ni == pi { continue // dont trust self } - if _, err = clu.WaspClient(allNodes[ni]).NodeApi.TrustPeer(context.Background()).PeeringTrustRequest( + if _, err = clients[ni].NodeApi.TrustPeer(context.Background()).PeeringTrustRequest( apiclient.PeeringTrustRequest{ Name: fmt.Sprintf("%d", pi), PublicKey: allPeers[pi].PublicKey, @@ -534,11 +556,18 @@ func (clu *Cluster) StartAndTrustAll(dataPath string) error { return fmt.Errorf("data path %s does not exist", dataPath) } - if err := clu.Start(); err != nil { + if err = clu.Start(); err != nil { return err } - if err := clu.TrustAll(); err != nil { + var jwtTokens []string + if clu.Config.Wasp[0].AuthScheme == "jwt" { + if jwtTokens, err = clu.Login(); err != nil { + return err + } + } + + if err := clu.TrustAll(jwtTokens...); err != nil { return err } diff --git a/tools/cluster/config.go b/tools/cluster/config.go index 2d44cfb822..cf57d2732f 100644 --- a/tools/cluster/config.go +++ b/tools/cluster/config.go @@ -29,6 +29,7 @@ func (w *WaspConfig) WaspConfigTemplateParams(i int) templates.WaspConfigParams MetricsPort: w.FirstMetricsPort + i, OffledgerBroadcastUpToNPeers: 10, PruningMinStatesToKeep: 10000, + AuthScheme: "none", } } diff --git a/tools/cluster/templates/waspconfig.go b/tools/cluster/templates/waspconfig.go index 6de466792b..6c05368fb9 100644 --- a/tools/cluster/templates/waspconfig.go +++ b/tools/cluster/templates/waspconfig.go @@ -17,6 +17,7 @@ type WaspConfigParams struct { ValidatorKeyPair *cryptolib.KeyPair ValidatorAddress string // bech32 encoded address of ValidatorKeyPair PruningMinStatesToKeep int + AuthScheme string } var WaspConfig = ` @@ -115,7 +116,7 @@ var WaspConfig = ` "enabled": true, "bindAddress": "0.0.0.0:{{.APIPort}}", "auth": { - "scheme": "none", + "scheme": "{{.AuthScheme}}", "jwt": { "duration": "24h" } diff --git a/tools/cluster/tests/wasp-cli_test.go b/tools/cluster/tests/wasp-cli_test.go index 8050058133..cc0eb377cb 100644 --- a/tools/cluster/tests/wasp-cli_test.go +++ b/tools/cluster/tests/wasp-cli_test.go @@ -45,6 +45,21 @@ func TestWaspCLINoChains(t *testing.T) { require.Contains(t, out[0], "Total 0 chain(s)") } +func TestWaspAuth(t *testing.T) { + w := newWaspCLITest(t, waspClusterOpts{ + modifyConfig: func(nodeIndex int, configParams templates.WaspConfigParams) templates.WaspConfigParams { + configParams.AuthScheme = "jwt" + return configParams + }, + }) + _, err := w.Run("chain", "list", "--node=0", "--node=0") + require.Error(t, err) + out := w.MustRun("auth", "login", "--node=0", "-u=wasp", "-p=wasp") + require.Equal(t, "Successfully authenticated", out[1]) + out = w.MustRun("chain", "list", "--node=0", "--node=0") + require.Contains(t, out[0], "Total 0 chain(s)") +} + func TestWaspCLI1Chain(t *testing.T) { w := newWaspCLITest(t) From 6a2c34468b6ddcac79874491cfcf44ba44b8c396 Mon Sep 17 00:00:00 2001 From: Julius Andrikonis Date: Wed, 2 Aug 2023 15:30:16 +0300 Subject: [PATCH 03/26] While writing file to WAL, temp file is written and then renamed --- .../sm_gpa/sm_gpa_utils/block_wal.go | 53 +++++++++++++------ 1 file changed, 37 insertions(+), 16 deletions(-) diff --git a/packages/chain/statemanager/sm_gpa/sm_gpa_utils/block_wal.go b/packages/chain/statemanager/sm_gpa/sm_gpa_utils/block_wal.go index 2dca39e4a4..fb9987adf5 100644 --- a/packages/chain/statemanager/sm_gpa/sm_gpa_utils/block_wal.go +++ b/packages/chain/statemanager/sm_gpa/sm_gpa_utils/block_wal.go @@ -24,7 +24,10 @@ type blockWAL struct { metrics *metrics.ChainBlockWALMetrics } -const constBlockWALFileSuffix = ".blk" +const ( + constBlockWALFileSuffix = ".blk" + constBlockWALTmpFileSuffix = ".tmp" +) func NewBlockWAL(log *logger.Logger, baseDir string, chainID isc.ChainID, metrics *metrics.ChainBlockWALMetrics) (BlockWAL, error) { dir := filepath.Join(baseDir, chainID.String()) @@ -45,26 +48,40 @@ func NewBlockWAL(log *logger.Logger, baseDir string, chainID isc.ChainID, metric func (bwT *blockWAL) Write(block state.Block) error { blockIndex := block.StateIndex() commitment := block.L1Commitment() - fileName := blockWALFileName(commitment.BlockHash()) - filePath := filepath.Join(bwT.dir, fileName) - f, err := os.OpenFile(filePath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o666) + tmpFileName := blockWALTmpFileName(commitment.BlockHash()) + tmpFilePath := filepath.Join(bwT.dir, tmpFileName) + err := func() error { // Function is used to make defered close occur when it is needed even if write is successful + f, err := os.OpenFile(tmpFilePath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o666) + if err != nil { + bwT.metrics.IncFailedWrites() + return fmt.Errorf("failed to create temporary file %s for writing block index %v: %w", tmpFileName, blockIndex, err) + } + defer f.Close() + blockBytes := block.Bytes() + n, err := f.Write(blockBytes) + if err != nil { + bwT.metrics.IncFailedWrites() + return fmt.Errorf("writing block index %v data to temporary file %s failed: %w", blockIndex, tmpFileName, err) + } + if len(blockBytes) != n { + bwT.metrics.IncFailedWrites() + return fmt.Errorf("only %v of total %v bytes of block index %v were written to temporary file %s", n, len(blockBytes), blockIndex, tmpFileName) + } + return nil + }() if err != nil { - bwT.metrics.IncFailedWrites() - return fmt.Errorf("opening file %s for writing block index %v failed: %w", fileName, blockIndex, err) + return err } - defer f.Close() - blockBytes := block.Bytes() - n, err := f.Write(blockBytes) + finalFileName := blockWALFileName(commitment.BlockHash()) + finalFilePath := filepath.Join(bwT.dir, finalFileName) + err = os.Rename(tmpFilePath, finalFilePath) if err != nil { - bwT.metrics.IncFailedWrites() - return fmt.Errorf("writing block index %v data to file %s failed: %w", blockIndex, fileName, err) - } - if len(blockBytes) != n { - bwT.metrics.IncFailedWrites() - return fmt.Errorf("only %v of total %v bytes of block index %v were written to file %s", n, len(blockBytes), blockIndex, fileName) + return fmt.Errorf("failed to move temporary WAL file %s to permanent location %s: %v", + tmpFilePath, finalFilePath, err) } + bwT.metrics.BlockWritten(block.StateIndex()) - bwT.LogDebugf("Block index %v %s written to wal; file name - %s", blockIndex, commitment, fileName) + bwT.LogDebugf("Block index %v %s written to wal; file name - %s", blockIndex, commitment, finalFileName) return nil } @@ -163,3 +180,7 @@ func blockFromFilePath(filePath string) (state.Block, error) { func blockWALFileName(blockHash state.BlockHash) string { return blockHash.String() + constBlockWALFileSuffix } + +func blockWALTmpFileName(blockHash state.BlockHash) string { + return blockWALFileName(blockHash) + constBlockWALTmpFileSuffix +} From b4deca0c1803f837fb2e1d531bd677b8e1047509 Mon Sep 17 00:00:00 2001 From: Julius Andrikonis Date: Thu, 3 Aug 2023 12:16:26 +0300 Subject: [PATCH 04/26] WAL improvements --- .../sm_gpa/sm_gpa_utils/block_wal.go | 83 ++++++++++++++----- .../sm_gpa_utils/block_wal_rapid_test.go | 14 ++-- .../sm_gpa/sm_gpa_utils/block_wal_test.go | 42 ++++++++-- 3 files changed, 106 insertions(+), 33 deletions(-) diff --git a/packages/chain/statemanager/sm_gpa/sm_gpa_utils/block_wal.go b/packages/chain/statemanager/sm_gpa/sm_gpa_utils/block_wal.go index fb9987adf5..ca2b0f588b 100644 --- a/packages/chain/statemanager/sm_gpa/sm_gpa_utils/block_wal.go +++ b/packages/chain/statemanager/sm_gpa/sm_gpa_utils/block_wal.go @@ -2,6 +2,7 @@ package sm_gpa_utils import ( "bufio" + "encoding/hex" "fmt" "os" "path/filepath" @@ -48,8 +49,13 @@ func NewBlockWAL(log *logger.Logger, baseDir string, chainID isc.ChainID, metric func (bwT *blockWAL) Write(block state.Block) error { blockIndex := block.StateIndex() commitment := block.L1Commitment() + subfolderName := blockWALSubFolderName(commitment.BlockHash()) + folderPath := filepath.Join(bwT.dir, subfolderName) + if err := ioutils.CreateDirectory(folderPath, 0o777); err != nil { + return fmt.Errorf("failed create folder %v for writing block index %v: %w", folderPath, blockIndex, err) + } tmpFileName := blockWALTmpFileName(commitment.BlockHash()) - tmpFilePath := filepath.Join(bwT.dir, tmpFileName) + tmpFilePath := filepath.Join(folderPath, tmpFileName) err := func() error { // Function is used to make defered close occur when it is needed even if write is successful f, err := os.OpenFile(tmpFilePath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o666) if err != nil { @@ -73,7 +79,7 @@ func (bwT *blockWAL) Write(block state.Block) error { return err } finalFileName := blockWALFileName(commitment.BlockHash()) - finalFilePath := filepath.Join(bwT.dir, finalFileName) + finalFilePath := filepath.Join(folderPath, finalFileName) err = os.Rename(tmpFilePath, finalFilePath) if err != nil { return fmt.Errorf("failed to move temporary WAL file %s to permanent location %s: %v", @@ -85,14 +91,35 @@ func (bwT *blockWAL) Write(block state.Block) error { return nil } +func (bwT *blockWAL) containsWithPath(blockHash state.BlockHash) (bool, string) { + subfolderName := blockWALSubFolderName(blockHash) + fileName := blockWALFileName(blockHash) + + pathWithSubFolder := filepath.Join(bwT.dir, subfolderName, fileName) + _, err := os.Stat(pathWithSubFolder) + if err == nil { + return true, pathWithSubFolder + } + + // Checked for backward compatibility and for ease of adding some blocks from other sources + pathNoSubFolder := filepath.Join(bwT.dir, fileName) + _, err = os.Stat(pathNoSubFolder) + if err == nil { + return true, pathNoSubFolder + } + return false, "" +} + func (bwT *blockWAL) Contains(blockHash state.BlockHash) bool { - _, err := os.Stat(filepath.Join(bwT.dir, blockWALFileName(blockHash))) - return err == nil + result, _ := bwT.containsWithPath(blockHash) + return result } func (bwT *blockWAL) Read(blockHash state.BlockHash) (state.Block, error) { - fileName := blockWALFileName(blockHash) - filePath := filepath.Join(bwT.dir, fileName) + conains, filePath := bwT.containsWithPath(blockHash) + if !conains { + return nil, fmt.Errorf("block hash %s is not present in WAL", blockHash) + } block, err := blockFromFilePath(filePath) if err != nil { bwT.metrics.IncFailedReads() @@ -105,24 +132,16 @@ func (bwT *blockWAL) Read(blockHash state.BlockHash) (state.Block, error) { // The blocks are provided ordered by the state index, so that they can be applied to the store. // This function reads blocks twice, but tries to minimize the amount of memory required to load the WAL. func (bwT *blockWAL) ReadAllByStateIndex(cb func(stateIndex uint32, block state.Block) bool) error { - dirEntries, err := os.ReadDir(bwT.dir) - if err != nil { - return err - } blocksByStateIndex := map[uint32][]string{} - for _, dirEntry := range dirEntries { - if !dirEntry.Type().IsRegular() { - continue + checkFile := func(filePath string) { + if !strings.HasSuffix(filePath, constBlockWALFileSuffix) { + return } - if !strings.HasSuffix(dirEntry.Name(), constBlockWALFileSuffix) { - continue - } - filePath := filepath.Join(bwT.dir, dirEntry.Name()) fileBlock, fileErr := blockFromFilePath(filePath) if fileErr != nil { bwT.metrics.IncFailedReads() - bwT.LogWarn("Unable to read %v: %v", filePath, err) - continue + bwT.LogWarn("Unable to read %v: %v", filePath, fileErr) + return } stateIndex := fileBlock.StateIndex() stateIndexPaths, found := blocksByStateIndex[stateIndex] @@ -133,6 +152,28 @@ func (bwT *blockWAL) ReadAllByStateIndex(cb func(stateIndex uint32, block state. } blocksByStateIndex[stateIndex] = stateIndexPaths } + + var checkDir func(dirPath string, dirEntries []os.DirEntry) + checkDir = func(dirPath string, dirEntries []os.DirEntry) { + for _, dirEntry := range dirEntries { + entryPath := filepath.Join(dirPath, dirEntry.Name()) + if dirEntry.IsDir() { + subDirEntries, err := os.ReadDir(entryPath) + if err == nil { + checkDir(entryPath, subDirEntries) + } + } else { + checkFile(entryPath) + } + } + } + + dirEntries, err := os.ReadDir(bwT.dir) + if err != nil { + return err + } + checkDir(bwT.dir, dirEntries) + allStateIndexes := lo.Keys(blocksByStateIndex) sort.Slice(allStateIndexes, func(i, j int) bool { return allStateIndexes[i] < allStateIndexes[j] }) for _, stateIndex := range allStateIndexes { @@ -177,6 +218,10 @@ func blockFromFilePath(filePath string) (state.Block, error) { return block, nil } +func blockWALSubFolderName(blockHash state.BlockHash) string { + return hex.EncodeToString(blockHash[:1]) +} + func blockWALFileName(blockHash state.BlockHash) string { return blockHash.String() + constBlockWALFileSuffix } diff --git a/packages/chain/statemanager/sm_gpa/sm_gpa_utils/block_wal_rapid_test.go b/packages/chain/statemanager/sm_gpa/sm_gpa_utils/block_wal_rapid_test.go index be7c42cc8c..659a8e0174 100644 --- a/packages/chain/statemanager/sm_gpa/sm_gpa_utils/block_wal_rapid_test.go +++ b/packages/chain/statemanager/sm_gpa/sm_gpa_utils/block_wal_rapid_test.go @@ -3,7 +3,6 @@ package sm_gpa_utils import ( "crypto/rand" "os" - "path/filepath" "testing" "github.com/samber/lo" @@ -44,7 +43,7 @@ func newBlockWALTestSM(t *rapid.T) *blockWALTestSM { return bwtsmT } -func (bwtsmT *blockWALTestSM) Cleanup() { +func (bwtsmT *blockWALTestSM) cleanup() { bwtsmT.log.Sync() os.RemoveAll(constTestFolder) } @@ -107,8 +106,8 @@ func (bwtsmT *blockWALTestSM) MoveBlock(t *rapid.T) { if blockHashOrig.Equals(blockHashToDamage) { t.Skip() } - fileOrigPath := bwtsmT.pathFromHash(blockHashOrig) - fileToDamagePath := bwtsmT.pathFromHash(blockHashToDamage) + fileOrigPath := walPathFromHash(bwtsmT.factory.GetChainID(), blockHashOrig) + fileToDamagePath := walPathFromHash(bwtsmT.factory.GetChainID(), blockHashToDamage) data, err := os.ReadFile(fileOrigPath) require.NoError(t, err) err = os.WriteFile(fileToDamagePath, data, 0o644) @@ -124,7 +123,7 @@ func (bwtsmT *blockWALTestSM) DamageBlock(t *rapid.T) { t.Skip() } blockHash := rapid.SampledFrom(blockHashes).Example() - filePath := bwtsmT.pathFromHash(blockHash) + filePath := walPathFromHash(bwtsmT.factory.GetChainID(), blockHash) data := make([]byte, 50) _, err := rand.Read(data) require.NoError(t, err) @@ -188,10 +187,6 @@ func (bwtsmT *blockWALTestSM) getGoodBlockHashes() []state.BlockHash { return result } -func (bwtsmT *blockWALTestSM) pathFromHash(blockHash state.BlockHash) string { - return filepath.Join(constTestFolder, bwtsmT.factory.GetChainID().String(), blockWALFileName(blockHash)) -} - func (bwtsmT *blockWALTestSM) invariantAllWrittenBlocksExist(t *rapid.T) { for blockHash := range bwtsmT.blocks { require.True(t, bwtsmT.bw.Contains(blockHash)) @@ -202,6 +197,7 @@ func TestBlockWALPropBased(t *testing.T) { rapid.Check(t, func(t *rapid.T) { sm := newBlockWALTestSM(t) t.Repeat(rapid.StateMachineActions(sm)) + sm.cleanup() }) } diff --git a/packages/chain/statemanager/sm_gpa/sm_gpa_utils/block_wal_test.go b/packages/chain/statemanager/sm_gpa/sm_gpa_utils/block_wal_test.go index 81ea15a778..086576391c 100644 --- a/packages/chain/statemanager/sm_gpa/sm_gpa_utils/block_wal_test.go +++ b/packages/chain/statemanager/sm_gpa/sm_gpa_utils/block_wal_test.go @@ -49,8 +49,8 @@ func TestBlockWALBasic(t *testing.T) { require.Error(t, err) } -// Check if existing WAL record is overwritten -func TestBlockWALOverwrite(t *testing.T) { +// Check if existing block in WAL is found even if it is not in a subfolder +func TestBlockWALNoSubfolder(t *testing.T) { log := testlogger.NewLogger(t) defer log.Sync() defer cleanupAfterTest(t) @@ -63,11 +63,39 @@ func TestBlockWALOverwrite(t *testing.T) { err = wal.Write(blocks[i]) require.NoError(t, err) } - pathFromHashFun := func(blockHash state.BlockHash) string { + pathNoSubfolderFromHashFun := func(blockHash state.BlockHash) string { return filepath.Join(constTestFolder, factory.GetChainID().String(), blockWALFileName(blockHash)) } - file0Path := pathFromHashFun(blocks[0].Hash()) - file1Path := pathFromHashFun(blocks[1].Hash()) + for _, block := range blocks { + pathWithSubfolder := walPathFromHash(factory.GetChainID(), block.Hash()) + pathNoSubfolder := pathNoSubfolderFromHashFun(block.Hash()) + err = os.Rename(pathWithSubfolder, pathNoSubfolder) + require.NoError(t, err) + } + for _, block := range blocks { + require.True(t, wal.Contains(block.Hash())) + blockRead, err := wal.Read(block.Hash()) + require.NoError(t, err) + CheckBlocksEqual(t, block, blockRead) + } +} + +// Check if existing WAL record is overwritten +func TestBlockWALOverwrite(t *testing.T) { + log := testlogger.NewLogger(t) + defer log.Sync() + defer cleanupAfterTest(t) + + factory := NewBlockFactory(t) + blocks := factory.GetBlocks(4, 1) + wal, err := NewBlockWAL(log, constTestFolder, factory.GetChainID(), mockBlockWALMetrics()) + require.NoError(t, err) + for i := range blocks { + err = wal.Write(blocks[i]) + require.NoError(t, err) + } + file0Path := walPathFromHash(factory.GetChainID(), blocks[0].Hash()) + file1Path := walPathFromHash(factory.GetChainID(), blocks[1].Hash()) err = os.Rename(file1Path, file0Path) require.NoError(t, err) // block[1] is no longer in WAL @@ -116,6 +144,10 @@ func TestBlockWALRestart(t *testing.T) { } } +func walPathFromHash(chainID isc.ChainID, blockHash state.BlockHash) string { + return filepath.Join(constTestFolder, chainID.String(), blockWALSubFolderName(blockHash), blockWALFileName(blockHash)) +} + func cleanupAfterTest(t *testing.T) { err := os.RemoveAll(constTestFolder) require.NoError(t, err) From 8f77a71c676fff98afb122a4f6518fa145fb5f32 Mon Sep 17 00:00:00 2001 From: Eric Hop Date: Thu, 3 Aug 2023 16:21:32 +0200 Subject: [PATCH 05/26] Add delayed request test --- .../testwasmlib/go/testwasmlibimpl/funcs.go | 17 ++++++++ .../rs/testwasmlibimpl/src/funcs.rs | 17 ++++++++ contracts/wasm/testwasmlib/schema.yaml | 12 ++++++ .../test/testwasmlib_client_test.go | 40 ++++++++++++++++++ .../testwasmlib/ts/testwasmlibimpl/funcs.ts | 17 ++++++++ .../test/solotutorial_bg.wasm | Bin 33682 -> 33681 bytes .../sbtests/sbtestsc/testcore_bg.wasm | Bin 82863 -> 82861 bytes tools/cluster/tests/wasm/inccounter_bg.wasm | Bin 64491 -> 64496 bytes 8 files changed, 103 insertions(+) diff --git a/contracts/wasm/testwasmlib/go/testwasmlibimpl/funcs.go b/contracts/wasm/testwasmlib/go/testwasmlibimpl/funcs.go index 8c8130c99b..4d260a1aec 100644 --- a/contracts/wasm/testwasmlib/go/testwasmlibimpl/funcs.go +++ b/contracts/wasm/testwasmlib/go/testwasmlibimpl/funcs.go @@ -793,3 +793,20 @@ func viewCheckEthEmptyAddressAndAgentID(ctx wasmlib.ScViewContext, f *CheckEthEm func viewCheckEthInvalidEmptyAddressFromString(_ wasmlib.ScViewContext, _ *CheckEthInvalidEmptyAddressFromStringContext) { _ = wasmtypes.AddressFromString("0x00") } + +func funcActivate(ctx wasmlib.ScFuncContext, f *ActivateContext) { + f.State.Active().SetValue(true) + deposit := ctx.Allowance().BaseTokens() + transfer := wasmlib.ScTransferFromBaseTokens(deposit) + ctx.TransferAllowed(ctx.AccountID(), transfer) + delay := f.Params.Seconds().Value() + testwasmlib.ScFuncs.Deactivate(ctx).Func.Delay(delay).Post() +} + +func funcDeactivate(_ wasmlib.ScFuncContext, f *DeactivateContext) { + f.State.Active().SetValue(false) +} + +func viewGetActive(_ wasmlib.ScViewContext, f *GetActiveContext) { + f.Results.Active().SetValue(f.State.Active().Value()) +} diff --git a/contracts/wasm/testwasmlib/rs/testwasmlibimpl/src/funcs.rs b/contracts/wasm/testwasmlib/rs/testwasmlibimpl/src/funcs.rs index dc0abad283..9d99124df5 100644 --- a/contracts/wasm/testwasmlib/rs/testwasmlibimpl/src/funcs.rs +++ b/contracts/wasm/testwasmlib/rs/testwasmlibimpl/src/funcs.rs @@ -1351,3 +1351,20 @@ pub fn view_check_eth_invalid_empty_address_from_string( ) { address_from_string("0x00"); } + +pub fn func_activate(ctx: &ScFuncContext, f: &ActivateContext) { + f.state.active().set_value(true); + let deposit = ctx.allowance().base_tokens(); + let transfer = wasmlib::ScTransfer::base_tokens(deposit); + ctx.transfer_allowed(&ctx.account_id(), &transfer); + let delay = f.params.seconds().value(); + testwasmlib::ScFuncs::deactivate(ctx).func.delay(delay).post(); +} + +pub fn func_deactivate(ctx: &ScFuncContext, f: &DeactivateContext) { + f.state.active().set_value(false); +} + +pub fn view_get_active(ctx: &ScViewContext, f: &GetActiveContext) { + f.results.active().set_value(f.state.active().value()); +} diff --git a/contracts/wasm/testwasmlib/schema.yaml b/contracts/wasm/testwasmlib/schema.yaml index cd336484b4..683656e856 100644 --- a/contracts/wasm/testwasmlib/schema.yaml +++ b/contracts/wasm/testwasmlib/schema.yaml @@ -58,6 +58,7 @@ typedefs: # ################################## state: + active: Bool # basic datatypes, using String arrayOfStringArray: StringArray[] arrayOfStringMap: StringMap[] @@ -75,6 +76,13 @@ state: # ################################## funcs: + activate: + params: + seconds: Uint32 + + deactivate: + access: self # only SC itself can invoke this function + stringMapOfStringArrayAppend: params: name: String @@ -208,6 +216,10 @@ funcs: # ################################## views: + getActive: + results: + active: Bool + stringMapOfStringArrayLength: params: name: String diff --git a/contracts/wasm/testwasmlib/test/testwasmlib_client_test.go b/contracts/wasm/testwasmlib/test/testwasmlib_client_test.go index 55b4f8d1d0..1d401fa8ed 100644 --- a/contracts/wasm/testwasmlib/test/testwasmlib_client_test.go +++ b/contracts/wasm/testwasmlib/test/testwasmlib_client_test.go @@ -154,6 +154,46 @@ func newClient(t testing.TB, svcClient wasmclient.IClientService, wallet *crypto return ctx } +func TestTimedDeactivation(t *testing.T) { + if !useDisposable { + t.SkipNow() + } + + //ctxCluster := setupClient(t) + + ctx := setupClientLib(t) + require.NoError(t, ctx.Err) + + active := getActive(t, ctx) + require.False(t, active) + + f := testwasmlib.ScFuncs.Activate(ctx) + f.Params.Seconds().SetValue(20) + f.Func.TransferBaseTokens(2_000_000).AllowanceBaseTokens(1_000_000).Post() + require.NoError(t, ctx.Err) + + ctx.WaitRequest() + require.NoError(t, ctx.Err) + + for i := 0; i < 20; i++ { + active = getActive(t, ctx) + fmt.Printf("TICK #%d: %v\n", i, active) + if !active { + break + } + time.Sleep(5 * time.Second) + } + + //_ = ctxCluster +} + +func getActive(t *testing.T, ctx *wasmclient.WasmClientContext) bool { + a := testwasmlib.ScFuncs.GetActive(ctx) + a.Func.Call() + require.NoError(t, ctx.Err) + return a.Results.Active().Value() +} + func TestClientAccountBalance(t *testing.T) { ctx := setupClient(t) wallet := ctx.CurrentKeyPair() diff --git a/contracts/wasm/testwasmlib/ts/testwasmlibimpl/funcs.ts b/contracts/wasm/testwasmlib/ts/testwasmlibimpl/funcs.ts index d771610b18..4757f8babb 100644 --- a/contracts/wasm/testwasmlib/ts/testwasmlibimpl/funcs.ts +++ b/contracts/wasm/testwasmlib/ts/testwasmlibimpl/funcs.ts @@ -780,3 +780,20 @@ export function viewCheckEthEmptyAddressAndAgentID(ctx: wasmlib.ScViewContext, f export function viewCheckEthInvalidEmptyAddressFromString(ctx: wasmlib.ScViewContext, f: sc.CheckEthInvalidEmptyAddressFromStringContext): void { wasmtypes.addressFromString("0x00"); } + +export function funcActivate(ctx: wasmlib.ScFuncContext, f: sc.ActivateContext): void { + f.state.active().setValue(true); + const deposit = ctx.allowance().baseTokens(); + const transfer = wasmlib.ScTransfer.baseTokens(deposit); + ctx.transferAllowed(ctx.accountID(), transfer); + const delay = f.params.seconds().value(); + sc.ScFuncs.deactivate(ctx).func.delay(delay).post(); +} + +export function funcDeactivate(ctx: wasmlib.ScFuncContext, f: sc.DeactivateContext): void { + f.state.active().setValue(false); +} + +export function viewGetActive(ctx: wasmlib.ScViewContext, f: sc.GetActiveContext): void { + f.results.active().setValue(f.state.active().value()); +} diff --git a/documentation/tutorial-examples/test/solotutorial_bg.wasm b/documentation/tutorial-examples/test/solotutorial_bg.wasm index 2e33d13bee86a06aa438f8ca0f53574710449aa6..aeac80beda946bad81217bac81c83220d94cf35b 100644 GIT binary patch delta 779 zcmZ9IUr3Wt7{<@}=G@vJJDbh6HT5`kxy^J@jwrQ`h)@PAwWOqAwOu$|8U>NAR3yYA znI5Q!F2Wlx6HX997XyikZVI{%uOq07sLOgzZ1L5BAJ6lg^Pcy6&$qg0tSlPE??xiA z@?(Ep7bSu^JM%-~;ep7A!<~;hn@=29z4Tp>#-$@POylZe=%BL{o011FdAz@t(3$ZXaVRH>cj32{Z6NNjf7JVN@m?N2^`bHg`#VZWSx&R+ zKzq_4+$xHXN+fA;IEWvzPqmLW8i9~Lwumne^)Z8k*dZaAIro8vWb*vB@p4J}$DTpd z+p$d>m1^Pg8l`h8l1sVd`yx`10QPY=rbg05bTkkZ>LPxHhp@$%Fk(p=b%Z}%)LGnhN}$S46iHgKv%bbR-hlIa|G-K=9)I- bXn7Cv=QS}In_qW#=!m7?*Qq|uKc~L|MAe+0 delta 765 zcmZ9HPe{{Y7{}lDotr=1X0J2bpSJn>H|K0^P$SV1Z-h`DmYPVJ7dJ11Asuy=SqCc# z)E7#_Qv`PF@CFrh7}a4A9i&T_pj!!b5TrWxePfFsz3}1t{r;ZkeSXiozQQ(E*xCV0 zrPhC@{Ao%gbtj8s9pl42mm0mr3+Ib5cc;$K_aWM^8p7kWUr&dBxoJjE^iEUXk9%bz z1HvP%g1oZ|zp38EN}eG>r6_bb1mPJy(zoM`s@rA4L&6~#KP#Bzg)Sl-riTg2hcr*l z)wNWIQCT6?X`l4Q$(d%~*Sna^MUy;TkiU(uXxDJO(RyusT)tET7hlv?;r~{7s@W)a z1&^m05l*?bSte;F;2;4=LdPfDSTJJB4*C6|DRVf81&OHK=Zq-&U1>>KQ~= zre4+42l`g-F{Mv*_iWax4cLD4{{Vfz_|`$Adi2_v26ZqWS05K5)UEaxGW9e0d_Fml zQ@e{1HC9S2c?cnvlapEKInZLuBk)mM&cgd`c@o~*Il&T-flJufFKBgqadvIZe;D()YYZ!);3gX zdkOe<(4qF{6Ke40)#ZM!ab(`^BBqZ{-9^9qxXfnH$mi6Qq@zd`;!C9Y!2JKhr} N;+V%J_2=d1^bg9$os9qh diff --git a/packages/vm/core/testcore/sbtests/sbtestsc/testcore_bg.wasm b/packages/vm/core/testcore/sbtests/sbtestsc/testcore_bg.wasm index d7fea954b6567143ec6974fbe2fc7eff53adc7df..159c25ab05c5a74594e9cf3031b1946a9ac46284 100644 GIT binary patch delta 4437 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 LlB7l_!qWc&Yzifi delta 4470 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 Z9TA|n&(PETp_0auZyd=)B`^|_{|nN*IkEr% diff --git a/tools/cluster/tests/wasm/inccounter_bg.wasm b/tools/cluster/tests/wasm/inccounter_bg.wasm index ccfba28257530c71e55321eba09e72c767c2a999..7d88d2fe99fd2c0343066d38c0ab5d02edd4802a 100644 GIT binary patch delta 8620 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 Dr6ct% delta 8689 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 Date: Fri, 4 Aug 2023 09:55:54 +0100 Subject: [PATCH 06/26] feat: mempool nonce override --- packages/chain/mempool/mempool.go | 78 +++----- packages/chain/mempool/mempool_test.go | 68 +++++++ packages/chain/mempool/typed_pool_by_nonce.go | 173 ++++++++++++++++++ packages/vm/vmimpl/runreq.go | 2 +- 4 files changed, 267 insertions(+), 54 deletions(-) create mode 100644 packages/chain/mempool/typed_pool_by_nonce.go diff --git a/packages/chain/mempool/mempool.go b/packages/chain/mempool/mempool.go index 1ddbfb00aa..6a21ff25e4 100644 --- a/packages/chain/mempool/mempool.go +++ b/packages/chain/mempool/mempool.go @@ -48,7 +48,6 @@ import ( "time" "github.com/samber/lo" - "golang.org/x/exp/slices" "github.com/iotaledger/hive.go/logger" consGR "github.com/iotaledger/wasp/packages/chain/cons/cons_gr" @@ -129,7 +128,7 @@ type mempoolImpl struct { tangleTime time.Time timePool TimePool onLedgerPool RequestPool[isc.OnLedgerRequest] - offLedgerPool RequestPool[isc.OffLedgerRequest] + offLedgerPool *TypedPoolByNonce[isc.OffLedgerRequest] distSync gpa.GPA chainHeadAO *isc.AliasOutputWithID chainHeadState state.State @@ -214,7 +213,7 @@ func New( tangleTime: time.Time{}, timePool: NewTimePool(metrics.SetTimePoolSize, log.Named("TIM")), onLedgerPool: NewTypedPool[isc.OnLedgerRequest](waitReq, metrics.SetOnLedgerPoolSize, metrics.SetOnLedgerReqTime, log.Named("ONL")), - offLedgerPool: NewTypedPool[isc.OffLedgerRequest](waitReq, metrics.SetOffLedgerPoolSize, metrics.SetOffLedgerReqTime, log.Named("OFF")), + offLedgerPool: NewTypedPoolByNonce[isc.OffLedgerRequest](waitReq, metrics.SetOffLedgerPoolSize, metrics.SetOffLedgerReqTime, log.Named("OFF")), chainHeadAO: nil, serverNodesUpdatedPipe: pipe.NewInfinitePipe[*reqServerNodesUpdated](), serverNodes: []*cryptolib.PublicKey{}, @@ -480,11 +479,11 @@ func (mpi *mempoolImpl) shouldAddOffledgerRequest(req isc.OffLedgerRequest) erro return fmt.Errorf("bad nonce, expected: %d", accountNonce) } - governanceState := governance.NewStateAccess(mpi.chainHeadState) // check user has on-chain balance accountsState := accounts.NewStateAccess(mpi.chainHeadState) if !accountsState.AccountExists(req.SenderAccount()) { // make an exception for gov calls (sender is chan owner and target is gov contract) + governanceState := governance.NewStateAccess(mpi.chainHeadState) chainOwner := governanceState.ChainOwnerID() isGovRequest := req.SenderAccount().Equals(chainOwner) && req.CallTarget().Contract == governance.Contract.Hname() if !isGovRequest { @@ -530,17 +529,12 @@ func (mpi *mempoolImpl) handleConsensusProposal(recv *reqConsensusProposal) { mpi.handleConsensusProposalForChainHead(recv) } -type reqRefNonce struct { - ref *isc.RequestRef - nonce uint64 -} - func (mpi *mempoolImpl) refsToPropose() []*isc.RequestRef { // // The case for matching ChainHeadAO and request BaseAO reqRefs := []*isc.RequestRef{} if !mpi.tangleTime.IsZero() { // Wait for tangle-time to process the on ledger requests. - mpi.onLedgerPool.Filter(func(request isc.OnLedgerRequest, ts time.Time) bool { + mpi.onLedgerPool.Filter(func(request isc.OnLedgerRequest, _ time.Time) bool { if isc.RequestIsExpired(request, mpi.tangleTime) { return false // Drop it from the mempool } @@ -551,53 +545,31 @@ func (mpi *mempoolImpl) refsToPropose() []*isc.RequestRef { }) } - expectedAccountNonces := map[string]uint64{} // string is isc.AgentID.String() - requestsNonces := map[string][]reqRefNonce{} // string is isc.AgentID.String() - - mpi.offLedgerPool.Filter(func(request isc.OffLedgerRequest, ts time.Time) bool { - ref := isc.RequestRefFromRequest(request) - reqRefs = append(reqRefs, ref) - - // collect the nonces for each account - senderKey := request.SenderAccount().String() - _, ok := expectedAccountNonces[senderKey] - if !ok { - // get the current state nonce so we can detect gaps with it - expectedAccountNonces[senderKey] = mpi.nonce(request.SenderAccount()) + mpi.offLedgerPool.Iterate(func(account string, entries []*OrderedPoolEntry[isc.OffLedgerRequest]) { + agentID, err := isc.AgentIDFromString(account) + if err != nil { + panic(fmt.Errorf("invalid agentID string: %s", err.Error())) } - requestsNonces[senderKey] = append(requestsNonces[senderKey], reqRefNonce{ref: ref, nonce: request.Nonce()}) - - return true // Keep them for now - }) - - // remove any gaps in the nonces of each account - { - doNotPropose := []*isc.RequestRef{} - for account, refNonces := range requestsNonces { - // sort by nonce - slices.SortFunc(refNonces, func(a, b reqRefNonce) bool { - return a.nonce < b.nonce - }) - // check for gaps with the state nonce - if expectedAccountNonces[account] != refNonces[0].nonce { - // if the first one doesn't match the nonce required from the state, don't propose any of the following - for _, ref := range refNonces { - doNotPropose = append(doNotPropose, ref.ref) - } - continue + accountNonce := mpi.nonce(agentID) + for _, e := range entries { + reqNonce := e.req.Nonce() + if reqNonce < accountNonce { + // nonce too old, delete + mpi.log.Debugf("refsToPropose, account: %s, removing old nonce from pool: %d", account, e.req.Nonce()) + mpi.offLedgerPool.Remove(e.req) + } + if reqNonce == accountNonce { + // expected nonce, add it to the list to propose + mpi.log.Debugf("refsToPropose, account: %s, proposing reqID %s with nonce %d: d", account, e.req.ID().String(), e.req.Nonce()) + reqRefs = append(reqRefs, isc.RequestRefFromRequest(e.req)) + accountNonce++ // increment the account nonce to match the next valid request } - // check for gaps within the request list - for i := 1; i < len(refNonces); i++ { - if refNonces[i].nonce != refNonces[i-1].nonce+1 { - doNotPropose = append(doNotPropose, refNonces[i].ref) - } + if reqNonce > accountNonce { + mpi.log.Debugf("refsToPropose, account: %s, req %s has a nouce %d which is too high, won't be proposed", account, e.req.ID().String(), e.req.Nonce()) + return // no more valid nonces for this account, continue to the next account } } - // remove undesirable requests from the proposal - reqRefs = lo.Filter(reqRefs, func(x *isc.RequestRef, _ int) bool { - return !slices.Contains(doNotPropose, x) - }) - } + }) return reqRefs } diff --git a/packages/chain/mempool/mempool_test.go b/packages/chain/mempool/mempool_test.go index c39f2bb82c..9d2fe4586e 100644 --- a/packages/chain/mempool/mempool_test.go +++ b/packages/chain/mempool/mempool_test.go @@ -603,6 +603,74 @@ func TestMempoolsNonceGaps(t *testing.T) { // nonce 10 was never proposed } +func TestMempoolOverrideNonce(t *testing.T) { + // 1 node setup + // send nonce 0 + // send another request with the same nonce 0 + // assert the last request is proposed + te := newEnv(t, 1, 0, true) + defer te.close() + + tangleTime := time.Now() + for _, node := range te.mempools { + node.ServerNodesUpdated(te.peerPubKeys, te.peerPubKeys) + node.TangleTimeUpdated(tangleTime) + } + awaitTrackHeadChannels := make([]<-chan bool, len(te.mempools)) + // deposit some funds so off-ledger requests can go through + t.Log("TrackNewChainHead") + for i, node := range te.mempools { + awaitTrackHeadChannels[i] = node.TrackNewChainHead(te.stateForAO(i, te.originAO), nil, te.originAO, []state.Block{}, []state.Block{}) + } + for i := range te.mempools { + <-awaitTrackHeadChannels[i] + } + + output := transaction.BasicOutputFromPostData( + te.governor.Address(), + isc.HnameNil, + isc.RequestParameters{ + TargetAddress: te.chainID.AsAddress(), + Assets: isc.NewAssetsBaseTokens(10 * isc.Million), + }, + ) + onLedgerReq, err := isc.OnLedgerFromUTXO(output, tpkg.RandOutputID(uint16(0))) + require.NoError(t, err) + for _, node := range te.mempools { + node.ReceiveOnLedgerRequest(onLedgerReq) + } + currentAO := blockFn(te, []isc.Request{onLedgerReq}, te.originAO, tangleTime) + + initialReq := isc.NewOffLedgerRequest( + isc.RandomChainID(), + isc.Hn("foo"), + isc.Hn("bar"), + dict.New(), + 0, + gas.LimitsDefault.MaxGasPerRequest, + ).Sign(te.governor) + + require.NoError(t, te.mempools[0].ReceiveOffLedgerRequest(initialReq)) + time.Sleep(200 * time.Millisecond) // give some time for the requests to reach the pool + + overwritingReq := isc.NewOffLedgerRequest( + isc.RandomChainID(), + isc.Hn("baz"), + isc.Hn("bar"), + dict.New(), + 0, + gas.LimitsDefault.MaxGasPerRequest, + ).Sign(te.governor) + + require.NoError(t, te.mempools[0].ReceiveOffLedgerRequest(overwritingReq)) + time.Sleep(200 * time.Millisecond) // give some time for the requests to reach the pool + reqRefs := <-te.mempools[0].ConsensusProposalAsync(te.ctx, currentAO) + proposedReqs := <-te.mempools[0].ConsensusRequestsAsync(te.ctx, reqRefs) + require.Len(t, proposedReqs, 1) + require.Equal(t, overwritingReq, proposedReqs[0]) + require.NotEqual(t, initialReq, proposedReqs[0]) +} + //////////////////////////////////////////////////////////////////////////////// // testEnv diff --git a/packages/chain/mempool/typed_pool_by_nonce.go b/packages/chain/mempool/typed_pool_by_nonce.go new file mode 100644 index 0000000000..bfe4153b0c --- /dev/null +++ b/packages/chain/mempool/typed_pool_by_nonce.go @@ -0,0 +1,173 @@ +// Copyright 2020 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +package mempool + +import ( + "fmt" + "time" + + "golang.org/x/exp/slices" + + "github.com/iotaledger/hive.go/ds/shrinkingmap" + "github.com/iotaledger/hive.go/logger" + "github.com/iotaledger/wasp/packages/isc" +) + +// keeps a map of requests ordered by nonce for each account +type TypedPoolByNonce[V isc.OffLedgerRequest] struct { + waitReq WaitReq + refLUT *shrinkingmap.ShrinkingMap[isc.RequestRefKey, *OrderedPoolEntry[V]] + // reqsByAcountOrdered keeps an ordered map of reqsByAcountOrdered for each account by nonce + reqsByAcountOrdered *shrinkingmap.ShrinkingMap[string, []*OrderedPoolEntry[V]] // string is isc.AgentID.String() + sizeMetric func(int) + timeMetric func(time.Duration) + log *logger.Logger +} + +var _ RequestPool[isc.OffLedgerRequest] = &TypedPoolByNonce[isc.OffLedgerRequest]{} + +func NewTypedPoolByNonce[V isc.OffLedgerRequest](waitReq WaitReq, sizeMetric func(int), timeMetric func(time.Duration), log *logger.Logger) *TypedPoolByNonce[V] { + return &TypedPoolByNonce[V]{ + waitReq: waitReq, + reqsByAcountOrdered: shrinkingmap.New[string, []*OrderedPoolEntry[V]](), + refLUT: shrinkingmap.New[isc.RequestRefKey, *OrderedPoolEntry[V]](), + sizeMetric: sizeMetric, + timeMetric: timeMetric, + log: log, + } +} + +type OrderedPoolEntry[V isc.OffLedgerRequest] struct { + req V + ts time.Time +} + +func (p *TypedPoolByNonce[V]) Has(reqRef *isc.RequestRef) bool { + return p.refLUT.Has(reqRef.AsKey()) +} + +func (p *TypedPoolByNonce[V]) Get(reqRef *isc.RequestRef) V { + entry, exists := p.refLUT.Get(reqRef.AsKey()) + if !exists { + return *new(V) + } + return entry.req +} + +func (p *TypedPoolByNonce[V]) Add(request V) { + ref := isc.RequestRefFromRequest(request) + entry := &OrderedPoolEntry[V]{req: request, ts: time.Now()} + account := request.SenderAccount().String() + + if !p.refLUT.Set(ref.AsKey(), entry) { + p.log.Debugf("NOT ADDED, already exists. reqID: %v as key=%v, senderAccount: ", request.ID(), ref, account) + return // not added already exists + } + + defer func() { + p.log.Debugf("ADD %v as key=%v, senderAccount: ", request.ID(), ref, account) + p.sizeMetric(p.refLUT.Size()) + p.waitReq.MarkAvailable(request) + }() + + reqsForAcount, exists := p.reqsByAcountOrdered.Get(account) + if !exists { + // no other requests for this account + p.reqsByAcountOrdered.Set(account, []*OrderedPoolEntry[V]{entry}) + return + } + + // add to the account requests, keep the slice ordered + + // find the index where the new entry should be added + index, exists := slices.BinarySearchFunc(reqsForAcount, entry, + func(a, b *OrderedPoolEntry[V]) int { + aNonce := a.req.Nonce() + bNonce := b.req.Nonce() + if aNonce == bNonce { + return 0 + } + if aNonce > bNonce { + return 1 + } + return -1 + }, + ) + if exists { + // same nonce, delete old request with overlapping nonce, replace with the new one + p.Remove(reqsForAcount[index].req) + // refresh `reqsForAcount` after removing the old req + reqsForAcount, exists = p.reqsByAcountOrdered.Get(account) + if !exists { + reqsForAcount = make([]*OrderedPoolEntry[V], 0) + } + // TODO could this create some race condition if there is ongoing voting on a batch propostal containing the old nonce? + } + + reqsForAcount = append(reqsForAcount, entry) // add to the end of the list (thus extending the array) + + // make room if target position is not at the end + if index != len(reqsForAcount)+1 { + copy(reqsForAcount[index+1:], reqsForAcount[index:]) + reqsForAcount[index] = entry + } + p.reqsByAcountOrdered.Set(account, reqsForAcount) +} + +func (p *TypedPoolByNonce[V]) Remove(request V) { + refKey := isc.RequestRefFromRequest(request).AsKey() + entry, exists := p.refLUT.Get(refKey) + if !exists { + return // does not exist + } + defer func() { + p.sizeMetric(p.refLUT.Size()) + p.timeMetric(time.Since(entry.ts)) + }() + if p.refLUT.Delete(refKey) { + p.log.Debugf("DEL %v as key=%v", request.ID(), refKey) + } + account := entry.req.SenderAccount().String() + reqsByAccount, exists := p.reqsByAcountOrdered.Get(account) + if !exists { + p.log.Error("inconsistency trying to DEL %v as key=%v, no request list for account %s", request.ID(), refKey, account) + return + } + // find the request in the accounts map + indexToDel := slices.IndexFunc(reqsByAccount, func(e *OrderedPoolEntry[V]) bool { + return true + }) + if indexToDel == -1 { + p.log.Error("inconsistency trying to DEL %v as key=%v, request not found in list for account %s", request.ID(), refKey, account) + return + } + if len(reqsByAccount) == 1 { // just remove the entire array for the account + p.reqsByAcountOrdered.Delete(account) + return + } + reqsByAccount[indexToDel] = nil // remove the pointer reference to allow GC of the entry object + reqsByAccount = slices.Delete(reqsByAccount, indexToDel, indexToDel+1) + p.reqsByAcountOrdered.Set(account, reqsByAccount) +} + +func (p *TypedPoolByNonce[V]) Iterate(predicate func(account string, requests []*OrderedPoolEntry[V])) { + p.reqsByAcountOrdered.ForEach(func(acc string, reqs []*OrderedPoolEntry[V]) bool { + predicate(acc, reqs) + return true + }) +} + +func (p *TypedPoolByNonce[V]) Filter(predicate func(request V, ts time.Time) bool) { + p.refLUT.ForEach(func(refKey isc.RequestRefKey, entry *OrderedPoolEntry[V]) bool { + if !predicate(entry.req, entry.ts) { + p.Remove(entry.req) + } + return true + }) + p.sizeMetric(p.refLUT.Size()) +} + +func (p *TypedPoolByNonce[V]) StatusString() string { + return fmt.Sprintf("{|req|=%d}", p.refLUT.Size()) +} diff --git a/packages/vm/vmimpl/runreq.go b/packages/vm/vmimpl/runreq.go index 890aad73f6..09b8d97f91 100644 --- a/packages/vm/vmimpl/runreq.go +++ b/packages/vm/vmimpl/runreq.go @@ -28,7 +28,7 @@ import ( "github.com/iotaledger/wasp/packages/vm/vmexceptions" ) -// runRequest processes a single isc.Request in the batch +// runRequest processes a single isc.Request in the batch, returning an error means the request will be skipped func (vmctx *vmContext) runRequest(req isc.Request, requestIndex uint16, maintenanceMode bool) ( res *vm.RequestResult, unprocessableToRetry []isc.OnLedgerRequest, From 95120c51ace9d353d91a19c599714f7f9a01295f Mon Sep 17 00:00:00 2001 From: Jorge Silva Date: Mon, 7 Aug 2023 10:35:08 +0100 Subject: [PATCH 07/26] minor fixes --- packages/chain/mempool/mempool.go | 2 +- packages/chain/mempool/typed_pool_by_nonce.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/chain/mempool/mempool.go b/packages/chain/mempool/mempool.go index 6a21ff25e4..f71bb47846 100644 --- a/packages/chain/mempool/mempool.go +++ b/packages/chain/mempool/mempool.go @@ -565,7 +565,7 @@ func (mpi *mempoolImpl) refsToPropose() []*isc.RequestRef { accountNonce++ // increment the account nonce to match the next valid request } if reqNonce > accountNonce { - mpi.log.Debugf("refsToPropose, account: %s, req %s has a nouce %d which is too high, won't be proposed", account, e.req.ID().String(), e.req.Nonce()) + mpi.log.Debugf("refsToPropose, account: %s, req %s has a nonce %d which is too high, won't be proposed", account, e.req.ID().String(), e.req.Nonce()) return // no more valid nonces for this account, continue to the next account } } diff --git a/packages/chain/mempool/typed_pool_by_nonce.go b/packages/chain/mempool/typed_pool_by_nonce.go index bfe4153b0c..606ac49f3e 100644 --- a/packages/chain/mempool/typed_pool_by_nonce.go +++ b/packages/chain/mempool/typed_pool_by_nonce.go @@ -151,9 +151,9 @@ func (p *TypedPoolByNonce[V]) Remove(request V) { p.reqsByAcountOrdered.Set(account, reqsByAccount) } -func (p *TypedPoolByNonce[V]) Iterate(predicate func(account string, requests []*OrderedPoolEntry[V])) { +func (p *TypedPoolByNonce[V]) Iterate(f func(account string, requests []*OrderedPoolEntry[V])) { p.reqsByAcountOrdered.ForEach(func(acc string, reqs []*OrderedPoolEntry[V]) bool { - predicate(acc, reqs) + f(acc, reqs) return true }) } From eb463cea8f6c59d124143d8d4ba6589da35eef52 Mon Sep 17 00:00:00 2001 From: Jorge Silva Date: Mon, 7 Aug 2023 13:02:18 +0100 Subject: [PATCH 08/26] fix: don't remove overwritten requests from the pool --- packages/chain/mempool/mempool.go | 5 +++++ packages/chain/mempool/typed_pool_by_nonce.go | 12 ++++-------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/packages/chain/mempool/mempool.go b/packages/chain/mempool/mempool.go index f71bb47846..104193a817 100644 --- a/packages/chain/mempool/mempool.go +++ b/packages/chain/mempool/mempool.go @@ -558,6 +558,11 @@ func (mpi *mempoolImpl) refsToPropose() []*isc.RequestRef { mpi.log.Debugf("refsToPropose, account: %s, removing old nonce from pool: %d", account, e.req.Nonce()) mpi.offLedgerPool.Remove(e.req) } + if e.old { + // this request was marked as "old", do not propose it + mpi.log.Debugf("refsToPropose, account: %s, skipping old request: %s", account, e.req.ID().String()) + continue + } if reqNonce == accountNonce { // expected nonce, add it to the list to propose mpi.log.Debugf("refsToPropose, account: %s, proposing reqID %s with nonce %d: d", account, e.req.ID().String(), e.req.Nonce()) diff --git a/packages/chain/mempool/typed_pool_by_nonce.go b/packages/chain/mempool/typed_pool_by_nonce.go index 606ac49f3e..ae06f896e2 100644 --- a/packages/chain/mempool/typed_pool_by_nonce.go +++ b/packages/chain/mempool/typed_pool_by_nonce.go @@ -40,6 +40,7 @@ func NewTypedPoolByNonce[V isc.OffLedgerRequest](waitReq WaitReq, sizeMetric fun type OrderedPoolEntry[V isc.OffLedgerRequest] struct { req V + old bool ts time.Time } @@ -95,14 +96,9 @@ func (p *TypedPoolByNonce[V]) Add(request V) { }, ) if exists { - // same nonce, delete old request with overlapping nonce, replace with the new one - p.Remove(reqsForAcount[index].req) - // refresh `reqsForAcount` after removing the old req - reqsForAcount, exists = p.reqsByAcountOrdered.Get(account) - if !exists { - reqsForAcount = make([]*OrderedPoolEntry[V], 0) - } - // TODO could this create some race condition if there is ongoing voting on a batch propostal containing the old nonce? + // same nonce, mark the existing request with overlapping nonce as "old", place the new one + // NOTE: do not delete the request here, as it might already be part of an on-going consensus round + reqsForAcount[index].old = true } reqsForAcount = append(reqsForAcount, entry) // add to the end of the list (thus extending the array) From ab5baeb6d35e854106bbc52bcf908a19c66d154a Mon Sep 17 00:00:00 2001 From: Julius Andrikonis Date: Mon, 7 Aug 2023 15:59:22 +0300 Subject: [PATCH 09/26] Fixes after review --- .../sm_gpa/sm_gpa_utils/block_wal.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/chain/statemanager/sm_gpa/sm_gpa_utils/block_wal.go b/packages/chain/statemanager/sm_gpa/sm_gpa_utils/block_wal.go index ca2b0f588b..c87a55dc37 100644 --- a/packages/chain/statemanager/sm_gpa/sm_gpa_utils/block_wal.go +++ b/packages/chain/statemanager/sm_gpa/sm_gpa_utils/block_wal.go @@ -91,33 +91,33 @@ func (bwT *blockWAL) Write(block state.Block) error { return nil } -func (bwT *blockWAL) containsWithPath(blockHash state.BlockHash) (bool, string) { +func (bwT *blockWAL) blockFilepath(blockHash state.BlockHash) (string, bool) { subfolderName := blockWALSubFolderName(blockHash) fileName := blockWALFileName(blockHash) pathWithSubFolder := filepath.Join(bwT.dir, subfolderName, fileName) _, err := os.Stat(pathWithSubFolder) if err == nil { - return true, pathWithSubFolder + return pathWithSubFolder, true } // Checked for backward compatibility and for ease of adding some blocks from other sources pathNoSubFolder := filepath.Join(bwT.dir, fileName) _, err = os.Stat(pathNoSubFolder) if err == nil { - return true, pathNoSubFolder + return pathNoSubFolder, true } - return false, "" + return "", false } func (bwT *blockWAL) Contains(blockHash state.BlockHash) bool { - result, _ := bwT.containsWithPath(blockHash) - return result + _, exists := bwT.blockFilepath(blockHash) + return exists } func (bwT *blockWAL) Read(blockHash state.BlockHash) (state.Block, error) { - conains, filePath := bwT.containsWithPath(blockHash) - if !conains { + filePath, exists := bwT.blockFilepath(blockHash) + if !exists { return nil, fmt.Errorf("block hash %s is not present in WAL", blockHash) } block, err := blockFromFilePath(filePath) From a04050e9db6e80b51b4a2e504e8db0b64392bfe0 Mon Sep 17 00:00:00 2001 From: Jorge Silva Date: Tue, 8 Aug 2023 10:47:36 +0100 Subject: [PATCH 10/26] fix: rotate-with-dkg maintenance offledger by default --- tools/cluster/tests/wasp-cli_rotation_test.go | 2 +- tools/wasp-cli/chain/rotate.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/cluster/tests/wasp-cli_rotation_test.go b/tools/cluster/tests/wasp-cli_rotation_test.go index 486b976fc6..716b9ea4a4 100644 --- a/tools/cluster/tests/wasp-cli_rotation_test.go +++ b/tools/cluster/tests/wasp-cli_rotation_test.go @@ -207,6 +207,6 @@ func TestRotateOnOrigin(t *testing.T) { w.MustRun("chain", "rotate-with-dkg", "--node=1", "--peers=2,3", "--skip-maintenance") // NOTE: must skip "start/stop maintenance" because node1 isn't part of the committee w.MustRun("chain", "deposit", "base:10000000", "--node=1") // deposit works // assert `rotate-with-dkg` works with maintenance (when the node is part of the initial/final committee) - w.MustRun("chain", "rotate-with-dkg", "--node=1") // NOTE: must skip "start/stop maintenance" because node1 isn't part of the committee + w.MustRun("chain", "rotate-with-dkg", "--node=1") w.MustRun("chain", "deposit", "base:10000000", "--node=1") // deposit works } diff --git a/tools/wasp-cli/chain/rotate.go b/tools/wasp-cli/chain/rotate.go index 8c8948cf02..513650c582 100644 --- a/tools/wasp-cli/chain/rotate.go +++ b/tools/wasp-cli/chain/rotate.go @@ -75,7 +75,7 @@ func initRotateWithDKGCmd() *cobra.Command { withChainFlag(cmd, &chain) cmd.Flags().IntVarP(&quorum, "quorum", "", 0, "quorum (default: 3/4s of the number of committee nodes)") cmd.Flags().BoolVar(&skipMaintenance, "skip-maintenance", false, "quorum (default: 3/4s of the number of committee nodes)") - cmd.Flags().BoolVarP(&offLedger, "off-ledger", "o", false, + cmd.Flags().BoolVarP(&offLedger, "off-ledger", "o", true, "post an off-ledger request", ) From 19693ca704f45aed0db5ca2eafb83ffc73aa882d Mon Sep 17 00:00:00 2001 From: Eric Hop Date: Tue, 8 Aug 2023 12:06:49 +0200 Subject: [PATCH 11/26] Fix rwutil problems --- .../test/testwasmlib_client_test.go | 23 +++++++++++++------ packages/state/block.go | 3 ++- packages/util/rwutil/convert.go | 2 +- 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/contracts/wasm/testwasmlib/test/testwasmlib_client_test.go b/contracts/wasm/testwasmlib/test/testwasmlib_client_test.go index 1d401fa8ed..dbcabde756 100644 --- a/contracts/wasm/testwasmlib/test/testwasmlib_client_test.go +++ b/contracts/wasm/testwasmlib/test/testwasmlib_client_test.go @@ -155,11 +155,14 @@ func newClient(t testing.TB, svcClient wasmclient.IClientService, wallet *crypto } func TestTimedDeactivation(t *testing.T) { - if !useDisposable { + if !useDisposable && !useCluster { t.SkipNow() } - //ctxCluster := setupClient(t) + var ctxCluster *wasmclient.WasmClientContext + if useCluster { + ctxCluster = setupClient(t) + } ctx := setupClientLib(t) require.NoError(t, ctx.Err) @@ -168,23 +171,29 @@ func TestTimedDeactivation(t *testing.T) { require.False(t, active) f := testwasmlib.ScFuncs.Activate(ctx) - f.Params.Seconds().SetValue(20) + f.Params.Seconds().SetValue(420) f.Func.TransferBaseTokens(2_000_000).AllowanceBaseTokens(1_000_000).Post() require.NoError(t, ctx.Err) ctx.WaitRequest() require.NoError(t, ctx.Err) - for i := 0; i < 20; i++ { + for i := 0; i < 100; i++ { active = getActive(t, ctx) - fmt.Printf("TICK #%d: %v\n", i, active) + seconds := 20 + fmt.Printf("TICK #%d: %v\n", i*seconds, active) if !active { break } - time.Sleep(5 * time.Second) + factor := time.Duration(seconds) + if useCluster { + // time marches 10x faster + factor /= 10 + } + time.Sleep(factor * time.Second) } - //_ = ctxCluster + _ = ctxCluster } func getActive(t *testing.T, ctx *wasmclient.WasmClientContext) bool { diff --git a/packages/state/block.go b/packages/state/block.go index dd16dd2c5f..486773dd70 100644 --- a/packages/state/block.go +++ b/packages/state/block.go @@ -111,12 +111,13 @@ func (b *block) readEssence(r io.Reader) (int, error) { func (b *block) writeEssence(w io.Writer) (int, error) { ww := rwutil.NewWriter(w) + counter := rwutil.NewWriteCounter(ww) ww.Write(b.mutations) ww.WriteBool(b.previousL1Commitment != nil) if b.previousL1Commitment != nil { ww.Write(b.previousL1Commitment) } - return len(ww.Bytes()), ww.Err + return counter.Count(), ww.Err } // test only function diff --git a/packages/util/rwutil/convert.go b/packages/util/rwutil/convert.go index 69fc202895..18c45117e4 100644 --- a/packages/util/rwutil/convert.go +++ b/packages/util/rwutil/convert.go @@ -29,7 +29,7 @@ type ( //////////////////// basic size-checked read/write \\\\\\\\\\\\\\\\\\\\ func ReadN(r io.Reader, data []byte) error { - n, err := r.Read(data) + n, err := io.ReadFull(r, data) if err != nil { return err } From 1d913b94a02bd41abbe730c7dafc622cc5233308 Mon Sep 17 00:00:00 2001 From: Julius Andrikonis Date: Tue, 8 Aug 2023 14:02:49 +0300 Subject: [PATCH 12/26] Block index is written into start of block WAL entry --- .../sm_gpa/sm_gpa_utils/block_cache.go | 2 +- .../sm_gpa/sm_gpa_utils/block_wal.go | 66 +++++++++++++++---- packages/chain/statemanager/state_manager.go | 7 +- 3 files changed, 60 insertions(+), 15 deletions(-) diff --git a/packages/chain/statemanager/sm_gpa/sm_gpa_utils/block_cache.go b/packages/chain/statemanager/sm_gpa/sm_gpa_utils/block_cache.go index 25c214875f..fabe0398d7 100644 --- a/packages/chain/statemanager/sm_gpa/sm_gpa_utils/block_cache.go +++ b/packages/chain/statemanager/sm_gpa/sm_gpa_utils/block_cache.go @@ -89,7 +89,7 @@ func (bcT *blockCache) GetBlock(commitment *state.L1Commitment) state.Block { if bcT.wal.Contains(commitment.BlockHash()) { block, err := bcT.wal.Read(commitment.BlockHash()) if err != nil { - bcT.log.Errorf("Error reading block %s from WAL: %w", commitment, err) + bcT.log.Errorf("Error reading block index %v %s from WAL: %w", block.StateIndex(), commitment, err) return nil } bcT.addBlockToCache(block) diff --git a/packages/chain/statemanager/sm_gpa/sm_gpa_utils/block_wal.go b/packages/chain/statemanager/sm_gpa/sm_gpa_utils/block_wal.go index c87a55dc37..6e8dd92e92 100644 --- a/packages/chain/statemanager/sm_gpa/sm_gpa_utils/block_wal.go +++ b/packages/chain/statemanager/sm_gpa/sm_gpa_utils/block_wal.go @@ -1,9 +1,10 @@ package sm_gpa_utils import ( - "bufio" + //"bufio" "encoding/hex" "fmt" + "io" "os" "path/filepath" "sort" @@ -16,6 +17,7 @@ import ( "github.com/iotaledger/wasp/packages/isc" "github.com/iotaledger/wasp/packages/metrics" "github.com/iotaledger/wasp/packages/state" + "github.com/iotaledger/wasp/packages/util/rwutil" ) type blockWAL struct { @@ -52,7 +54,7 @@ func (bwT *blockWAL) Write(block state.Block) error { subfolderName := blockWALSubFolderName(commitment.BlockHash()) folderPath := filepath.Join(bwT.dir, subfolderName) if err := ioutils.CreateDirectory(folderPath, 0o777); err != nil { - return fmt.Errorf("failed create folder %v for writing block index %v: %w", folderPath, blockIndex, err) + return fmt.Errorf("failed to create folder %s for writing block: %w", folderPath, err) } tmpFileName := blockWALTmpFileName(commitment.BlockHash()) tmpFilePath := filepath.Join(folderPath, tmpFileName) @@ -60,18 +62,19 @@ func (bwT *blockWAL) Write(block state.Block) error { f, err := os.OpenFile(tmpFilePath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o666) if err != nil { bwT.metrics.IncFailedWrites() - return fmt.Errorf("failed to create temporary file %s for writing block index %v: %w", tmpFileName, blockIndex, err) + return fmt.Errorf("failed to create temporary file %s for writing block: %w", tmpFilePath, err) } defer f.Close() - blockBytes := block.Bytes() - n, err := f.Write(blockBytes) - if err != nil { + ww := rwutil.NewWriter(f) + ww.WriteUint32(blockIndex) + if ww.Err != nil { bwT.metrics.IncFailedWrites() - return fmt.Errorf("writing block index %v data to temporary file %s failed: %w", blockIndex, tmpFileName, err) + return fmt.Errorf("failed to write block index into temporary file %s: %w", tmpFilePath, ww.Err) } - if len(blockBytes) != n { + err = block.Write(f) + if err != nil { bwT.metrics.IncFailedWrites() - return fmt.Errorf("only %v of total %v bytes of block index %v were written to temporary file %s", n, len(blockBytes), blockIndex, tmpFileName) + return fmt.Errorf("writing block to temporary file %s failed: %w", tmpFilePath, err) } return nil }() @@ -87,7 +90,7 @@ func (bwT *blockWAL) Write(block state.Block) error { } bwT.metrics.BlockWritten(block.StateIndex()) - bwT.LogDebugf("Block index %v %s written to wal; file name - %s", blockIndex, commitment, finalFileName) + bwT.LogDebugf("Block index %v %s written to wal; file name - %s", blockIndex, commitment, finalFilePath) return nil } @@ -193,7 +196,48 @@ func (bwT *blockWAL) ReadAllByStateIndex(cb func(stateIndex uint32, block state. return nil } +func blockInfoFromFilePath[I any](filePath string, getInfoFun func(io.Reader) (I, error)) (I, error) { + f, err := os.OpenFile(filePath, os.O_RDONLY, 0o666) + if err != nil { + var info I + return info, fmt.Errorf("opening file %s for reading failed: %w", filePath, err) + } + defer f.Close() + return getInfoFun(f) +} + +/*func blockIndexFromFilePath(filePath string) (uint32, error) { + return blockInfoFromFilePath(filePath, blockIndexFromReader) +}*/ + func blockFromFilePath(filePath string) (state.Block, error) { + return blockInfoFromFilePath(filePath, blockFromReader) +} + +func blockIndexFromReader(r io.Reader) (uint32, error) { + rr := rwutil.NewReader(r) + info := rr.ReadUint32() + return info, rr.Err +} + +func blockFromReader(r io.Reader) (state.Block, error) { + blockIndex, err := blockIndexFromReader(r) + if err != nil { + return nil, fmt.Errorf("failed to read block index in header: %w", err) + } + block := state.NewBlock() + err = block.Read(r) + if err != nil { + return nil, fmt.Errorf("failed to read block: %w", err) + } + if blockIndex != block.StateIndex() { + return nil, fmt.Errorf("block index in header %v does not match block index in block %v", + blockIndex, block.StateIndex()) + } + return block, nil +} + +/*func blockFromFilePath(filePath string) (state.Block, error) { f, err := os.OpenFile(filePath, os.O_RDONLY, 0o666) if err != nil { return nil, fmt.Errorf("opening file %s for reading failed: %w", filePath, err) @@ -216,7 +260,7 @@ func blockFromFilePath(filePath string) (state.Block, error) { return nil, fmt.Errorf("error parsing block from bytes read from file %s: %w", filePath, err) } return block, nil -} +}*/ func blockWALSubFolderName(blockHash state.BlockHash) string { return hex.EncodeToString(blockHash[:1]) diff --git a/packages/chain/statemanager/state_manager.go b/packages/chain/statemanager/state_manager.go index 61d7ab6919..841a3f9858 100644 --- a/packages/chain/statemanager/state_manager.go +++ b/packages/chain/statemanager/state_manager.go @@ -378,15 +378,16 @@ func (smT *stateManager) handleNodePublicKeys(req *reqChainNodesUpdated) { func (smT *stateManager) handlePreliminaryBlock(msg *reqPreliminaryBlock) { if !smT.wal.Contains(msg.block.Hash()) { if err := smT.wal.Write(msg.block); err != nil { - smT.log.Warnf("Preliminary block %v cannot be saved to the WAL: %v", msg.block.L1Commitment(), err) + smT.log.Warnf("Preliminary block index %v %s cannot be saved to the WAL: %v", + msg.block.StateIndex(), msg.block.L1Commitment(), err) msg.Respond(err) return } - smT.log.Warnf("Preliminary block %v saved to the WAL.", msg.block.L1Commitment()) + smT.log.Warnf("Preliminary block index %v %s saved to the WAL.", msg.block.StateIndex(), msg.block.L1Commitment()) msg.Respond(nil) return } - smT.log.Warnf("Preliminary block %v already exist in the WAL.", msg.block.L1Commitment()) + smT.log.Warnf("Preliminary block index %v %s already exist in the WAL.", msg.block.StateIndex(), msg.block.L1Commitment()) msg.Respond(nil) } From 0cf748ff9bfbaf26f7515821b82728fe4693a4c7 Mon Sep 17 00:00:00 2001 From: Julius Andrikonis Date: Tue, 8 Aug 2023 14:15:33 +0300 Subject: [PATCH 13/26] State restoring from WAL optimised --- .../statemanager/sm_gpa/sm_gpa_utils/block_wal.go | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/chain/statemanager/sm_gpa/sm_gpa_utils/block_wal.go b/packages/chain/statemanager/sm_gpa/sm_gpa_utils/block_wal.go index 6e8dd92e92..9cef9044ea 100644 --- a/packages/chain/statemanager/sm_gpa/sm_gpa_utils/block_wal.go +++ b/packages/chain/statemanager/sm_gpa/sm_gpa_utils/block_wal.go @@ -140,13 +140,12 @@ func (bwT *blockWAL) ReadAllByStateIndex(cb func(stateIndex uint32, block state. if !strings.HasSuffix(filePath, constBlockWALFileSuffix) { return } - fileBlock, fileErr := blockFromFilePath(filePath) - if fileErr != nil { + stateIndex, err := blockIndexFromFilePath(filePath) + if err != nil { bwT.metrics.IncFailedReads() - bwT.LogWarn("Unable to read %v: %v", filePath, fileErr) + bwT.LogWarn("Unable to read %v: %v", filePath, err) return } - stateIndex := fileBlock.StateIndex() stateIndexPaths, found := blocksByStateIndex[stateIndex] if found { stateIndexPaths = append(stateIndexPaths, filePath) @@ -206,9 +205,9 @@ func blockInfoFromFilePath[I any](filePath string, getInfoFun func(io.Reader) (I return getInfoFun(f) } -/*func blockIndexFromFilePath(filePath string) (uint32, error) { +func blockIndexFromFilePath(filePath string) (uint32, error) { return blockInfoFromFilePath(filePath, blockIndexFromReader) -}*/ +} func blockFromFilePath(filePath string) (state.Block, error) { return blockInfoFromFilePath(filePath, blockFromReader) From bda64a433bdf0ade28c79e73ba06b70c376b480a Mon Sep 17 00:00:00 2001 From: Julius Andrikonis Date: Tue, 8 Aug 2023 14:41:38 +0300 Subject: [PATCH 14/26] Cleanup --- .../sm_gpa/sm_gpa_utils/block_wal.go | 26 ------------------- 1 file changed, 26 deletions(-) diff --git a/packages/chain/statemanager/sm_gpa/sm_gpa_utils/block_wal.go b/packages/chain/statemanager/sm_gpa/sm_gpa_utils/block_wal.go index 9cef9044ea..5f59585320 100644 --- a/packages/chain/statemanager/sm_gpa/sm_gpa_utils/block_wal.go +++ b/packages/chain/statemanager/sm_gpa/sm_gpa_utils/block_wal.go @@ -1,7 +1,6 @@ package sm_gpa_utils import ( - //"bufio" "encoding/hex" "fmt" "io" @@ -236,31 +235,6 @@ func blockFromReader(r io.Reader) (state.Block, error) { return block, nil } -/*func blockFromFilePath(filePath string) (state.Block, error) { - f, err := os.OpenFile(filePath, os.O_RDONLY, 0o666) - if err != nil { - return nil, fmt.Errorf("opening file %s for reading failed: %w", filePath, err) - } - defer f.Close() - stat, err := f.Stat() - if err != nil { - return nil, fmt.Errorf("reading file %s information failed: %w", filePath, err) - } - blockBytes := make([]byte, stat.Size()) - n, err := bufio.NewReader(f).Read(blockBytes) - if err != nil { - return nil, fmt.Errorf("reading file %s failed: %w", filePath, err) - } - if int64(n) != stat.Size() { - return nil, fmt.Errorf("only %v of total %v bytes of file %s were read", n, stat.Size(), filePath) - } - block, err := state.BlockFromBytes(blockBytes) - if err != nil { - return nil, fmt.Errorf("error parsing block from bytes read from file %s: %w", filePath, err) - } - return block, nil -}*/ - func blockWALSubFolderName(blockHash state.BlockHash) string { return hex.EncodeToString(blockHash[:1]) } From 53e96c3055412ac3df4e0881b8412a028fa51900 Mon Sep 17 00:00:00 2001 From: Jorge Silva Date: Tue, 8 Aug 2023 13:54:29 +0100 Subject: [PATCH 15/26] fix: mempool by nonce removal during iteration --- packages/chain/mempool/mempool.go | 7 +-- packages/chain/mempool/typed_pool_by_nonce.go | 6 +-- .../chain/mempool/typed_pool_by_nonce_test.go | 51 +++++++++++++++++++ packages/testutil/dummyrequest.go | 9 ++++ 4 files changed, 67 insertions(+), 6 deletions(-) create mode 100644 packages/chain/mempool/typed_pool_by_nonce_test.go diff --git a/packages/chain/mempool/mempool.go b/packages/chain/mempool/mempool.go index 104193a817..1885e4f7b3 100644 --- a/packages/chain/mempool/mempool.go +++ b/packages/chain/mempool/mempool.go @@ -555,8 +555,9 @@ func (mpi *mempoolImpl) refsToPropose() []*isc.RequestRef { reqNonce := e.req.Nonce() if reqNonce < accountNonce { // nonce too old, delete - mpi.log.Debugf("refsToPropose, account: %s, removing old nonce from pool: %d", account, e.req.Nonce()) + mpi.log.Debugf("refsToPropose, account: %s, removing request (%s) with old nonce (%d) from the pool", account, e.req.ID(), e.req.Nonce()) mpi.offLedgerPool.Remove(e.req) + continue } if e.old { // this request was marked as "old", do not propose it @@ -565,12 +566,12 @@ func (mpi *mempoolImpl) refsToPropose() []*isc.RequestRef { } if reqNonce == accountNonce { // expected nonce, add it to the list to propose - mpi.log.Debugf("refsToPropose, account: %s, proposing reqID %s with nonce %d: d", account, e.req.ID().String(), e.req.Nonce()) + mpi.log.Debugf("refsToPropose, account: %s, proposing reqID %s with nonce: %d", account, e.req.ID().String(), e.req.Nonce()) reqRefs = append(reqRefs, isc.RequestRefFromRequest(e.req)) accountNonce++ // increment the account nonce to match the next valid request } if reqNonce > accountNonce { - mpi.log.Debugf("refsToPropose, account: %s, req %s has a nonce %d which is too high, won't be proposed", account, e.req.ID().String(), e.req.Nonce()) + mpi.log.Debugf("refsToPropose, account: %s, req %s has a nonce %d which is too high (expected %d), won't be proposed", account, e.req.ID().String(), e.req.Nonce(), accountNonce) return // no more valid nonces for this account, continue to the next account } } diff --git a/packages/chain/mempool/typed_pool_by_nonce.go b/packages/chain/mempool/typed_pool_by_nonce.go index ae06f896e2..a115f9426e 100644 --- a/packages/chain/mempool/typed_pool_by_nonce.go +++ b/packages/chain/mempool/typed_pool_by_nonce.go @@ -132,7 +132,7 @@ func (p *TypedPoolByNonce[V]) Remove(request V) { } // find the request in the accounts map indexToDel := slices.IndexFunc(reqsByAccount, func(e *OrderedPoolEntry[V]) bool { - return true + return refKey == isc.RequestRefFromRequest(e.req).AsKey() }) if indexToDel == -1 { p.log.Error("inconsistency trying to DEL %v as key=%v, request not found in list for account %s", request.ID(), refKey, account) @@ -148,8 +148,8 @@ func (p *TypedPoolByNonce[V]) Remove(request V) { } func (p *TypedPoolByNonce[V]) Iterate(f func(account string, requests []*OrderedPoolEntry[V])) { - p.reqsByAcountOrdered.ForEach(func(acc string, reqs []*OrderedPoolEntry[V]) bool { - f(acc, reqs) + p.reqsByAcountOrdered.ForEach(func(acc string, entries []*OrderedPoolEntry[V]) bool { + f(acc, slices.Clone(entries)) return true }) } diff --git a/packages/chain/mempool/typed_pool_by_nonce_test.go b/packages/chain/mempool/typed_pool_by_nonce_test.go new file mode 100644 index 0000000000..85ef62afd8 --- /dev/null +++ b/packages/chain/mempool/typed_pool_by_nonce_test.go @@ -0,0 +1,51 @@ +package mempool + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/iotaledger/wasp/packages/isc" + "github.com/iotaledger/wasp/packages/testutil" + "github.com/iotaledger/wasp/packages/testutil/testkey" + "github.com/iotaledger/wasp/packages/testutil/testlogger" +) + +func TestSomething(t *testing.T) { + waitReq := NewWaitReq(waitRequestCleanupEvery) + pool := NewTypedPoolByNonce[isc.OffLedgerRequest](waitReq, func(int) {}, func(time.Duration) {}, testlogger.NewSilentLogger("", true)) + + // generate a bunch of requests for the same account + kp, addr := testkey.GenKeyAddr() + agentID := isc.NewAgentID(addr) + + req0 := testutil.DummyOffledgerRequestForAccount(isc.RandomChainID(), 0, kp) + req1 := testutil.DummyOffledgerRequestForAccount(isc.RandomChainID(), 1, kp) + req2 := testutil.DummyOffledgerRequestForAccount(isc.RandomChainID(), 2, kp) + req2new := testutil.DummyOffledgerRequestForAccount(isc.RandomChainID(), 2, kp) + pool.Add(req0) + pool.Add(req1) + pool.Add(req1) // try to add the same request many times + pool.Add(req2) + pool.Add(req1) + require.EqualValues(t, 3, pool.refLUT.Size()) + require.EqualValues(t, 1, pool.reqsByAcountOrdered.Size()) + reqsInPoolForAccount, _ := pool.reqsByAcountOrdered.Get(agentID.String()) + require.Len(t, reqsInPoolForAccount, 3) + pool.Add(req2new) + pool.Add(req2new) + require.EqualValues(t, 4, pool.refLUT.Size()) + require.EqualValues(t, 1, pool.reqsByAcountOrdered.Size()) + reqsInPoolForAccount, _ = pool.reqsByAcountOrdered.Get(agentID.String()) + require.Len(t, reqsInPoolForAccount, 4) + + // try to remove everything during iteration + pool.Iterate(func(account string, entries []*OrderedPoolEntry[isc.OffLedgerRequest]) { + for _, e := range entries { + pool.Remove(e.req) + } + }) + require.EqualValues(t, 0, pool.refLUT.Size()) + require.EqualValues(t, 0, pool.reqsByAcountOrdered.Size()) +} diff --git a/packages/testutil/dummyrequest.go b/packages/testutil/dummyrequest.go index 014a4e0560..697e13b83b 100644 --- a/packages/testutil/dummyrequest.go +++ b/packages/testutil/dummyrequest.go @@ -1,6 +1,7 @@ package testutil import ( + "github.com/iotaledger/wasp/packages/cryptolib" "github.com/iotaledger/wasp/packages/isc" "github.com/iotaledger/wasp/packages/kv/dict" "github.com/iotaledger/wasp/packages/testutil/testkey" @@ -15,3 +16,11 @@ func DummyOffledgerRequest(chainID isc.ChainID) isc.OffLedgerRequest { keys, _ := testkey.GenKeyAddr() return req.Sign(keys) } + +func DummyOffledgerRequestForAccount(chainID isc.ChainID, nonce uint64, kp *cryptolib.KeyPair) isc.OffLedgerRequest { + contract := isc.Hn("somecontract") + entrypoint := isc.Hn("someentrypoint") + args := dict.Dict{} + req := isc.NewOffLedgerRequest(chainID, contract, entrypoint, args, nonce, gas.LimitsDefault.MaxGasPerRequest) + return req.Sign(kp) +} From 1528092c32c71e1d4ee62de874d66786b7c8f107 Mon Sep 17 00:00:00 2001 From: Jorge Silva Date: Tue, 8 Aug 2023 14:54:05 +0100 Subject: [PATCH 16/26] fix: too many inputs in state transition --- packages/vm/vmtxbuilder/foundries.go | 56 ++++++++++---------- packages/vm/vmtxbuilder/nfts.go | 50 +++++++++--------- packages/vm/vmtxbuilder/tokens.go | 48 +++++++++--------- packages/vm/vmtxbuilder/totals.go | 22 ++++---- packages/vm/vmtxbuilder/txbuilder.go | 62 ++++++++++------------- packages/vm/vmtxbuilder/txbuilder_test.go | 15 ------ 6 files changed, 114 insertions(+), 139 deletions(-) diff --git a/packages/vm/vmtxbuilder/foundries.go b/packages/vm/vmtxbuilder/foundries.go index 580a6c602e..7ffde82f9e 100644 --- a/packages/vm/vmtxbuilder/foundries.go +++ b/packages/vm/vmtxbuilder/foundries.go @@ -44,9 +44,9 @@ func (txb *AnchorTransactionBuilder) CreateNewFoundry( } f.Amount = parameters.L1().Protocol.RentStructure.MinRent(f) txb.invokedFoundries[f.SerialNumber] = &foundryInvoked{ - serialNumber: f.SerialNumber, - in: nil, - out: f, + serialNumber: f.SerialNumber, + accountingInput: nil, + accountingOutput: f, } return f.SerialNumber, f.Amount } @@ -60,14 +60,14 @@ func (txb *AnchorTransactionBuilder) ModifyNativeTokenSupply(nativeTokenID iotag panic(vm.ErrFoundryDoesNotExist) } // check if the loaded foundry matches the nativeTokenID - if nativeTokenID != f.in.MustNativeTokenID() { + if nativeTokenID != f.accountingInput.MustNativeTokenID() { panic(fmt.Errorf("%v: requested token ID: %s, foundry token id: %s", - vm.ErrCantModifySupplyOfTheToken, nativeTokenID.String(), f.in.MustNativeTokenID().String())) + vm.ErrCantModifySupplyOfTheToken, nativeTokenID.String(), f.accountingInput.MustNativeTokenID().String())) } defer txb.mustCheckTotalNativeTokensExceeded() - simpleTokenScheme := util.MustTokenScheme(f.out.TokenScheme) + simpleTokenScheme := util.MustTokenScheme(f.accountingOutput.TokenScheme) // check the supply bounds var newMinted, newMelted *big.Int @@ -88,7 +88,7 @@ func (txb *AnchorTransactionBuilder) ModifyNativeTokenSupply(nativeTokenID iotag simpleTokenScheme.MeltedTokens = newMelted txb.invokedFoundries[sn] = f - adjustment += int64(f.in.Amount) - int64(f.out.Amount) + adjustment += int64(f.accountingInput.Amount) - int64(f.accountingOutput.Amount) return adjustment } @@ -103,10 +103,10 @@ func (txb *AnchorTransactionBuilder) ensureFoundry(sn uint32) *foundryInvoked { return nil } f := &foundryInvoked{ - serialNumber: foundryOutput.SerialNumber, - outputID: outputID, - in: foundryOutput, - out: cloneFoundryOutput(foundryOutput), + serialNumber: foundryOutput.SerialNumber, + accountingInputID: outputID, + accountingInput: foundryOutput, + accountingOutput: cloneFoundryOutput(foundryOutput), } txb.invokedFoundries[sn] = f return f @@ -118,14 +118,14 @@ func (txb *AnchorTransactionBuilder) DestroyFoundry(sn uint32) uint64 { if f == nil { panic(vm.ErrFoundryDoesNotExist) } - if f.in == nil { + if f.accountingInput == nil { panic(vm.ErrCantDestroyFoundryBeingCreated) } defer txb.mustCheckTotalNativeTokensExceeded() - f.out = nil - return f.in.Amount + f.accountingOutput = nil + return f.accountingInput.Amount } func (txb *AnchorTransactionBuilder) nextFoundrySerialNumber() uint32 { @@ -172,27 +172,27 @@ func (txb *AnchorTransactionBuilder) FoundriesToBeUpdated() ([]uint32, []uint32) func (txb *AnchorTransactionBuilder) FoundryOutputsBySN(serNums []uint32) map[uint32]*iotago.FoundryOutput { ret := make(map[uint32]*iotago.FoundryOutput) for _, sn := range serNums { - ret[sn] = txb.invokedFoundries[sn].out + ret[sn] = txb.invokedFoundries[sn].accountingOutput } return ret } type foundryInvoked struct { - serialNumber uint32 - outputID iotago.OutputID // if in != nil - in *iotago.FoundryOutput // nil if created - out *iotago.FoundryOutput // nil if destroyed + serialNumber uint32 + accountingInputID iotago.OutputID // if in != nil + accountingInput *iotago.FoundryOutput // nil if created + accountingOutput *iotago.FoundryOutput // nil if destroyed } func (f *foundryInvoked) Clone() *foundryInvoked { outputID := iotago.OutputID{} - copy(outputID[:], f.outputID[:]) + copy(outputID[:], f.accountingInputID[:]) return &foundryInvoked{ - serialNumber: f.serialNumber, - outputID: outputID, - in: cloneFoundryOutput(f.in), - out: cloneFoundryOutput(f.out), + serialNumber: f.serialNumber, + accountingInputID: outputID, + accountingInput: cloneFoundryOutput(f.accountingInput), + accountingOutput: cloneFoundryOutput(f.accountingOutput), } } @@ -201,20 +201,20 @@ func (f *foundryInvoked) isNewCreated() bool { } func (f *foundryInvoked) requiresExistingAccountingUTXOAsInput() bool { - if f.in == nil { + if f.accountingInput == nil { return false } - if identicalFoundries(f.in, f.out) { + if identicalFoundries(f.accountingInput, f.accountingOutput) { return false } return true } func (f *foundryInvoked) producesAccountingOutput() bool { - if f.out == nil { + if f.accountingOutput == nil { return false } - if identicalFoundries(f.in, f.out) { + if identicalFoundries(f.accountingInput, f.accountingOutput) { return false } return true diff --git a/packages/vm/vmtxbuilder/nfts.go b/packages/vm/vmtxbuilder/nfts.go index 9eee321696..579fbd25c4 100644 --- a/packages/vm/vmtxbuilder/nfts.go +++ b/packages/vm/vmtxbuilder/nfts.go @@ -10,11 +10,11 @@ import ( ) type nftIncluded struct { - ID iotago.NFTID - outputID iotago.OutputID // only available when the input is already accounted for (NFT was deposited in a previous block) - in *iotago.NFTOutput - out *iotago.NFTOutput // this is not the same as in the `nativeTokenBalance` struct, this can be the accounting output, or the output leaving the chain. // TODO should refactor to follow the same logic so its easier to grok - sentOutside bool + ID iotago.NFTID + accountingInputID iotago.OutputID // only available when the input is already accounted for (NFT was deposited in a previous block) + accountingInput *iotago.NFTOutput + resultingOutput *iotago.NFTOutput // this is not the same as in the `nativeTokenBalance` struct, this can be the accounting output, or the output leaving the chain. // TODO should refactor to follow the same logic so its easier to grok + sentOutside bool } // 3 cases of handling NFTs in txbuilder @@ -28,13 +28,13 @@ func (n *nftIncluded) Clone() *nftIncluded { copy(nftID[:], n.ID[:]) outputID := iotago.OutputID{} - copy(outputID[:], n.outputID[:]) + copy(outputID[:], n.accountingInputID[:]) return &nftIncluded{ - ID: nftID, - outputID: outputID, - in: cloneInternalNFTOutputOrNil(n.in), - out: cloneInternalNFTOutputOrNil(n.out), + ID: nftID, + accountingInputID: outputID, + accountingInput: cloneInternalNFTOutputOrNil(n.accountingInput), + resultingOutput: cloneInternalNFTOutputOrNil(n.resultingOutput), } } @@ -61,7 +61,7 @@ func (txb *AnchorTransactionBuilder) NFTOutputs() []*iotago.NFTOutput { for _, nft := range txb.nftsSorted() { if !nft.sentOutside { // outputs sent outside are already added to txb.postedOutputs - outs = append(outs, nft.out) + outs = append(outs, nft.resultingOutput) } } return outs @@ -71,9 +71,9 @@ func (txb *AnchorTransactionBuilder) NFTOutputsToBeUpdated() (toBeAdded, toBeRem toBeAdded = make([]*iotago.NFTOutput, 0, len(txb.nftsIncluded)) toBeRemoved = make([]*iotago.NFTOutput, 0, len(txb.nftsIncluded)) for _, nft := range txb.nftsSorted() { - if nft.in != nil { + if nft.accountingInput != nil { // to remove if input is not nil (nft exists in accounting), and its sent to outside the chain - toBeRemoved = append(toBeRemoved, nft.out) + toBeRemoved = append(toBeRemoved, nft.resultingOutput) continue } if nft.sentOutside { @@ -81,7 +81,7 @@ func (txb *AnchorTransactionBuilder) NFTOutputsToBeUpdated() (toBeAdded, toBeRem continue } // to add if input is nil (doesn't exist in accounting), and its not sent outside the chain - toBeAdded = append(toBeAdded, nft.out) + toBeAdded = append(toBeAdded, nft.resultingOutput) } return toBeAdded, toBeRemoved } @@ -111,10 +111,10 @@ func (txb *AnchorTransactionBuilder) internalNFTOutputFromRequest(nftOutput *iot out.Amount = parameters.L1().Protocol.RentStructure.MinRent(out) ret := &nftIncluded{ - ID: out.NFTID, - in: nil, - out: out, - sentOutside: false, + ID: out.NFTID, + accountingInput: nil, + resultingOutput: out, + sentOutside: false, } txb.nftsIncluded[out.NFTID] = ret @@ -129,8 +129,8 @@ func (txb *AnchorTransactionBuilder) sendNFT(o *iotago.NFTOutput) int64 { if txb.nftsIncluded[o.NFTID] != nil { // NFT comes in and out in the same block txb.nftsIncluded[o.NFTID].sentOutside = true - sd := txb.nftsIncluded[o.NFTID].out.Amount // reimburse the SD cost - txb.nftsIncluded[o.NFTID].out = o + sd := txb.nftsIncluded[o.NFTID].resultingOutput.Amount // reimburse the SD cost + txb.nftsIncluded[o.NFTID].resultingOutput = o return int64(sd) } if txb.InputsAreFull() { @@ -140,11 +140,11 @@ func (txb *AnchorTransactionBuilder) sendNFT(o *iotago.NFTOutput) int64 { // using NFT already owned by the chain in, outputID := txb.accountsView.NFTOutput(o.NFTID) toInclude := &nftIncluded{ - ID: o.NFTID, - in: in, - outputID: outputID, - out: o, - sentOutside: true, + ID: o.NFTID, + accountingInput: in, + accountingInputID: outputID, + resultingOutput: o, + sentOutside: true, } txb.nftsIncluded[o.NFTID] = toInclude diff --git a/packages/vm/vmtxbuilder/tokens.go b/packages/vm/vmtxbuilder/tokens.go index feb538807f..1473752f93 100644 --- a/packages/vm/vmtxbuilder/tokens.go +++ b/packages/vm/vmtxbuilder/tokens.go @@ -15,10 +15,10 @@ import ( // nativeTokenBalance represents on-chain account of the specific native token type nativeTokenBalance struct { - nativeTokenID iotago.NativeTokenID - accountingoutputID iotago.OutputID // if in != nil, otherwise zeroOutputID - in *iotago.BasicOutput // if nil it means output does not exist, this is new account for the token_id - accountingOutput *iotago.BasicOutput // current balance of the token_id on the chain + nativeTokenID iotago.NativeTokenID + accountingInputID iotago.OutputID // if in != nil, otherwise zeroOutputID + accountingInput *iotago.BasicOutput // if nil it means output does not exist, this is new account for the token_id + accountingOutput *iotago.BasicOutput // current balance of the token_id on the chain } func (n *nativeTokenBalance) Clone() *nativeTokenBalance { @@ -26,13 +26,13 @@ func (n *nativeTokenBalance) Clone() *nativeTokenBalance { copy(nativeTokenID[:], n.nativeTokenID[:]) outputID := iotago.OutputID{} - copy(outputID[:], n.accountingoutputID[:]) + copy(outputID[:], n.accountingInputID[:]) return &nativeTokenBalance{ - nativeTokenID: nativeTokenID, - accountingoutputID: outputID, - in: cloneInternalBasicOutputOrNil(n.in), - accountingOutput: cloneInternalBasicOutputOrNil(n.accountingOutput), + nativeTokenID: nativeTokenID, + accountingInputID: outputID, + accountingInput: cloneInternalBasicOutputOrNil(n.accountingInput), + accountingOutput: cloneInternalBasicOutputOrNil(n.accountingOutput), } } @@ -55,7 +55,7 @@ func (n *nativeTokenBalance) requiresExistingAccountingUTXOAsInput() bool { // value didn't change return false } - return n.in != nil + return n.accountingInput != nil } func (n *nativeTokenBalance) getOutValue() *big.Int { @@ -86,23 +86,23 @@ func (n *nativeTokenBalance) updateMinSD() { func (n *nativeTokenBalance) identicalInOut() bool { switch { - case n.in == n.accountingOutput: + case n.accountingInput == n.accountingOutput: panic("identicalBasicOutputs: internal inconsistency 1") - case n.in == nil || n.accountingOutput == nil: + case n.accountingInput == nil || n.accountingOutput == nil: return false - case !n.in.Ident().Equal(n.accountingOutput.Ident()): + case !n.accountingInput.Ident().Equal(n.accountingOutput.Ident()): return false - case n.in.Amount != n.accountingOutput.Amount: + case n.accountingInput.Amount != n.accountingOutput.Amount: return false - case !n.in.NativeTokens.Equal(n.accountingOutput.NativeTokens): + case !n.accountingInput.NativeTokens.Equal(n.accountingOutput.NativeTokens): return false - case !n.in.Features.Equal(n.accountingOutput.Features): + case !n.accountingInput.Features.Equal(n.accountingOutput.Features): return false - case len(n.in.NativeTokens) != 1: + case len(n.accountingInput.NativeTokens) != 1: panic("identicalBasicOutputs: internal inconsistency 2") case len(n.accountingOutput.NativeTokens) != 1: panic("identicalBasicOutputs: internal inconsistency 3") - case n.in.NativeTokens[0].ID != n.nativeTokenID: + case n.accountingInput.NativeTokens[0].ID != n.nativeTokenID: panic("identicalBasicOutputs: internal inconsistency 4") case n.accountingOutput.NativeTokens[0].ID != n.nativeTokenID: panic("identicalBasicOutputs: internal inconsistency 5") @@ -187,11 +187,11 @@ func (txb *AnchorTransactionBuilder) addNativeTokenBalanceDelta(nativeTokenID io if util.IsZeroBigInt(nt.getOutValue()) { // 0 native tokens on the output side - if nt.in == nil { + if nt.accountingInput == nil { // in this case the internar accounting output that would be created is not needed anymore, reiburse the SD return int64(nt.accountingOutput.Amount) } - return int64(nt.in.Amount) + return int64(nt.accountingInput.Amount) } // update the SD in case the storage deposit has changed from the last time this output was used @@ -228,10 +228,10 @@ func (txb *AnchorTransactionBuilder) ensureNativeTokenBalance(nativeTokenID iota } nativeTokenBalance := &nativeTokenBalance{ - nativeTokenID: nativeTokenID, - accountingoutputID: outputID, - in: basicOutputIn, - accountingOutput: basicOutputOut, + nativeTokenID: nativeTokenID, + accountingInputID: outputID, + accountingInput: basicOutputIn, + accountingOutput: basicOutputOut, } txb.balanceNativeTokens[nativeTokenID] = nativeTokenBalance return nativeTokenBalance diff --git a/packages/vm/vmtxbuilder/totals.go b/packages/vm/vmtxbuilder/totals.go index d7fe896d0a..d23843c38e 100644 --- a/packages/vm/vmtxbuilder/totals.go +++ b/packages/vm/vmtxbuilder/totals.go @@ -45,10 +45,10 @@ func (txb *AnchorTransactionBuilder) sumInputs() *TransactionTotals { if !ok { s = new(big.Int) } - s.Add(s, ntb.in.NativeTokens[0].Amount) + s.Add(s, ntb.accountingInput.NativeTokens[0].Amount) totals.NativeTokenBalances[id] = s // sum up storage deposit in inputs of internal UTXOs - totals.TotalBaseTokensInStorageDeposit += ntb.in.Amount + totals.TotalBaseTokensInStorageDeposit += ntb.accountingInput.Amount } // sum up all explicitly consumed outputs, except anchor output for _, out := range txb.consumed { @@ -65,16 +65,16 @@ func (txb *AnchorTransactionBuilder) sumInputs() *TransactionTotals { } for _, f := range txb.invokedFoundries { if f.requiresExistingAccountingUTXOAsInput() { - totals.TotalBaseTokensInStorageDeposit += f.in.Amount - simpleTokenScheme := util.MustTokenScheme(f.in.TokenScheme) - totals.TokenCirculatingSupplies[f.in.MustNativeTokenID()] = new(big.Int). + totals.TotalBaseTokensInStorageDeposit += f.accountingInput.Amount + simpleTokenScheme := util.MustTokenScheme(f.accountingInput.TokenScheme) + totals.TokenCirculatingSupplies[f.accountingInput.MustNativeTokenID()] = new(big.Int). Sub(simpleTokenScheme.MintedTokens, simpleTokenScheme.MeltedTokens) } } for _, nft := range txb.nftsIncluded { - if !isc.IsEmptyOutputID(nft.outputID) { - totals.TotalBaseTokensInStorageDeposit += nft.in.Amount + if !isc.IsEmptyOutputID(nft.accountingInputID) { + totals.TotalBaseTokensInStorageDeposit += nft.accountingInput.Amount } } @@ -111,10 +111,10 @@ func (txb *AnchorTransactionBuilder) sumOutputs() *TransactionTotals { if !f.producesAccountingOutput() { continue } - totals.TotalBaseTokensInStorageDeposit += f.out.Amount - id := f.out.MustNativeTokenID() + totals.TotalBaseTokensInStorageDeposit += f.accountingOutput.Amount + id := f.accountingOutput.MustNativeTokenID() totals.TokenCirculatingSupplies[id] = big.NewInt(0) - simpleTokenScheme := util.MustTokenScheme(f.out.TokenScheme) + simpleTokenScheme := util.MustTokenScheme(f.accountingOutput.TokenScheme) totals.TokenCirculatingSupplies[id].Sub(simpleTokenScheme.MintedTokens, simpleTokenScheme.MeltedTokens) } for _, o := range txb.postedOutputs { @@ -131,7 +131,7 @@ func (txb *AnchorTransactionBuilder) sumOutputs() *TransactionTotals { } for _, nft := range txb.nftsIncluded { if !nft.sentOutside { - totals.TotalBaseTokensInStorageDeposit += nft.out.Amount + totals.TotalBaseTokensInStorageDeposit += nft.resultingOutput.Amount } } return totals diff --git a/packages/vm/vmtxbuilder/txbuilder.go b/packages/vm/vmtxbuilder/txbuilder.go index 51f62f3100..bd9c8acfac 100644 --- a/packages/vm/vmtxbuilder/txbuilder.go +++ b/packages/vm/vmtxbuilder/txbuilder.go @@ -95,10 +95,10 @@ func (txb *AnchorTransactionBuilder) Clone() *AnchorTransactionBuilder { } } -// SplitAssetsIntoInternalOutputs splits the native Tokens/NFT from a given (request) output. +// splitAssetsIntoInternalOutputs splits the native Tokens/NFT from a given (request) output. // returns the resulting outputs and the list of new outputs // (some of the native tokens might already have an accounting output owned by the chain, so we don't need new outputs for those) -func (txb *AnchorTransactionBuilder) SplitAssetsIntoInternalOutputs(req isc.OnLedgerRequest) uint64 { +func (txb *AnchorTransactionBuilder) splitAssetsIntoInternalOutputs(req isc.OnLedgerRequest) uint64 { requiredSD := uint64(0) for _, nativeToken := range req.Assets().NativeTokens { // ensure this NT is in the txbuilder, update it @@ -117,27 +117,32 @@ func (txb *AnchorTransactionBuilder) SplitAssetsIntoInternalOutputs(req isc.OnLe if req.NFT() != nil { // create new output nftIncl := txb.internalNFTOutputFromRequest(req.Output().(*iotago.NFTOutput), req.OutputID()) - requiredSD += nftIncl.out.Amount + requiredSD += nftIncl.resultingOutput.Amount } txb.consumed = append(txb.consumed, req) return requiredSD } +func (txb *AnchorTransactionBuilder) assertLimits() { + if txb.InputsAreFull() { + panic(vmexceptions.ErrInputLimitExceeded) + } + if txb.outputsAreFull() { + panic(vmexceptions.ErrOutputLimitExceeded) + } + txb.mustCheckTotalNativeTokensExceeded() +} + // Consume adds an input to the transaction. // It panics if transaction cannot hold that many inputs // All explicitly consumed inputs will hold fixed index in the transaction // It updates total assets held by the chain. So it may panic due to exceed output counts // Returns the amount of baseTokens needed to cover SD costs for the NTs/NFT contained by the request output func (txb *AnchorTransactionBuilder) Consume(req isc.OnLedgerRequest) uint64 { - if txb.InputsAreFull() { - panic(vmexceptions.ErrInputLimitExceeded) - } - - defer txb.mustCheckTotalNativeTokensExceeded() - + defer txb.assertLimits() // deduct the minSD for all the outputs that need to be created - requiredSD := txb.SplitAssetsIntoInternalOutputs(req) + requiredSD := txb.splitAssetsIntoInternalOutputs(req) return requiredSD } @@ -145,31 +150,16 @@ func (txb *AnchorTransactionBuilder) Consume(req isc.OnLedgerRequest) uint64 { // consumes the original request and cretes a new output keeping assets intact // return the position of the resulting output in `txb.postedOutputs` func (txb *AnchorTransactionBuilder) ConsumeUnprocessable(req isc.OnLedgerRequest) int { - if txb.InputsAreFull() { - panic(vmexceptions.ErrInputLimitExceeded) - } - - if txb.outputsAreFull() { - panic(vmexceptions.ErrOutputLimitExceeded) - } - - defer txb.mustCheckTotalNativeTokensExceeded() - + defer txb.assertLimits() txb.consumed = append(txb.consumed, req) - txb.postedOutputs = append(txb.postedOutputs, retryOutputFromOnLedgerRequest(req, txb.anchorOutput.AliasID)) - return len(txb.postedOutputs) - 1 } // AddOutput adds an information about posted request. It will produce output // Return adjustment needed for the L2 ledger (adjustment on base tokens related to storage deposit) func (txb *AnchorTransactionBuilder) AddOutput(o iotago.Output) int64 { - if txb.outputsAreFull() { - panic(vmexceptions.ErrOutputLimitExceeded) - } - - defer txb.mustCheckTotalNativeTokensExceeded() + defer txb.assertLimits() storageDeposit := parameters.L1().Protocol.RentStructure.MinRent(o) if o.Deposit() < storageDeposit { @@ -239,27 +229,27 @@ func (txb *AnchorTransactionBuilder) inputs() (iotago.OutputSet, iotago.OutputID // internal native token outputs for _, nativeTokenBalance := range txb.nativeTokenOutputsSorted() { if nativeTokenBalance.requiresExistingAccountingUTXOAsInput() { - outputID := nativeTokenBalance.accountingoutputID + outputID := nativeTokenBalance.accountingInputID outputIDs = append(outputIDs, outputID) - inputs[outputID] = nativeTokenBalance.in + inputs[outputID] = nativeTokenBalance.accountingInput } } // foundries for _, foundry := range txb.foundriesSorted() { if foundry.requiresExistingAccountingUTXOAsInput() { - outputID := foundry.outputID + outputID := foundry.accountingInputID outputIDs = append(outputIDs, outputID) - inputs[outputID] = foundry.in + inputs[outputID] = foundry.accountingInput } } // nfts for _, nft := range txb.nftsSorted() { - if !isc.IsEmptyOutputID(nft.outputID) { - outputID := nft.outputID + if !isc.IsEmptyOutputID(nft.accountingInputID) { + outputID := nft.accountingInputID outputIDs = append(outputIDs, outputID) - inputs[outputID] = nft.in + inputs[outputID] = nft.accountingInput } } @@ -325,7 +315,7 @@ func (txb *AnchorTransactionBuilder) outputs(stateMetadata []byte) iotago.Output // creating outputs for updated foundries foundriesToBeUpdated, _ := txb.FoundriesToBeUpdated() for _, sn := range foundriesToBeUpdated { - ret = append(ret, txb.invokedFoundries[sn].out) + ret = append(ret, txb.invokedFoundries[sn].accountingOutput) } // creating outputs for new NFTs nftOuts := txb.NFTOutputs() @@ -351,7 +341,7 @@ func (txb *AnchorTransactionBuilder) numInputs() int { } } for _, nft := range txb.nftsIncluded { - if !isc.IsEmptyOutputID(nft.outputID) { + if !isc.IsEmptyOutputID(nft.accountingInputID) { ret++ } } diff --git a/packages/vm/vmtxbuilder/txbuilder_test.go b/packages/vm/vmtxbuilder/txbuilder_test.go index 32dcf45cbb..bb8b6f2379 100644 --- a/packages/vm/vmtxbuilder/txbuilder_test.go +++ b/packages/vm/vmtxbuilder/txbuilder_test.go @@ -271,13 +271,6 @@ func TestTxBuilderConsistency(t *testing.T) { runConsume(txb, nativeTokenIDs, runTimes, testAmount, mockedAccounts) }, vmexceptions.ErrInputLimitExceeded) require.Error(t, err, vmexceptions.ErrInputLimitExceeded) - - essence, _ := txb.BuildTransactionEssence(dummyStateMetadata) - txb.MustBalanced() - - essenceBytes, err := essence.Serialize(serializer.DeSeriModeNoValidation, nil) - require.NoError(t, err) - t.Logf("essence bytes len = %d", len(essenceBytes)) }) t.Run("exceeded outputs", func(t *testing.T) { const runTimesInputs = 120 @@ -295,15 +288,7 @@ func TestTxBuilderConsistency(t *testing.T) { addOutput(txb, 1, nativeTokenIDs[idx], mockedAccounts) } }, vmexceptions.ErrOutputLimitExceeded) - require.Error(t, err, vmexceptions.ErrOutputLimitExceeded) - - essence, _ := txb.BuildTransactionEssence(dummyStateMetadata) - txb.MustBalanced() - - essenceBytes, err := essence.Serialize(serializer.DeSeriModeNoValidation, nil) - require.NoError(t, err) - t.Logf("essence bytes len = %d", len(essenceBytes)) }) t.Run("randomize", func(t *testing.T) { const runTimes = 30 From 00a14b1ad4aaa2708a1d2d3ac3854bb3d7cd3275 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 8 Aug 2023 23:23:38 +0000 Subject: [PATCH 17/26] chore(deps): update golang docker tag to v1.21 --- Dockerfile | 2 +- Dockerfile.noncached | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index c2ad20a3ae..f6e4505829 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # syntax=docker/dockerfile:1 -ARG GOLANG_IMAGE_TAG=1.20-bullseye +ARG GOLANG_IMAGE_TAG=1.21-bullseye # Build stage FROM golang:${GOLANG_IMAGE_TAG} AS build diff --git a/Dockerfile.noncached b/Dockerfile.noncached index f25f884341..2f9a52d9fa 100644 --- a/Dockerfile.noncached +++ b/Dockerfile.noncached @@ -1,5 +1,5 @@ # syntax=docker/dockerfile:1 -ARG GOLANG_IMAGE_TAG=1.20-bullseye +ARG GOLANG_IMAGE_TAG=1.21-bullseye # Build stage FROM golang:${GOLANG_IMAGE_TAG} AS build From 27593954c8643a8d554b1eb8e616c4529a472c81 Mon Sep 17 00:00:00 2001 From: Jorge Silva Date: Wed, 9 Aug 2023 11:54:05 +0100 Subject: [PATCH 18/26] fix: webapi blockinfo use hex instead of raw bytes for previousAO --- packages/webapi/models/core_blocklog.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/webapi/models/core_blocklog.go b/packages/webapi/models/core_blocklog.go index 59c36e28e5..a76000e7a1 100644 --- a/packages/webapi/models/core_blocklog.go +++ b/packages/webapi/models/core_blocklog.go @@ -30,7 +30,7 @@ func MapBlockInfoResponse(info *blocklog.BlockInfo) *BlockInfoResponse { prevAOStr := "" if info.PreviousAliasOutput != nil { blockindex = info.PreviousAliasOutput.GetAliasOutput().StateIndex + 1 - prevAOStr = string(info.PreviousAliasOutput.Bytes()) + prevAOStr = iotago.EncodeHex(info.PreviousAliasOutput.Bytes()) } return &BlockInfoResponse{ BlockIndex: blockindex, From 1bb68036583098e43b14ba483e9d47ab7942a393 Mon Sep 17 00:00:00 2001 From: Julius Andrikonis Date: Thu, 10 Aug 2023 15:15:44 +0300 Subject: [PATCH 19/26] [Almost complete] backward compatibility with legacy WAL format --- .../sm_gpa/sm_gpa_utils/block_wal.go | 95 ++++++++++++++---- .../sm_gpa/sm_gpa_utils/block_wal_test.go | 96 ++++++++++++++++++- 2 files changed, 166 insertions(+), 25 deletions(-) diff --git a/packages/chain/statemanager/sm_gpa/sm_gpa_utils/block_wal.go b/packages/chain/statemanager/sm_gpa/sm_gpa_utils/block_wal.go index 5f59585320..7944764814 100644 --- a/packages/chain/statemanager/sm_gpa/sm_gpa_utils/block_wal.go +++ b/packages/chain/statemanager/sm_gpa/sm_gpa_utils/block_wal.go @@ -47,6 +47,13 @@ func NewBlockWAL(log *logger.Logger, baseDir string, chainID isc.ChainID, metric } // Overwrites, if block is already in WAL +// Block format (version 1): +// - Version (4 bytes, unsigned int); value 1 +// - State index (4 bytes, unsigned int) +// - Block bytes +// +// Block format (legacy = version 0): +// - Block bytes func (bwT *blockWAL) Write(block state.Block) error { blockIndex := block.StateIndex() commitment := block.L1Commitment() @@ -65,6 +72,7 @@ func (bwT *blockWAL) Write(block state.Block) error { } defer f.Close() ww := rwutil.NewWriter(f) + ww.WriteUint32(1) // Version; 4 bytes (instead of just 1) to lower number of possible collisions with legacy WAL format ww.WriteUint32(blockIndex) if ww.Err != nil { bwT.metrics.IncFailedWrites() @@ -194,14 +202,38 @@ func (bwT *blockWAL) ReadAllByStateIndex(cb func(stateIndex uint32, block state. return nil } -func blockInfoFromFilePath[I any](filePath string, getInfoFun func(io.Reader) (I, error)) (I, error) { +func blockInfoFromFilePath[I any](filePath string, getInfoFun func(uint32, io.Reader) (I, error)) (I, error) { f, err := os.OpenFile(filePath, os.O_RDONLY, 0o666) + var info I if err != nil { - var info I return info, fmt.Errorf("opening file %s for reading failed: %w", filePath, err) } defer f.Close() - return getInfoFun(f) + rr := rwutil.NewReader(f) + version := rr.ReadUint32() + if rr.Err != nil { + return info, fmt.Errorf("failed reading file version: %w", rr.Err) + } + var errV error + if version == 1 { + info, errV = getInfoFun(version, f) + if errV == nil { + return info, nil + } + // error reading as version 1, maybe it's legacy version? + } + // backwards compatibility - reading legacy version + // NOTE: reopening file, because version bytes (or possibly more) has already been read + f, err = os.OpenFile(filePath, os.O_RDONLY, 0o666) + if err != nil { + return info, fmt.Errorf("reopening file %s for reading failed: %w", filePath, err) + } + defer f.Close() + info, err = getInfoFun(0, f) + if errV == nil { + return info, err + } + return info, fmt.Errorf("version %v error: %w, legacy version error: %w", version, errV, err) } func blockIndexFromFilePath(filePath string) (uint32, error) { @@ -212,27 +244,48 @@ func blockFromFilePath(filePath string) (state.Block, error) { return blockInfoFromFilePath(filePath, blockFromReader) } -func blockIndexFromReader(r io.Reader) (uint32, error) { - rr := rwutil.NewReader(r) - info := rr.ReadUint32() - return info, rr.Err +func blockIndexFromReader(version uint32, r io.Reader) (uint32, error) { + switch version { + case 1: + rr := rwutil.NewReader(r) + index := rr.ReadUint32() + return index, rr.Err + case 0: + block := state.NewBlock() + err := block.Read(r) + if err != nil { + return 0, err + } + return block.StateIndex(), nil + default: + return 0, fmt.Errorf("unknown block version %v", version) + } } -func blockFromReader(r io.Reader) (state.Block, error) { - blockIndex, err := blockIndexFromReader(r) - if err != nil { - return nil, fmt.Errorf("failed to read block index in header: %w", err) - } - block := state.NewBlock() - err = block.Read(r) - if err != nil { - return nil, fmt.Errorf("failed to read block: %w", err) - } - if blockIndex != block.StateIndex() { - return nil, fmt.Errorf("block index in header %v does not match block index in block %v", - blockIndex, block.StateIndex()) +func blockFromReader(version uint32, r io.Reader) (state.Block, error) { + switch version { + case 1: + blockIndex, err := blockIndexFromReader(version, r) + if err != nil { + return nil, fmt.Errorf("failed to read block index in header: %w", err) + } + block := state.NewBlock() + err = block.Read(r) + if err != nil { + return nil, fmt.Errorf("failed to read block: %w", err) + } + if blockIndex != block.StateIndex() { + return nil, fmt.Errorf("block index in header %v does not match block index in block %v", + blockIndex, block.StateIndex()) + } + return block, nil + case 0: + block := state.NewBlock() + err := block.Read(r) + return block, err + default: + return nil, fmt.Errorf("unknown block version %v", version) } - return block, nil } func blockWALSubFolderName(blockHash state.BlockHash) string { diff --git a/packages/chain/statemanager/sm_gpa/sm_gpa_utils/block_wal_test.go b/packages/chain/statemanager/sm_gpa/sm_gpa_utils/block_wal_test.go index 086576391c..28be65f7ec 100644 --- a/packages/chain/statemanager/sm_gpa/sm_gpa_utils/block_wal_test.go +++ b/packages/chain/statemanager/sm_gpa/sm_gpa_utils/block_wal_test.go @@ -49,6 +49,24 @@ func TestBlockWALBasic(t *testing.T) { require.Error(t, err) } +// Check if block prior to version 1 is read (that has no version data) +func TestBlockWALLegacy(t *testing.T) { + log := testlogger.NewLogger(t) + defer log.Sync() + defer cleanupAfterTest(t) + + factory := NewBlockFactory(t) + blocks := factory.GetBlocks(4, 1) + wal, err := NewBlockWAL(log, constTestFolder, factory.GetChainID(), mockBlockWALMetrics()) + require.NoError(t, err) + writeBlocksLegacy(t, factory.GetChainID(), blocks) + for i := range blocks { + block, err := wal.Read(blocks[i].Hash()) + require.NoError(t, err) + CheckBlocksEqual(t, blocks[i], block) + } +} + // Check if existing block in WAL is found even if it is not in a subfolder func TestBlockWALNoSubfolder(t *testing.T) { log := testlogger.NewLogger(t) @@ -63,12 +81,9 @@ func TestBlockWALNoSubfolder(t *testing.T) { err = wal.Write(blocks[i]) require.NoError(t, err) } - pathNoSubfolderFromHashFun := func(blockHash state.BlockHash) string { - return filepath.Join(constTestFolder, factory.GetChainID().String(), blockWALFileName(blockHash)) - } for _, block := range blocks { pathWithSubfolder := walPathFromHash(factory.GetChainID(), block.Hash()) - pathNoSubfolder := pathNoSubfolderFromHashFun(block.Hash()) + pathNoSubfolder := walPathNoSubfolderFromHash(factory.GetChainID(), block.Hash()) err = os.Rename(pathWithSubfolder, pathNoSubfolder) require.NoError(t, err) } @@ -144,10 +159,83 @@ func TestBlockWALRestart(t *testing.T) { } } +func testReadAllByStateIndex(t *testing.T, addToWALFun func(isc.ChainID, BlockWAL, []state.Block)) { + log := testlogger.NewLogger(t) + defer log.Sync() + defer cleanupAfterTest(t) + + factory := NewBlockFactory(t) + mainBlocks := 50 + branchBlocks := 20 + branchBlockIndex := mainBlocks - branchBlocks - 1 + blocksMain := factory.GetBlocks(mainBlocks, 1) + blocksBranch := factory.GetBlocksFrom(branchBlocks, 1, blocksMain[branchBlockIndex].L1Commitment(), 2) + wal, err := NewBlockWAL(log, constTestFolder, factory.GetChainID(), mockBlockWALMetrics()) + require.NoError(t, err) + addToWALFun(factory.GetChainID(), wal, blocksMain) + addToWALFun(factory.GetChainID(), wal, blocksBranch) + + var blocksRead []state.Block + err = wal.ReadAllByStateIndex(func(stateIndex uint32, block state.Block) bool { + require.Equal(t, stateIndex, block.StateIndex()) + blocksRead = append(blocksRead, block) + return true + }) + require.NoError(t, err) + + for i := 0; i <= branchBlockIndex; i++ { + require.Equal(t, uint32(i+1), blocksRead[i].StateIndex()) + CheckBlocksEqual(t, blocksMain[i], blocksRead[i]) + } + for i := branchBlockIndex + 1; i < mainBlocks; i++ { + blocksReadIndex := i*2 - branchBlockIndex - 1 + block1 := blocksRead[blocksReadIndex] + block2 := blocksRead[blocksReadIndex+1] + require.Equal(t, uint32(i+1), block1.StateIndex()) + require.Equal(t, uint32(i+1), block2.StateIndex()) + if !blocksMain[i].L1Commitment().Equals(block1.L1Commitment()) { + block1, block2 = block2, block1 + } + CheckBlocksEqual(t, blocksMain[i], block1) + CheckBlocksEqual(t, blocksBranch[i-branchBlockIndex-1], block2) + } +} + +func TestReadAllByStateIndexV1(t *testing.T) { + testReadAllByStateIndex(t, func(chainID isc.ChainID, wal BlockWAL, blocks []state.Block) { + for _, block := range blocks { + err := wal.Write(block) + require.NoError(t, err) + } + }) +} + +func TestReadAllByStateIndexLegacy(t *testing.T) { + testReadAllByStateIndex(t, func(chainID isc.ChainID, wal BlockWAL, blocks []state.Block) { + writeBlocksLegacy(t, chainID, blocks) + }) +} + func walPathFromHash(chainID isc.ChainID, blockHash state.BlockHash) string { return filepath.Join(constTestFolder, chainID.String(), blockWALSubFolderName(blockHash), blockWALFileName(blockHash)) } +func walPathNoSubfolderFromHash(chainID isc.ChainID, blockHash state.BlockHash) string { + return filepath.Join(constTestFolder, chainID.String(), blockWALFileName(blockHash)) +} + +func writeBlocksLegacy(t *testing.T, chainID isc.ChainID, blocks []state.Block) { + for _, block := range blocks { + filePath := walPathNoSubfolderFromHash(chainID, block.Hash()) + f, err := os.OpenFile(filePath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o666) + require.NoError(t, err) + err = block.Write(f) + require.NoError(t, err) + err = f.Close() + require.NoError(t, err) + } +} + func cleanupAfterTest(t *testing.T) { err := os.RemoveAll(constTestFolder) require.NoError(t, err) From 43fc66ca5e09aff8283c32e53c4698e44620ba04 Mon Sep 17 00:00:00 2001 From: Eric Hop Date: Sat, 12 Aug 2023 09:19:03 -0700 Subject: [PATCH 20/26] Remove useless byte counters from read/write block essence --- packages/state/block.go | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/packages/state/block.go b/packages/state/block.go index 486773dd70..69382a61fd 100644 --- a/packages/state/block.go +++ b/packages/state/block.go @@ -41,7 +41,7 @@ func (b *block) Bytes() []byte { func (b *block) essenceBytes() []byte { ww := rwutil.NewBytesWriter() - ww.WriteFromFunc(b.writeEssence) + b.writeEssence(ww) return ww.Bytes() } @@ -85,20 +85,18 @@ func (b *block) TrieRoot() trie.Hash { func (b *block) Read(r io.Reader) error { rr := rwutil.NewReader(r) rr.ReadN(b.trieRoot[:]) - rr.ReadFromFunc(b.readEssence) + b.readEssence(rr) return rr.Err } func (b *block) Write(w io.Writer) error { ww := rwutil.NewWriter(w) ww.WriteN(b.trieRoot[:]) - ww.WriteFromFunc(b.writeEssence) + b.writeEssence(ww) return ww.Err } -func (b *block) readEssence(r io.Reader) (int, error) { - rr := rwutil.NewReader(r) - counter := rwutil.NewReadCounter(rr) +func (b *block) readEssence(rr *rwutil.Reader) { b.mutations = buffered.NewMutations() rr.Read(b.mutations) hasPrevL1Commitment := rr.ReadBool() @@ -106,18 +104,14 @@ func (b *block) readEssence(r io.Reader) (int, error) { b.previousL1Commitment = new(L1Commitment) rr.Read(b.previousL1Commitment) } - return counter.Count(), rr.Err } -func (b *block) writeEssence(w io.Writer) (int, error) { - ww := rwutil.NewWriter(w) - counter := rwutil.NewWriteCounter(ww) +func (b *block) writeEssence(ww *rwutil.Writer) { ww.Write(b.mutations) ww.WriteBool(b.previousL1Commitment != nil) if b.previousL1Commitment != nil { ww.Write(b.previousL1Commitment) } - return counter.Count(), ww.Err } // test only function From 115343b9942181960b5df9a96613003434a9c3ee Mon Sep 17 00:00:00 2001 From: cwarnerdev <138500512+cwarnerdev@users.noreply.github.com> Date: Mon, 7 Aug 2023 04:58:31 -1000 Subject: [PATCH 21/26] initial iscutils prng --- tools/evm/iscutils/README.md | 40 +++++++++++++++++++++++++++++++++ tools/evm/iscutils/package.json | 24 ++++++++++++++++++++ tools/evm/iscutils/prng.sol | 39 ++++++++++++++++++++++++++++++++ 3 files changed, 103 insertions(+) create mode 100644 tools/evm/iscutils/README.md create mode 100644 tools/evm/iscutils/package.json create mode 100644 tools/evm/iscutils/prng.sol diff --git a/tools/evm/iscutils/README.md b/tools/evm/iscutils/README.md new file mode 100644 index 0000000000..512d335caf --- /dev/null +++ b/tools/evm/iscutils/README.md @@ -0,0 +1,40 @@ +# @iota/iscutils + +The iscutils package contains various utility methods to simplify the interaction with the IOTA Magic contract. This utility library is designed to be used with [@iota/iscmagic](https://www.npmjs.com/package/@iota/iscmagic/) npm package. + +The Magic contract, an EVM contract, is deployed by default on every ISC chain. It has several methods, accessed via different interfaces like ISCSandbox, ISCAccounts, ISCUtil and more. These can be utilized within any Solidity contract by importing the @iota/iscmagic library. + +For further information on the Magic contract, check the [Wiki](https://wiki.iota.org/shimmer/smart-contracts/guide/evm/magic/). + +## Installing @iota/iscutils contracts + +The @iota/iscutils contracts are installable via __NPM__ with + +```bash +npm install @iota/iscutils +``` + +After installing `@iota/iscutils` you can use the functions by importing them as you normally would. + +```solidity +pragma solidity >=0.8.5; + +import "@iota/iscmagic/ISC.sol"; +import "@iota/iscutils/prng.sol"; + +contract MyEVMContract { + using PRNG for PRNG.PRNGState; + + event PseudoRNG(uint256 value); + + PRNG.PRNGState private prngState; + + function emitValue() public { + bytes32 e = ISC.sandbox.getEntropy(); + prngState.seed(e); + uint256 random = prngState.generateRandomNumber(); + emit PseudoRNG(random); + } +} + +``` \ No newline at end of file diff --git a/tools/evm/iscutils/package.json b/tools/evm/iscutils/package.json new file mode 100644 index 0000000000..beeb2d882c --- /dev/null +++ b/tools/evm/iscutils/package.json @@ -0,0 +1,24 @@ +{ + "name": "@iota/iscutils", + "version": "0.0.0", + "description": "", + "repository": { + "type": "git", + "url": "https://github.com/iotaledger/wasp.git", + "directory": "tools/evm/iscutils" + }, + "keywords": [ + "iscutils", + "wasp", + "solidity", + "evm", + "isc", + "iota" + ], + "author": "Iota Smart Contracts", + "license": "Apache-2.0", + "bugs": { + "url": "https://github.com/iotaledger/wasp/issues" + }, + "homepage": "https://github.com/iotaledger/wasp/blob/develop/packages/vm/core/evm/README.md" +} diff --git a/tools/evm/iscutils/prng.sol b/tools/evm/iscutils/prng.sol new file mode 100644 index 0000000000..95d9af76b9 --- /dev/null +++ b/tools/evm/iscutils/prng.sol @@ -0,0 +1,39 @@ +// Copyright 2020 IOTA Stiftung +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.5; + +/// @title Pseudorandom Number Generator (PRNG) Library +/// @notice This library is used to generate pseudorandom numbers +/// @dev Not recommended for generating cryptographic secure randomness +library PRNG { + /// @notice Parameters of the PRNG (specifically, a Linear Congruential Generator) + /// @dev These are constants required for generating the random number sequence + uint256 constant LCG_MULTIPLIER = 1103515245; // Multiplier in LCG + uint256 constant LCG_INCREMENT = 12345; // Increment in LCG + uint256 constant LCG_MODULUS = 2**31; // Modulus in LCG + + /// @dev Represents the state of the PRNG + struct PRNGState { + uint256 state; + } + + /// @notice Generate a new pseudorandom number + /// @dev Uses the formula of Linear Congruential Generator (LCG): new_state = (LCG_MULTIPLIER*state + LCG_INCREMENT) mod LCG_MODULUS + /// @param self The PRNGState struct to use and alter the state + /// @return The generated pseudorandom number + function generateRandomNumber(PRNGState storage self) public returns (uint256) { + require(self.state != 0, "Generator has not been initialized, state is zero."); + self.state = (LCG_MULTIPLIER * self.state + LCG_INCREMENT) % LCG_MODULUS; + return self.state; + } + + /// @notice Seed the PRNG + /// @dev The seed should not be zero + /// @param self The PRNGState struct to update the state + /// @param entropy The seed value (entropy) + function seed(PRNGState storage self, bytes32 entropy) internal { + require(entropy != bytes32(0), "Entered entropy should not be zero"); + self.state = uint256(entropy); + } +} + From 851d348b3782e3914381293f189c327f72f4199e Mon Sep 17 00:00:00 2001 From: cwarnerdev <138500512+cwarnerdev@users.noreply.github.com> Date: Wed, 9 Aug 2023 05:27:33 -1000 Subject: [PATCH 22/26] add npmignore --- tools/evm/iscutils/.npmignore | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 tools/evm/iscutils/.npmignore diff --git a/tools/evm/iscutils/.npmignore b/tools/evm/iscutils/.npmignore new file mode 100644 index 0000000000..844b7744c3 --- /dev/null +++ b/tools/evm/iscutils/.npmignore @@ -0,0 +1,2 @@ +** +!*.sol \ No newline at end of file From dc8b3d7cf2dae87b023a46698bcbdd7bc5a6ecae Mon Sep 17 00:00:00 2001 From: cwarnerdev <138500512+cwarnerdev@users.noreply.github.com> Date: Wed, 9 Aug 2023 05:28:38 -1000 Subject: [PATCH 23/26] create reusable npm publish workflow and update release.yml to use it --- .../{publish-iscmagic.yml => publish-npm.yml} | 13 ++++++++----- .github/workflows/release.yml | 18 ++++++++++++------ 2 files changed, 20 insertions(+), 11 deletions(-) rename .github/workflows/{publish-iscmagic.yml => publish-npm.yml} (76%) diff --git a/.github/workflows/publish-iscmagic.yml b/.github/workflows/publish-npm.yml similarity index 76% rename from .github/workflows/publish-iscmagic.yml rename to .github/workflows/publish-npm.yml index 577ed4b9a7..94fd91c2c3 100644 --- a/.github/workflows/publish-iscmagic.yml +++ b/.github/workflows/publish-npm.yml @@ -1,4 +1,4 @@ -name: Publish @iota/iscmagic +name: Publish @iota NPM packages on: workflow_call: @@ -6,6 +6,9 @@ on: version: required: true type: string + workingDirectory: + required: true + type: string secrets: NPM_TOKEN: required: true @@ -15,18 +18,18 @@ jobs: runs-on: ubuntu-latest defaults: run: - working-directory: ./packages/vm/core/evm/iscmagic + working-directory: ${{ inputs.workingDirectory }} steps: - uses: actions/checkout@v3 - - + - uses: actions/setup-node@v3 with: node-version: lts/* registry-url: 'https://registry.npmjs.org' scope: iota - - + - run: npm version ${{ inputs.version }} - - + - run: npm publish --access public env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index dfc7255c35..c0a1067c3a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -115,10 +115,16 @@ jobs: build-args: | BUILD_LD_FLAGS=-X=github.com/iotaledger/wasp/components/app.Version=${{ steps.tagger.outputs.tag }} - release-iscmagic: - uses: ./.github/workflows/publish-iscmagic.yml + + release-npm-packacges: needs: release-docker - with: - version: ${{ needs.release-docker.outputs.version }} - secrets: - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + runs-on: ubuntu-latest + strategy: + matrix: + workingDirectory: ['./packages/vm/core/evm/iscmagic', './tools/evm/iscutils'] + steps: + - name: Release NPM package + uses: ./.github/workflows/publish-npm.yml + with: + version: ${{ needs.release-docker.outputs.version }} + workingDirectory: ${{ matrix.workingDirectory }} \ No newline at end of file From bfb27e7665b35bfbc1f7a2b8757ac3b3f578b77c Mon Sep 17 00:00:00 2001 From: cwarnerdev <138500512+cwarnerdev@users.noreply.github.com> Date: Wed, 9 Aug 2023 05:31:39 -1000 Subject: [PATCH 24/26] add description to pacakge.json --- tools/evm/iscutils/package.json | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tools/evm/iscutils/package.json b/tools/evm/iscutils/package.json index beeb2d882c..5ff06831a5 100644 --- a/tools/evm/iscutils/package.json +++ b/tools/evm/iscutils/package.json @@ -1,7 +1,7 @@ { "name": "@iota/iscutils", "version": "0.0.0", - "description": "", + "description": "The iscutils package contains various utility methods to simplify the interaction with the IOTA Magic contract.", "repository": { "type": "git", "url": "https://github.com/iotaledger/wasp.git", @@ -12,8 +12,7 @@ "wasp", "solidity", "evm", - "isc", - "iota" + "isc" ], "author": "Iota Smart Contracts", "license": "Apache-2.0", From bd2e6847cdf5d784b9ac868b4f0a32a1ef70e86e Mon Sep 17 00:00:00 2001 From: cwarnerdev <138500512+cwarnerdev@users.noreply.github.com> Date: Mon, 14 Aug 2023 04:55:54 -1000 Subject: [PATCH 25/26] update package readme --- tools/evm/iscutils/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/evm/iscutils/package.json b/tools/evm/iscutils/package.json index 5ff06831a5..cab237c1fa 100644 --- a/tools/evm/iscutils/package.json +++ b/tools/evm/iscutils/package.json @@ -19,5 +19,5 @@ "bugs": { "url": "https://github.com/iotaledger/wasp/issues" }, - "homepage": "https://github.com/iotaledger/wasp/blob/develop/packages/vm/core/evm/README.md" + "homepage": "https://github.com/iotaledger/wasp/blob/develop/tools/evm/iscutils/README.md" } From 7c7aa27103fe09f5f6a8403933da96716ee88c69 Mon Sep 17 00:00:00 2001 From: cwarnerdev <138500512+cwarnerdev@users.noreply.github.com> Date: Mon, 14 Aug 2023 04:56:36 -1000 Subject: [PATCH 26/26] use hashing instead of LCG --- tools/evm/iscutils/prng.sol | 25 +++++++++---------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/tools/evm/iscutils/prng.sol b/tools/evm/iscutils/prng.sol index 95d9af76b9..e546823cb2 100644 --- a/tools/evm/iscutils/prng.sol +++ b/tools/evm/iscutils/prng.sol @@ -6,25 +6,19 @@ pragma solidity >=0.8.5; /// @notice This library is used to generate pseudorandom numbers /// @dev Not recommended for generating cryptographic secure randomness library PRNG { - /// @notice Parameters of the PRNG (specifically, a Linear Congruential Generator) - /// @dev These are constants required for generating the random number sequence - uint256 constant LCG_MULTIPLIER = 1103515245; // Multiplier in LCG - uint256 constant LCG_INCREMENT = 12345; // Increment in LCG - uint256 constant LCG_MODULUS = 2**31; // Modulus in LCG - /// @dev Represents the state of the PRNG struct PRNGState { - uint256 state; + bytes32 state; } /// @notice Generate a new pseudorandom number - /// @dev Uses the formula of Linear Congruential Generator (LCG): new_state = (LCG_MULTIPLIER*state + LCG_INCREMENT) mod LCG_MODULUS + /// @dev Takes the current state, hashes it and returns the new state. /// @param self The PRNGState struct to use and alter the state /// @return The generated pseudorandom number - function generateRandomNumber(PRNGState storage self) public returns (uint256) { - require(self.state != 0, "Generator has not been initialized, state is zero."); - self.state = (LCG_MULTIPLIER * self.state + LCG_INCREMENT) % LCG_MODULUS; - return self.state; + function generateRandomNumber(PRNGState storage self) internal returns (uint256) { + require(self.state != bytes32(0), "state must be seeded first"); + self.state = keccak256(abi.encodePacked(self.state)); + return uint256(self.state); } /// @notice Seed the PRNG @@ -32,8 +26,7 @@ library PRNG { /// @param self The PRNGState struct to update the state /// @param entropy The seed value (entropy) function seed(PRNGState storage self, bytes32 entropy) internal { - require(entropy != bytes32(0), "Entered entropy should not be zero"); - self.state = uint256(entropy); + require(entropy != bytes32(0), "seed must not be zero"); + self.state = entropy; } -} - +} \ No newline at end of file