From 6a77b0efea6250537c11baeaccca516f8b1d03ae Mon Sep 17 00:00:00 2001 From: Idris Bowman <34751375+V00D00-child@users.noreply.github.com> Date: Mon, 8 Jul 2024 17:23:23 -0400 Subject: [PATCH] feat: add sqllite db connection and userop data type --- go.mod | 5 + go.sum | 12 ++ internal/data/database.go | 25 ++++ internal/data/userop.go | 279 ++++++++++++++++++++++++++++++++++++ internal/mempool/mempool.go | 1 + internal/utils/util.go | 22 ++- 6 files changed, 343 insertions(+), 1 deletion(-) create mode 100644 internal/data/database.go create mode 100644 internal/data/userop.go create mode 100644 internal/mempool/mempool.go diff --git a/go.mod b/go.mod index 73dadb3..4411f92 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,8 @@ require ( github.com/tyler-smith/go-bip32 v1.0.0 github.com/tyler-smith/go-bip39 v1.1.0 github.com/urfave/cli/v2 v2.27.2 + gorm.io/driver/sqlite v1.5.6 + gorm.io/gorm v1.25.10 ) require ( @@ -49,11 +51,14 @@ require ( github.com/google/uuid v1.3.0 // indirect github.com/gorilla/websocket v1.4.2 // indirect github.com/holiman/uint256 v1.2.4 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.2.7 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-sqlite3 v1.14.22 // indirect github.com/mmcloughlin/addchain v0.4.0 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/term v0.5.0 // indirect diff --git a/go.sum b/go.sum index 07249d2..9fda62d 100644 --- a/go.sum +++ b/go.sum @@ -130,6 +130,8 @@ github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb/go.mod h1:/XxbfmMg github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -149,6 +151,10 @@ github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc= github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= @@ -177,6 +183,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI= github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= @@ -355,6 +363,10 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/sqlite v1.5.6 h1:fO/X46qn5NUEEOZtnjJRWRzZMe8nqJiQ9E+0hi+hKQE= +gorm.io/driver/sqlite v1.5.6/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4= +gorm.io/gorm v1.25.10 h1:dQpO+33KalOA+aFYGlK+EfxcI5MbO7EP2yYygwh9h+s= +gorm.io/gorm v1.25.10/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= launchpad.net/gocheck v0.0.0-20140225173054-000000000087 h1:Izowp2XBH6Ya6rv+hqbceQyw/gSGoXfH/UPoTGduL54= diff --git a/internal/data/database.go b/internal/data/database.go new file mode 100644 index 0000000..8cc9f29 --- /dev/null +++ b/internal/data/database.go @@ -0,0 +1,25 @@ +package data + +import ( + "gorm.io/driver/sqlite" + "gorm.io/gorm" +) + +func OpenConnection(isInMemory bool) (*gorm.DB, error) { + var db *gorm.DB + var err error + + if isInMemory { + db, err = gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) + } else { + db, err = gorm.Open(sqlite.Open("betsy.db"), &gorm.Config{}) + } + + if err != nil { + return nil, err + } + + db.AutoMigrate(&UserOpV7Hexify{}) + + return db, nil +} diff --git a/internal/data/userop.go b/internal/data/userop.go new file mode 100644 index 0000000..b43ab1b --- /dev/null +++ b/internal/data/userop.go @@ -0,0 +1,279 @@ +package data + +import ( + "errors" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/transeptorlabs/betsy/contracts/entrypoint" + "github.com/transeptorlabs/betsy/internal/utils" + "gorm.io/gorm" +) + +// UserOpV7Hexify is a struct used to store user operations in a database. Contains all EIP-4337 with all hex fields. +type UserOpV7Hexify struct { + gorm.Model // Embedding gorm.Model adds fields ID(primaryKey), CreatedAt, UpdatedAt, DeletedAt + UserOpHash string `gorm:"index;unique;not null"` + Sender string `json:"sender" mapstructure:"sender" validate:"required" gorm:"not null"` + Nonce string `json:"nonce" mapstructure:"nonce" validate:"required" gorm:"not null"` + + // (optional) + Factory string `json:"factory" mapstructure:"factory" validate:"required"` + FactoryData string `json:"factoryData" mapstructure:"factoryData"` + + CallData string `json:"callData" mapstructure:"callData" validate:"required" gorm:"not null"` + CallGasLimit string `json:"callGasLimit" mapstructure:"callGasLimit" validate:"required" gorm:"not null"` + VerificationGasLimit string `json:"verificationGasLimit" mapstructure:"verificationGasLimit" validate:"required" gorm:"not null"` + PreVerificationGas string `json:"preVerificationGas" mapstructure:"preVerificationGas" validate:"required" gorm:"not null" ` + MaxFeePerGas string `json:"maxFeePerGas" mapstructure:"maxFeePerGas" validate:"required" gorm:"not null"` + MaxPriorityFeePerGas string `json:"maxPriorityFeePerGas" mapstructure:"maxPriorityFeePerGas" validate:"required" gorm:"not null"` + + // (optional) + Paymaster string `json:"paymaster" mapstructure:"paymaster" validate:"required"` + PaymasterVerificationGasLimit string `json:"paymasterVerificationGasLimit" mapstructure:"paymasterVerificationGasLimit"` + PaymasterPostOpGasLimit string `json:"paymasterPostOpGasLimit" mapstructure:"paymasterPostOpGasLimit"` + PaymasterData string `json:"paymasterAndData" mapstructure:"paymasterAndData"` + + Signature string `gorm:"not null" json:"signature" mapstructure:"signature"` +} + +func (op *UserOpV7Hexify) BeforeCreate(tx *gorm.DB) (err error) { + ok, invalidAddress := op.IsValidAddresses() + if !ok { + return errors.New("can't save invalid eth address:" + invalidAddress) + } + + err = op.SetUserOpHash() + if err != nil { + return err + } + + return nil +} + +func (op *UserOpV7Hexify) IsValidAddresses() (bool, string) { + isSenderAddress := common.IsHexAddress(op.Sender) + if !isSenderAddress { + return isSenderAddress, op.Sender + } + + return true, "" +} + +func (op *UserOpV7Hexify) SetUserOpHash() error { + op.UserOpHash = "" + return nil +} + +func (op *UserOpV7Hexify) GetInitCode() ([]byte, error) { + if op.Factory == "0x" { + return []byte{}, nil + } + + if op.FactoryData == "0x" { + return nil, errors.New("Got Factory but missing FactoryData") + } + + factoryDecoded, err := hexutil.Decode(op.Factory) + if err != nil { + return nil, errors.New("factory (bytes) conversion failed") + } + + factoryDataDecoded, err := hexutil.Decode(op.FactoryData) + if err != nil { + return nil, errors.New("factoryData (bytes) conversion failed") + } + + concatenatedBytes := append(factoryDecoded, factoryDataDecoded...) + + return concatenatedBytes, nil +} + +func (op *UserOpV7Hexify) GetAccountGasLimits() ([32]byte, error) { + verificationGasLimitDecoded, err := hexutil.Decode(op.VerificationGasLimit) + if err != nil { + return [32]byte{}, errors.New("verificationGasLimit (bytes) conversion failed") + } + + callGasLimitDecoded, err := hexutil.Decode(op.CallGasLimit) + if err != nil { + return [32]byte{}, errors.New("callGasLimit (bytes) conversion failed") + } + + // Truncate if longer than 16 bytes or Pad with leading zeros if shorter than 16 bytes + verificationGasLimitDecodedPadded, err := utils.PadToBytes16(verificationGasLimitDecoded) + if err != nil { + return [32]byte{}, err + } + + callGasLimitDecodedPadded, err := utils.PadToBytes16(callGasLimitDecoded) + if err != nil { + return [32]byte{}, err + } + + // Concatenate the byte slices + concatenatedBytes := append(verificationGasLimitDecodedPadded, callGasLimitDecodedPadded...) + + if len(concatenatedBytes) != 32 { + return [32]byte{}, errors.New("concatenatedBytes(verificationGasLimitDecodedPadded, callGasLimitDecodedPadded) is not equal to 32 bytes") + } + + // Convert concatenatedBytes to [32]byte + var result [32]byte + copy(result[:], concatenatedBytes) + + return result, nil +} + +func (op *UserOpV7Hexify) GasFees() ([32]byte, error) { + maxPriorityFeePerGasDecoded, err := hexutil.Decode(op.MaxPriorityFeePerGas) + if err != nil { + return [32]byte{}, errors.New("maxPriorityFeePerGas (bytes) conversion failed") + } + + maxFeePerGasDecoded, err := hexutil.Decode(op.MaxFeePerGas) + if err != nil { + return [32]byte{}, errors.New("maxFeePerGas (bytes) conversion failed") + } + + // Truncate if longer than 16 bytes or Pad with leading zeros if shorter than 16 bytes + maxPriorityFeePerGasDecodedPadded, err := utils.PadToBytes16(maxPriorityFeePerGasDecoded) + if err != nil { + return [32]byte{}, err + } + + maxFeePerGasDecodedPadded, err := utils.PadToBytes16(maxFeePerGasDecoded) + if err != nil { + return [32]byte{}, err + } + + // Concatenate the byte slices + concatenatedBytes := append(maxPriorityFeePerGasDecodedPadded, maxFeePerGasDecodedPadded...) + + if len(concatenatedBytes) != 32 { + return [32]byte{}, errors.New("concatenatedBytes(maxPriorityFeePerGasDecodedPadded, maxFeePerGasDecodedPadded) is not equal to 32 bytes") + } + + // Convert concatenatedBytes to [32]byte + var result [32]byte + copy(result[:], concatenatedBytes) + + return result, nil +} + +func (op *UserOpV7Hexify) GetPaymasterAndData() ([]byte, error) { + if op.Paymaster == "0x" { + return []byte{}, nil + } + + if op.PreVerificationGas == "0x" { + return nil, errors.New("Got Paymaster but missing PreVerificationGas") + } + + if op.PaymasterPostOpGasLimit == "0x" { + return nil, errors.New("Got Paymaster but missing PaymasterPostOpGasLimit") + } + + // Decode all paymaster values + paymasterDecoded, err := hexutil.Decode(op.Paymaster) + if err != nil { + return nil, errors.New("paymasterDecoded (bytes) conversion failed") + } + + preVerificationGasDecoded, err := hexutil.Decode(op.PreVerificationGas) + if err != nil { + return []byte{}, errors.New("preVerificationGas (bytes) conversion failed") + } + + paymasterPostOpGasLimitDecoded, err := hexutil.Decode(op.PaymasterPostOpGasLimit) + if err != nil { + return []byte{}, errors.New("paymasterPostOpGasLimit (bytes) conversion failed") + } + + // Truncate if longer than 16 bytes or Pad with leading zeros if shorter than 16 bytes + preVerificationGasDecodedPadded, err := utils.PadToBytes16(preVerificationGasDecoded) + if err != nil { + return []byte{}, err + } + + paymasterPostOpGasLimitDecodedPadded, err := utils.PadToBytes16(paymasterPostOpGasLimitDecoded) + if err != nil { + return []byte{}, err + } + concatenatedGasBytes := append(preVerificationGasDecodedPadded, paymasterPostOpGasLimitDecodedPadded...) + + // Concatenate the byte slices + concatenatedPaymasterBytes := append( + paymasterDecoded, + concatenatedGasBytes..., + ) + + var paymasterDataDecoded []byte + if op.PaymasterData == "0x" { + paymasterDataDecoded = []byte{} + } else { + paymasterDataDecoded, err = hexutil.Decode(op.PaymasterData) + if err != nil { + return nil, errors.New("paymasterData (bytes) conversion failed") + } + } + + return append( + concatenatedPaymasterBytes, + paymasterDataDecoded..., + ), nil +} + +func (op *UserOpV7Hexify) PackUserOp() (*entrypoint.PackedUserOperation, error) { + nonceDecoded, err := hexutil.DecodeBig(op.Nonce) + if err != nil { + return nil, errors.New("nonce (bigInt) conversion failed") + } + + initCodeDecoded, err := op.GetInitCode() + if err != nil { + return nil, errors.New("nonce (bigInt) conversion failed") + } + + callDataDecoded, err := hexutil.Decode(op.CallData) + if err != nil { + return nil, errors.New("calldata (bytes) conversion failed") + } + + preVerificationGaseDecoded, err := hexutil.DecodeBig(op.PreVerificationGas) + if err != nil { + return nil, errors.New("preVerificationGas (bigInt) conversion failed") + } + + accountGasLimitsDecoded, err := op.GetAccountGasLimits() + if err != nil { + return nil, errors.New("accountGasLimit (bytes 32) conversion failed") + } + + gasFeesDecoded, err := op.GasFees() + if err != nil { + return nil, errors.New("gasFees (bytes 32) conversion failed") + } + + paymasterAndDataDecoded, err := op.GetPaymasterAndData() + if err != nil { + return nil, errors.New("paymasterAndData (bytes) conversion failed") + } + + signatureDecoded, err := hexutil.Decode(op.Signature) + if err != nil { + return nil, errors.New("signature (bytes) conversion failed") + } + + return &entrypoint.PackedUserOperation{ + Sender: common.HexToAddress(op.Sender), + Nonce: nonceDecoded, + InitCode: initCodeDecoded, + CallData: callDataDecoded, + AccountGasLimits: accountGasLimitsDecoded, + PreVerificationGas: preVerificationGaseDecoded, + GasFees: gasFeesDecoded, + PaymasterAndData: paymasterAndDataDecoded, + Signature: signatureDecoded, + }, nil +} diff --git a/internal/mempool/mempool.go b/internal/mempool/mempool.go new file mode 100644 index 0000000..6a26ef9 --- /dev/null +++ b/internal/mempool/mempool.go @@ -0,0 +1 @@ +package mempool diff --git a/internal/utils/util.go b/internal/utils/util.go index 6e4735d..4d85e29 100644 --- a/internal/utils/util.go +++ b/internal/utils/util.go @@ -1,6 +1,9 @@ package utils -import "os" +import ( + "errors" + "os" +) // RemoveFile removes a file from the filesystem func RemoveFile(file string) error { @@ -17,3 +20,20 @@ func RemoveDevWallets() error { } return nil } + +// PadToBytes16 takes []byte and pads to [16]byte +func PadToBytes16(data []byte) ([]byte, error) { + if len(data) > 16 { + data = data[:16] + } else if len(data) < 16 { + padded := make([]byte, 16) + copy(padded[16-len(data):], data) + data = padded + } + + if len(data) != 16 { + return []byte{}, errors.New("padToBytes16(data) is not equal to 16 bytes") + } + + return data, nil +}