diff --git a/cmd/debugger/main.go b/cmd/debugger/main.go new file mode 100644 index 00000000..73c80ce4 --- /dev/null +++ b/cmd/debugger/main.go @@ -0,0 +1,134 @@ +package main + +import ( + "context" + "fmt" + "github.com/Layr-Labs/go-sidecar/internal/clients/ethereum" + "github.com/Layr-Labs/go-sidecar/internal/clients/etherscan" + "github.com/Layr-Labs/go-sidecar/internal/config" + "github.com/Layr-Labs/go-sidecar/internal/contractManager" + "github.com/Layr-Labs/go-sidecar/internal/contractStore/sqliteContractStore" + "github.com/Layr-Labs/go-sidecar/internal/eigenState/avsOperators" + "github.com/Layr-Labs/go-sidecar/internal/eigenState/operatorShares" + "github.com/Layr-Labs/go-sidecar/internal/eigenState/stakerDelegations" + "github.com/Layr-Labs/go-sidecar/internal/eigenState/stakerShares" + "github.com/Layr-Labs/go-sidecar/internal/eigenState/stateManager" + "github.com/Layr-Labs/go-sidecar/internal/fetcher" + "github.com/Layr-Labs/go-sidecar/internal/indexer" + "github.com/Layr-Labs/go-sidecar/internal/logger" + "github.com/Layr-Labs/go-sidecar/internal/metrics" + "github.com/Layr-Labs/go-sidecar/internal/pipeline" + "github.com/Layr-Labs/go-sidecar/internal/sidecar" + "github.com/Layr-Labs/go-sidecar/internal/sqlite" + "github.com/Layr-Labs/go-sidecar/internal/sqlite/migrations" + sqliteBlockStore "github.com/Layr-Labs/go-sidecar/internal/storage/sqlite" + "go.uber.org/zap" + "log" +) + +func main() { + ctx := context.Background() + cfg := config.NewConfig() + + fmt.Printf("Config: %+v\n", cfg) + + l, _ := logger.NewLogger(&logger.LoggerConfig{Debug: cfg.Debug}) + + sdc, err := metrics.InitStatsdClient(cfg.StatsdUrl) + if err != nil { + l.Sugar().Fatal("Failed to setup statsd client", zap.Error(err)) + } + + etherscanClient := etherscan.NewEtherscanClient(cfg, l) + client := ethereum.NewClient(cfg.EthereumRpcConfig.BaseUrl, l) + + db := sqlite.NewSqlite(cfg.SqliteConfig.GetSqlitePath()) + + grm, err := sqlite.NewGormSqliteFromSqlite(db) + if err != nil { + l.Error("Failed to create gorm instance", zap.Error(err)) + panic(err) + } + + migrator := migrations.NewSqliteMigrator(grm, l) + if err = migrator.MigrateAll(); err != nil { + log.Fatalf("Failed to migrate: %v", err) + } + + contractStore := sqliteContractStore.NewSqliteContractStore(grm, l, cfg) + if err := contractStore.InitializeCoreContracts(); err != nil { + log.Fatalf("Failed to initialize core contracts: %v", err) + } + + cm := contractManager.NewContractManager(contractStore, etherscanClient, client, sdc, l) + + mds := sqliteBlockStore.NewSqliteBlockStore(grm, l, cfg) + if err != nil { + log.Fatalln(err) + } + + sm := stateManager.NewEigenStateManager(l, grm) + + if _, err := avsOperators.NewAvsOperators(sm, grm, cfg.Network, cfg.Environment, l, cfg); err != nil { + l.Sugar().Fatalw("Failed to create AvsOperatorsModel", zap.Error(err)) + } + if _, err := operatorShares.NewOperatorSharesModel(sm, grm, cfg.Network, cfg.Environment, l, cfg); err != nil { + l.Sugar().Fatalw("Failed to create OperatorSharesModel", zap.Error(err)) + } + if _, err := stakerDelegations.NewStakerDelegationsModel(sm, grm, cfg.Network, cfg.Environment, l, cfg); err != nil { + l.Sugar().Fatalw("Failed to create StakerDelegationsModel", zap.Error(err)) + } + if _, err := stakerShares.NewStakerSharesModel(sm, grm, cfg.Network, cfg.Environment, l, cfg); err != nil { + l.Sugar().Fatalw("Failed to create StakerSharesModel", zap.Error(err)) + } + + fetchr := fetcher.NewFetcher(client, cfg, l) + + idxr := indexer.NewIndexer(mds, contractStore, etherscanClient, cm, client, fetchr, l, cfg) + + p := pipeline.NewPipeline(fetchr, idxr, mds, sm, l) + + // Create new sidecar instance + sidecar := sidecar.NewSidecar(&sidecar.SidecarConfig{ + GenesisBlockNumber: cfg.GetGenesisBlockNumber(), + }, cfg, mds, p, sm, l, client) + + // RPC channel to notify the RPC server to shutdown gracefully + rpcChannel := make(chan bool) + err = sidecar.WithRpcServer(ctx, mds, sm, rpcChannel) + if err != nil { + l.Sugar().Fatalw("Failed to start RPC server", zap.Error(err)) + } + + block, err := fetchr.FetchBlock(ctx, 1215893) + if err != nil { + l.Sugar().Fatalw("Failed to fetch block", zap.Error(err)) + } + + transactionHash := "0xf6775c38af1d2802bcbc2b7c8959c0d5b48c63a14bfeda0261ba29d76c68c423" + transaction := ðereum.EthereumTransaction{} + + for _, tx := range block.Block.Transactions { + if tx.Hash.Value() == transactionHash { + transaction = tx + break + } + } + + logIndex := 4 + receipt := block.TxReceipts[transaction.Hash.Value()] + var interestingLog *ethereum.EthereumEventLog + + for _, log := range receipt.Logs { + if log.LogIndex.Value() == uint64(logIndex) { + fmt.Printf("Log: %+v\n", log) + interestingLog = log + } + } + + decodedLog, err := idxr.DecodeLogWithAbi(nil, receipt, interestingLog) + if err != nil { + l.Sugar().Fatalw("Failed to decode log", zap.Error(err)) + } + l.Sugar().Infof("Decoded log: %+v", decodedLog) +} diff --git a/go.mod b/go.mod index 2f98fef6..b9f700dd 100644 --- a/go.mod +++ b/go.mod @@ -46,7 +46,7 @@ require ( github.com/klauspost/compress v1.17.9 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-isatty v0.0.19 // indirect - github.com/mattn/go-sqlite3 v1.14.22 // indirect + github.com/mattn/go-sqlite3 v1.14.23 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mmcloughlin/addchain v0.4.0 // indirect github.com/pkg/errors v0.9.1 // indirect diff --git a/go.sum b/go.sum index 3b9403fc..05724e40 100644 --- a/go.sum +++ b/go.sum @@ -159,6 +159,8 @@ github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4 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/mattn/go-sqlite3 v1.14.23 h1:gbShiuAP1W5j9UOksQ06aiiqPMxYecovVGwmTxWtuw0= +github.com/mattn/go-sqlite3 v1.14.23/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.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= diff --git a/internal/eigenState/operatorShares/operatorShares.go b/internal/eigenState/operatorShares/operatorShares.go index 79ec0b60..6c10117d 100644 --- a/internal/eigenState/operatorShares/operatorShares.go +++ b/internal/eigenState/operatorShares/operatorShares.go @@ -9,8 +9,8 @@ import ( "github.com/Layr-Labs/go-sidecar/internal/eigenState/stateManager" "github.com/Layr-Labs/go-sidecar/internal/eigenState/types" "github.com/Layr-Labs/go-sidecar/internal/storage" + "github.com/Layr-Labs/go-sidecar/internal/types/numbers" "github.com/Layr-Labs/go-sidecar/internal/utils" - "github.com/holiman/uint256" "github.com/wealdtech/go-merkletree/v2" "github.com/wealdtech/go-merkletree/v2/keccak256" orderedmap "github.com/wk8/go-ordered-map/v2" @@ -18,6 +18,7 @@ import ( "golang.org/x/xerrors" "gorm.io/gorm" "gorm.io/gorm/clause" + "math/big" "slices" "sort" "strings" @@ -37,14 +38,15 @@ type OperatorShares struct { type AccumulatedStateChange struct { Operator string Strategy string - Shares *uint256.Int + Shares *big.Int BlockNumber uint64 + IsNegative bool } type OperatorSharesDiff struct { Operator string Strategy string - Shares *uint256.Int + Shares *big.Int BlockNumber uint64 IsNew bool } @@ -136,21 +138,21 @@ func (osm *OperatorSharesModel) GetStateTransitions() (types.StateTransitions[Ac operator := strings.ToLower(arguments[0].Value.(string)) sharesStr := outputData.Shares.String() - shares, err := uint256.FromDecimal(sharesStr) - if err != nil { - osm.logger.Sugar().Errorw("Failed to convert shares to uint256", - zap.Error(err), + shares, success := numbers.NewBig257().SetString(sharesStr, 10) + if !success { + osm.logger.Sugar().Errorw("Failed to convert shares to big.Int", zap.String("shares", sharesStr), zap.String("transactionHash", log.TransactionHash), zap.Uint64("transactionIndex", log.TransactionIndex), zap.Uint64("blockNumber", log.BlockNumber), ) - return nil, xerrors.Errorf("Failed to convert shares to uint256: %s", sharesStr) + return nil, xerrors.Errorf("Failed to convert shares to big.Int: %s", sharesStr) } + isNegative := false // All shares are emitted as ABS(shares), so we need to negate the shares if the event is a decrease if log.EventName == "OperatorSharesDecreased" { - shares = shares.Neg(shares) + isNegative = true } slotId := NewSlotId(operator, outputData.Strategy) @@ -161,10 +163,15 @@ func (osm *OperatorSharesModel) GetStateTransitions() (types.StateTransitions[Ac Strategy: outputData.Strategy, Shares: shares, BlockNumber: log.BlockNumber, + IsNegative: isNegative, } osm.stateAccumulator[log.BlockNumber][slotId] = record } else { - record.Shares = record.Shares.Add(record.Shares, shares) + if isNegative { + record.Shares = record.Shares.Sub(record.Shares, shares) + } else { + record.Shares = record.Shares.Add(record.Shares, shares) + } } return record, nil @@ -305,9 +312,9 @@ func (osm *OperatorSharesModel) prepareState(blockNumber uint64) ([]OperatorShar } if existingRecord, ok := mappedRecords[slotId]; ok { - existingShares, err := uint256.FromDecimal(existingRecord.Shares) - if err != nil { - osm.logger.Sugar().Errorw("Failed to convert existing shares to uint256", zap.Error(err)) + existingShares, success := numbers.NewBig257().SetString(existingRecord.Shares, 10) + if !success { + osm.logger.Sugar().Errorw("Failed to convert existing shares to big.Int") continue } prepared.Shares = existingShares.Add(existingShares, newState.Shares) diff --git a/internal/eigenState/operatorShares/operatorShares_test.go b/internal/eigenState/operatorShares/operatorShares_test.go index 8e18a4db..36444eff 100644 --- a/internal/eigenState/operatorShares/operatorShares_test.go +++ b/internal/eigenState/operatorShares/operatorShares_test.go @@ -2,7 +2,6 @@ package operatorShares import ( "database/sql" - "fmt" "github.com/Layr-Labs/go-sidecar/internal/config" "github.com/Layr-Labs/go-sidecar/internal/eigenState/stateManager" "github.com/Layr-Labs/go-sidecar/internal/logger" @@ -13,6 +12,7 @@ import ( "go.uber.org/zap" "gorm.io/gorm" "math/big" + "strings" "testing" "time" ) @@ -129,7 +129,43 @@ func Test_OperatorSharesState(t *testing.T) { stateRoot, err := model.GenerateStateRoot(blockNumber) assert.Nil(t, err) - fmt.Printf("StateRoot: %s\n", stateRoot) + assert.True(t, len(stateRoot) > 0) + + teardown(model) + }) + t.Run("Should handle state transition for operator shares decreased", func(t *testing.T) { + esm := stateManager.NewEigenStateManager(l, grm) + blockNumber := uint64(200) + log := storage.TransactionLog{ + TransactionHash: "some hash", + TransactionIndex: big.NewInt(100).Uint64(), + BlockNumber: blockNumber, + Address: cfg.GetContractsMapForEnvAndNetwork().DelegationManager, + Arguments: `[{"Name": "operator", "Type": "address", "Value": "0x32f766cf7BC7dEE7F65573587BECd7AdB2a5CC7f"}, {"Name": "staker", "Type": "address", "Value": ""}, {"Name": "strategy", "Type": "address", "Value": ""}, {"Name": "shares", "Type": "uint256", "Value": ""}]`, + EventName: "OperatorSharesDecreased", + LogIndex: big.NewInt(400).Uint64(), + OutputData: `{"shares": 1670000000000000000000, "staker": "0x32f766cf7bc7dee7f65573587becd7adb2a5cc7f", "strategy": "0x80528d6e9a2babfc766965e0e26d5ab08d9cfaf9"}`, + CreatedAt: time.Time{}, + UpdatedAt: time.Time{}, + DeletedAt: time.Time{}, + } + + model, err := NewOperatorSharesModel(esm, grm, cfg.Network, cfg.Environment, l, cfg) + assert.Nil(t, err) + + err = model.InitBlockProcessing(blockNumber) + assert.Nil(t, err) + + stateChange, err := model.HandleStateChange(&log) + assert.Nil(t, err) + assert.NotNil(t, stateChange) + + stateChangeTyped := stateChange.(*AccumulatedStateChange) + + assert.Equal(t, "1670000000000000000000", stateChangeTyped.Shares.String()) + assert.Equal(t, true, stateChangeTyped.IsNegative) + assert.Equal(t, strings.ToLower("0x32f766cf7BC7dEE7F65573587BECd7AdB2a5CC7f"), stateChangeTyped.Operator) + assert.Equal(t, "0x80528d6e9a2babfc766965e0e26d5ab08d9cfaf9", stateChangeTyped.Strategy) teardown(model) }) diff --git a/internal/eigenState/stakerShares/stakerShares.go b/internal/eigenState/stakerShares/stakerShares.go index 2bdbec3c..16f4e22e 100644 --- a/internal/eigenState/stakerShares/stakerShares.go +++ b/internal/eigenState/stakerShares/stakerShares.go @@ -2,6 +2,7 @@ package stakerShares import ( "database/sql" + "encoding/hex" "encoding/json" "fmt" "github.com/Layr-Labs/go-sidecar/internal/config" @@ -9,8 +10,8 @@ import ( "github.com/Layr-Labs/go-sidecar/internal/eigenState/stateManager" "github.com/Layr-Labs/go-sidecar/internal/eigenState/types" "github.com/Layr-Labs/go-sidecar/internal/storage" + "github.com/Layr-Labs/go-sidecar/internal/types/numbers" "github.com/Layr-Labs/go-sidecar/internal/utils" - "github.com/holiman/uint256" "github.com/wealdtech/go-merkletree/v2" "github.com/wealdtech/go-merkletree/v2/keccak256" orderedmap "github.com/wk8/go-ordered-map/v2" @@ -18,6 +19,7 @@ import ( "golang.org/x/xerrors" "gorm.io/gorm" "gorm.io/gorm/clause" + "math/big" "slices" "sort" "strings" @@ -35,15 +37,14 @@ type StakerShares struct { type AccumulatedStateChange struct { Staker string Strategy string - Shares *uint256.Int + Shares *big.Int BlockNumber uint64 - IsNegative bool } type StakerSharesDiff struct { Staker string Strategy string - Shares *uint256.Int + Shares *big.Int BlockNumber uint64 IsNew bool } @@ -137,9 +138,9 @@ func (ss *StakerSharesModel) handleStakerDepositEvent(log *storage.TransactionLo return nil, xerrors.Errorf("No staker address found in event") } - shares, err := uint256.FromDecimal(outputData.Shares.String()) - if err != nil { - return nil, xerrors.Errorf("Failed to convert shares to uint256: %s", outputData.Shares) + shares, success := numbers.NewBig257().SetString(outputData.Shares.String(), 10) + if !success { + return nil, xerrors.Errorf("Failed to convert shares to big.Int: %s", outputData.Shares) } return &AccumulatedStateChange{ @@ -180,9 +181,9 @@ func (ss *StakerSharesModel) handlePodSharesUpdatedEvent(log *storage.Transactio sharesDeltaStr := outputData.SharesDelta.String() - sharesDelta, err := uint256.FromDecimal(sharesDeltaStr) - if err != nil { - return nil, xerrors.Errorf("Failed to convert shares to uint256: %s", sharesDelta) + sharesDelta, success := numbers.NewBig257().SetString(sharesDeltaStr, 10) + if !success { + return nil, xerrors.Errorf("Failed to convert shares to big.Int: %s", sharesDelta) } return &AccumulatedStateChange{ @@ -211,30 +212,162 @@ func (ss *StakerSharesModel) handleM1StakerWithdrawals(log *storage.TransactionL return nil, xerrors.Errorf("No staker address found in event") } - shares, err := uint256.FromDecimal(outputData.Shares.String()) - if err != nil { - return nil, xerrors.Errorf("Failed to convert shares to uint256: %s", outputData.Shares) + shares, success := numbers.NewBig257().SetString(outputData.Shares.String(), 10) + if !success { + return nil, xerrors.Errorf("Failed to convert shares to big.Int: %s", outputData.Shares) } return &AccumulatedStateChange{ Staker: stakerAddress, Strategy: outputData.Strategy, - Shares: shares, + Shares: shares.Mul(shares, big.NewInt(-1)), BlockNumber: log.BlockNumber, - IsNegative: true, }, nil } -func (ss *StakerSharesModel) handleM2StakerWithdrawals(log *storage.TransactionLog) (*AccumulatedStateChange, error) { - // TODO(seanmcgary): come back to this... - return nil, nil +type m2MigrationOutputData struct { + OldWithdrawalRoot []byte `json:"oldWithdrawalRoot"` + OldWithdrawalRootString string } -func (ss *StakerSharesModel) GetStateTransitions() (types.StateTransitions[AccumulatedStateChange], []uint64) { - stateChanges := make(types.StateTransitions[AccumulatedStateChange]) +func parseLogOutputForM2MigrationEvent(outputDataStr string) (*m2MigrationOutputData, error) { + outputData := &m2MigrationOutputData{} + decoder := json.NewDecoder(strings.NewReader(outputDataStr)) + decoder.UseNumber() - stateChanges[0] = func(log *storage.TransactionLog) (*AccumulatedStateChange, error) { - var parsedRecord *AccumulatedStateChange + err := decoder.Decode(&outputData) + if err != nil { + return nil, err + } + outputData.OldWithdrawalRootString = hex.EncodeToString(outputData.OldWithdrawalRoot) + return outputData, err +} + +// handleMigratedM2StakerWithdrawals handles the WithdrawalMigrated event from the DelegationManager contract +// +// Since we have already counted M1 withdrawals due to processing events block-by-block, we need to handle not double subtracting. +// Assuming that M2 WithdrawalQueued events always result in a subtraction, if we encounter a migration event, we need +// to add the amount back to the shares to get the correct final state. +func (ss *StakerSharesModel) handleMigratedM2StakerWithdrawals(log *storage.TransactionLog) ([]*AccumulatedStateChange, error) { + outputData, err := parseLogOutputForM2MigrationEvent(log.OutputData) + if err != nil { + return nil, err + } + query := ` + with migration as ( + select + json_extract(tl.output_data, '$.nonce') as nonce, + coalesce(json_extract(tl.output_data, '$.depositor'), json_extract(tl.output_data, '$.staker')) as staker + from transaction_logs tl + where + tl.address = @strategyManagerAddress + and tl.block_number <= @logBlockNumber + and tl.event_name = 'WithdrawalQueued' + and bytes_to_hex(json_extract(tl.output_data, '$.withdrawalRoot')) = @oldWithdrawalRoot + ), + share_withdrawal_queued as ( + select + tl.*, + json_extract(tl.output_data, '$.nonce') as nonce, + coalesce(json_extract(tl.output_data, '$.depositor'), json_extract(tl.output_data, '$.staker')) as staker + from transaction_logs as tl + where + tl.address = @strategyManagerAddress + and tl.event_name = 'ShareWithdrawalQueued' + ) + select + * + from share_withdrawal_queued + where + nonce = (select nonce from migration) + and staker = (select staker from migration) + ` + logs := make([]storage.TransactionLog, 0) + res := ss.Db. + Raw(query, + sql.Named("strategyManagerAddress", ss.globalConfig.GetContractsMapForEnvAndNetwork().StrategyManager), + sql.Named("logBlockNumber", log.BlockNumber), + sql.Named("oldWithdrawalRoot", outputData.OldWithdrawalRootString), + ). + Scan(&logs) + + if res.Error != nil { + ss.logger.Sugar().Errorw("Failed to fetch share withdrawal queued logs", zap.Error(res.Error)) + return nil, res.Error + } + + changes := make([]*AccumulatedStateChange, 0) + for _, l := range logs { + c, err := ss.handleStakerDepositEvent(&l) + if err != nil { + return nil, err + } + changes = append(changes, c) + } + + return changes, nil +} + +type m2WithdrawalOutputData struct { + Withdrawal struct { + Nonce int `json:"nonce"` + Shares []json.Number `json:"shares"` + Staker string `json:"staker"` + StartBlock uint64 `json:"startBlock"` + Strategies []string `json:"strategies"` + } `json:"withdrawal"` + WithdrawalRoot []byte `json:"withdrawalRoot"` + WithdrawalRootString string +} + +func parseLogOutputForM2WithdrawalEvent(outputDataStr string) (*m2WithdrawalOutputData, error) { + outputData := &m2WithdrawalOutputData{} + decoder := json.NewDecoder(strings.NewReader(outputDataStr)) + decoder.UseNumber() + + err := decoder.Decode(&outputData) + if err != nil { + return nil, err + } + outputData.Withdrawal.Staker = strings.ToLower(outputData.Withdrawal.Staker) + outputData.WithdrawalRootString = hex.EncodeToString(outputData.WithdrawalRoot) + return outputData, err +} + +// handleM2QueuedWithdrawal handles the WithdrawalQueued event from the DelegationManager contract for M2 +func (ss *StakerSharesModel) handleM2QueuedWithdrawal(log *storage.TransactionLog) ([]*AccumulatedStateChange, error) { + outputData, err := parseLogOutputForM2WithdrawalEvent(log.OutputData) + if err != nil { + return nil, err + } + + records := make([]*AccumulatedStateChange, 0) + + for i, strategy := range outputData.Withdrawal.Strategies { + shares, success := numbers.NewBig257().SetString(outputData.Withdrawal.Shares[i].String(), 10) + if !success { + return nil, xerrors.Errorf("Failed to convert shares to big.Int: %s", outputData.Withdrawal.Shares[i]) + } + r := &AccumulatedStateChange{ + Staker: outputData.Withdrawal.Staker, + Strategy: strategy, + Shares: shares.Mul(shares, big.NewInt(-1)), + BlockNumber: log.BlockNumber, + } + records = append(records, r) + } + return records, nil +} + +type AccumulatedStateChanges struct { + Changes []*AccumulatedStateChange +} + +func (ss *StakerSharesModel) GetStateTransitions() (types.StateTransitions[AccumulatedStateChanges], []uint64) { + stateChanges := make(types.StateTransitions[AccumulatedStateChanges]) + + stateChanges[0] = func(log *storage.TransactionLog) (*AccumulatedStateChanges, error) { + var parsedRecords []*AccumulatedStateChange var err error contractAddresses := ss.globalConfig.GetContractsMapForEnvAndNetwork() @@ -242,13 +375,30 @@ func (ss *StakerSharesModel) GetStateTransitions() (types.StateTransitions[Accum // Staker shares is a bit more complex and has 4 possible contract/event combinations // that we need to handle if log.Address == contractAddresses.StrategyManager && log.EventName == "Deposit" { - parsedRecord, err = ss.handleStakerDepositEvent(log) + record, err := ss.handleStakerDepositEvent(log) + if err == nil { + parsedRecords = append(parsedRecords, record) + } } else if log.Address == contractAddresses.EigenpodManager && log.EventName == "PodSharesUpdated" { - parsedRecord, err = ss.handlePodSharesUpdatedEvent(log) + record, err := ss.handlePodSharesUpdatedEvent(log) + if err == nil { + parsedRecords = append(parsedRecords, record) + } } else if log.Address == contractAddresses.StrategyManager && log.EventName == "ShareWithdrawalQueued" && log.TransactionHash != "0x62eb0d0865b2636c74ed146e2d161e39e42b09bac7f86b8905fc7a830935dc1e" { - parsedRecord, err = ss.handleM1StakerWithdrawals(log) + record, err := ss.handleM1StakerWithdrawals(log) + if err == nil { + parsedRecords = append(parsedRecords, record) + } + } else if log.Address == contractAddresses.DelegationManager && log.EventName == "WithdrawalQueued" { + records, err := ss.handleM2QueuedWithdrawal(log) + if err == nil && records != nil { + parsedRecords = append(parsedRecords, records...) + } } else if log.Address == contractAddresses.DelegationManager && log.EventName == "WithdrawalMigrated" { - parsedRecord, err = ss.handleM2StakerWithdrawals(log) + records, err := ss.handleMigratedM2StakerWithdrawals(log) + if err == nil { + parsedRecords = append(parsedRecords, records...) + } } else { ss.logger.Sugar().Debugw("Got stakerShares event that we don't handle", zap.String("eventName", log.EventName), @@ -258,7 +408,7 @@ func (ss *StakerSharesModel) GetStateTransitions() (types.StateTransitions[Accum if err != nil { return nil, err } - if parsedRecord == nil { + if parsedRecords == nil { return nil, nil } @@ -267,21 +417,21 @@ func (ss *StakerSharesModel) GetStateTransitions() (types.StateTransitions[Accum return nil, xerrors.Errorf("No state accumulator found for block %d", log.BlockNumber) } - slotId := NewSlotId(parsedRecord.Staker, parsedRecord.Strategy) - record, ok := ss.stateAccumulator[log.BlockNumber][slotId] - if !ok { - record = parsedRecord - ss.stateAccumulator[log.BlockNumber][slotId] = record - } else { - if record.IsNegative { - record.Shares = record.Shares.Sub(record.Shares, parsedRecord.Shares) + for _, parsedRecord := range parsedRecords { + if parsedRecord == nil { + continue + } + slotId := NewSlotId(parsedRecord.Staker, parsedRecord.Strategy) + record, ok := ss.stateAccumulator[log.BlockNumber][slotId] + if !ok { + record = parsedRecord + ss.stateAccumulator[log.BlockNumber][slotId] = record } else { record.Shares = record.Shares.Add(record.Shares, parsedRecord.Shares) } - } - return record, nil + return &AccumulatedStateChanges{Changes: parsedRecords}, nil } // Create an ordered list of block numbers @@ -302,6 +452,7 @@ func (ss *StakerSharesModel) getContractAddressesForEnvironment() map[string][]s return map[string][]string{ contracts.DelegationManager: []string{ "WithdrawalMigrated", + "WithdrawalQueued", }, contracts.StrategyManager: []string{ "Deposit", @@ -335,7 +486,7 @@ func (ss *StakerSharesModel) HandleStateChange(log *storage.TransactionLog) (int return nil, err } if change == nil { - return nil, xerrors.Errorf("No state change found for block %d", blockNumber) + return nil, nil } return change, nil } @@ -425,9 +576,9 @@ func (ss *StakerSharesModel) prepareState(blockNumber uint64) ([]StakerSharesDif } if existingRecord, ok := mappedRecords[slotId]; ok { - existingShares, err := uint256.FromDecimal(existingRecord.Shares) - if err != nil { - ss.logger.Sugar().Errorw("Failed to convert existing shares to uint256", zap.Error(err)) + existingShares, success := numbers.NewBig257().SetString(existingRecord.Shares, 10) + if !success { + ss.logger.Sugar().Errorw("Failed to convert existing shares to big.Int") continue } prepared.Shares = existingShares.Add(existingShares, newState.Shares) diff --git a/internal/eigenState/stakerShares/stakerShares_test.go b/internal/eigenState/stakerShares/stakerShares_test.go index ec2487fb..d17d8a16 100644 --- a/internal/eigenState/stakerShares/stakerShares_test.go +++ b/internal/eigenState/stakerShares/stakerShares_test.go @@ -7,7 +7,7 @@ import ( "github.com/Layr-Labs/go-sidecar/internal/sqlite/migrations" "github.com/Layr-Labs/go-sidecar/internal/storage" "github.com/Layr-Labs/go-sidecar/internal/tests" - "github.com/holiman/uint256" + "github.com/Layr-Labs/go-sidecar/internal/types/numbers" "github.com/stretchr/testify/assert" "go.uber.org/zap" "gorm.io/gorm" @@ -39,7 +39,16 @@ func setup() ( } func teardown(model *StakerSharesModel) { - model.Db.Exec("truncate table staker_shares cascade") + queries := []string{ + `truncate table staker_shares cascade`, + `truncate table blocks cascade`, + `truncate table transactions cascade`, + `truncate table transaction_logs cascade`, + } + for _, query := range queries { + + model.Db.Raw(query) + } } func Test_StakerSharesState(t *testing.T) { @@ -81,12 +90,14 @@ func Test_StakerSharesState(t *testing.T) { assert.Nil(t, err) assert.NotNil(t, change) - typedChange := change.(*AccumulatedStateChange) + typedChange := change.(*AccumulatedStateChanges) - expectedShares, _ := uint256.FromDecimal("159925690037480381") - assert.Equal(t, expectedShares, typedChange.Shares) - assert.Equal(t, "0xaf6fb48ac4a60c61a64124ce9dc28f508dc8de8d", typedChange.Staker) - assert.Equal(t, "0x7d704507b76571a51d9cae8addabbfd0ba0e63d3", typedChange.Strategy) + assert.Equal(t, 1, len(typedChange.Changes)) + + expectedShares, _ := numbers.NewBig257().SetString("159925690037480381", 10) + assert.Equal(t, expectedShares, typedChange.Changes[0].Shares) + assert.Equal(t, "0xaf6fb48ac4a60c61a64124ce9dc28f508dc8de8d", typedChange.Changes[0].Staker) + assert.Equal(t, "0x7d704507b76571a51d9cae8addabbfd0ba0e63d3", typedChange.Changes[0].Strategy) teardown(model) }) @@ -116,12 +127,13 @@ func Test_StakerSharesState(t *testing.T) { assert.Nil(t, err) assert.NotNil(t, change) - typedChange := change.(*AccumulatedStateChange) + typedChange := change.(*AccumulatedStateChanges) + assert.Equal(t, 1, len(typedChange.Changes)) - expectedShares, _ := uint256.FromDecimal("246393621132195985") - assert.Equal(t, expectedShares, typedChange.Shares) - assert.Equal(t, "0x9c01148c464cf06d135ad35d3d633ab4b46b9b78", typedChange.Staker) - assert.Equal(t, "0x298afb19a105d59e74658c4c334ff360bade6dd2", typedChange.Strategy) + expectedShares, _ := numbers.NewBig257().SetString("-246393621132195985", 10) + assert.Equal(t, expectedShares, typedChange.Changes[0].Shares) + assert.Equal(t, "0x9c01148c464cf06d135ad35d3d633ab4b46b9b78", typedChange.Changes[0].Staker) + assert.Equal(t, "0x298afb19a105d59e74658c4c334ff360bade6dd2", typedChange.Changes[0].Strategy) teardown(model) }) @@ -151,16 +163,356 @@ func Test_StakerSharesState(t *testing.T) { assert.Nil(t, err) assert.NotNil(t, change) - typedChange := change.(*AccumulatedStateChange) + typedChange := change.(*AccumulatedStateChanges) + assert.Equal(t, 1, len(typedChange.Changes)) + + expectedShares, _ := numbers.NewBig257().SetString("32000000000000000000", 10) + assert.Equal(t, expectedShares, typedChange.Changes[0].Shares) + assert.Equal(t, strings.ToLower("0x0808D4689B347D499a96f139A5fC5B5101258406"), typedChange.Changes[0].Staker) + assert.Equal(t, "0xbeac0eeeeeeeeeeeeeeeeeeeeeeeeeeeeeebeac0", typedChange.Changes[0].Strategy) + + teardown(model) + }) + t.Run("Should capture M2 withdrawals", func(t *testing.T) { + esm := stateManager.NewEigenStateManager(l, grm) + blockNumber := uint64(200) + log := storage.TransactionLog{ + TransactionHash: "some hash", + TransactionIndex: big.NewInt(300).Uint64(), + BlockNumber: blockNumber, + Address: cfg.GetContractsMapForEnvAndNetwork().DelegationManager, + Arguments: `[{"Name": "withdrawalRoot", "Type": "bytes32", "Value": ""}, {"Name": "withdrawal", "Type": "(address,address,address,uint256,uint32,address[],uint256[])", "Value": ""}]`, + EventName: "WithdrawalQueued", + LogIndex: big.NewInt(600).Uint64(), + OutputData: `{"withdrawal": {"nonce": 0, "shares": [1000000000000000000], "staker": "0x3c42cd72639e3e8d11ab8d0072cc13bd5d8aa83c", "startBlock": 1215690, "strategies": ["0xd523267698c81a372191136e477fdebfa33d9fb4"], "withdrawer": "0x3c42cd72639e3e8d11ab8d0072cc13bd5d8aa83c", "delegatedTo": "0x2177dee1f66d6dbfbf517d9c4f316024c6a21aeb"}, "withdrawalRoot": [24, 23, 49, 137, 14, 63, 119, 12, 234, 225, 63, 35, 109, 249, 112, 24, 241, 118, 212, 52, 22, 107, 202, 56, 105, 37, 68, 47, 169, 23, 142, 135]}`, + CreatedAt: time.Time{}, + UpdatedAt: time.Time{}, + DeletedAt: time.Time{}, + } + + model, err := NewStakerSharesModel(esm, grm, cfg.Network, cfg.Environment, l, cfg) + + err = model.InitBlockProcessing(blockNumber) + assert.Nil(t, err) + + change, err := model.HandleStateChange(&log) + assert.Nil(t, err) + assert.NotNil(t, change) + + typedChange := change.(*AccumulatedStateChanges) + assert.Equal(t, 1, len(typedChange.Changes)) + + expectedShares, _ := numbers.NewBig257().SetString("-1000000000000000000", 10) + assert.Equal(t, expectedShares, typedChange.Changes[0].Shares) + assert.Equal(t, strings.ToLower("0x3c42cd72639e3e8d11ab8d0072cc13bd5d8aa83c"), typedChange.Changes[0].Staker) + assert.Equal(t, "0xd523267698c81a372191136e477fdebfa33d9fb4", typedChange.Changes[0].Strategy) + + teardown(model) + }) + t.Run("Should capture M2 migration", func(t *testing.T) { + t.Skip() + esm := stateManager.NewEigenStateManager(l, grm) + + originBlockNumber := uint64(100) + + block := storage.Block{ + Number: originBlockNumber, + Hash: "some hash", + } + res := grm.Model(storage.Block{}).Create(&block) + if res.Error != nil { + t.Fatal(res.Error) + } + + transaction := storage.Transaction{ + BlockNumber: block.Number, + TransactionHash: "0x5ff283cb420cdf950036d538e2223d5b504b875828f6e0d243002f429da6faa2", + TransactionIndex: big.NewInt(200).Uint64(), + FromAddress: "0x9c01148c464cf06d135ad35d3d633ab4b46b9b78", + } + res = grm.Model(storage.Transaction{}).Create(&transaction) + if res.Error != nil { + t.Fatal(res.Error) + } + + // setup M1 withdrawal WithdrawalQueued (has root) and N many ShareWithdrawalQueued events (staker, strategy, shares) + shareWithdrawalQueued := storage.TransactionLog{ + TransactionHash: "0x5ff283cb420cdf950036d538e2223d5b504b875828f6e0d243002f429da6faa2", + TransactionIndex: big.NewInt(200).Uint64(), + BlockNumber: originBlockNumber, + Address: cfg.GetContractsMapForEnvAndNetwork().StrategyManager, + Arguments: `[{"Name": "depositor", "Type": "address", "Value": null, "Indexed": false}, {"Name": "nonce", "Type": "uint96", "Value": null, "Indexed": false}, {"Name": "strategy", "Type": "address", "Value": null, "Indexed": false}, {"Name": "shares", "Type": "uint256", "Value": null, "Indexed": false}]`, + EventName: "ShareWithdrawalQueued", + LogIndex: big.NewInt(1).Uint64(), + OutputData: `{"nonce": 0, "shares": 246393621132195985, "strategy": "0x298afb19a105d59e74658c4c334ff360bade6dd2", "depositor": "0x9c01148c464cf06d135ad35d3d633ab4b46b9b78"}`, + CreatedAt: time.Time{}, + UpdatedAt: time.Time{}, + DeletedAt: time.Time{}, + } + res = grm.Model(storage.TransactionLog{}).Create(&shareWithdrawalQueued) + if res.Error != nil { + t.Fatal(res.Error) + } + + withdrawalQueued := storage.TransactionLog{ + TransactionHash: "0x5ff283cb420cdf950036d538e2223d5b504b875828f6e0d243002f429da6faa2", + TransactionIndex: big.NewInt(200).Uint64(), + BlockNumber: originBlockNumber, + Address: cfg.GetContractsMapForEnvAndNetwork().StrategyManager, + Arguments: `[{"Name": "depositor", "Type": "address", "Value": null, "Indexed": false}, {"Name": "nonce", "Type": "uint96", "Value": null, "Indexed": false}, {"Name": "withdrawer", "Type": "address", "Value": null, "Indexed": false}, {"Name": "delegatedAddress", "Type": "address", "Value": null, "Indexed": false}, {"Name": "withdrawalRoot", "Type": "bytes32", "Value": null, "Indexed": false}]`, + EventName: "WithdrawalQueued", + LogIndex: big.NewInt(2).Uint64(), + OutputData: `{"nonce": 0, "depositor": "0x9c01148c464cf06d135ad35d3d633ab4b46b9b78", "withdrawer": "0x9c01148c464cf06d135ad35d3d633ab4b46b9b78", "withdrawalRoot": [31, 200, 156, 159, 43, 41, 112, 204, 139, 225, 142, 72, 58, 63, 194, 149, 59, 254, 218, 227, 162, 25, 237, 7, 103, 240, 24, 255, 31, 152, 236, 84], "delegatedAddress": "0x0000000000000000000000000000000000000000"}`, + CreatedAt: time.Time{}, + UpdatedAt: time.Time{}, + DeletedAt: time.Time{}, + } + res = grm.Model(storage.TransactionLog{}).Create(&withdrawalQueued) + if res.Error != nil { + t.Fatal(res.Error) + } + + blockNumber := uint64(200) + log := storage.TransactionLog{ + TransactionHash: "some hash", + TransactionIndex: big.NewInt(300).Uint64(), + BlockNumber: blockNumber, + Address: cfg.GetContractsMapForEnvAndNetwork().DelegationManager, + Arguments: `[{"Name": "oldWithdrawalRoot", "Type": "bytes32", "Value": ""}, {"Name": "newWithdrawalRoot", "Type": "bytes32", "Value": ""}]`, + EventName: "WithdrawalMigrated", + LogIndex: big.NewInt(600).Uint64(), + OutputData: `{"newWithdrawalRoot": [218, 200, 138, 86, 38, 9, 156, 119, 73, 13, 168, 40, 209, 43, 238, 83, 234, 177, 230, 73, 120, 205, 255, 143, 255, 216, 51, 209, 137, 100, 163, 233], "oldWithdrawalRoot": [31, 200, 156, 159, 43, 41, 112, 204, 139, 225, 142, 72, 58, 63, 194, 149, 59, 254, 218, 227, 162, 25, 237, 7, 103, 240, 24, 255, 31, 152, 236, 84]}`, + CreatedAt: time.Time{}, + UpdatedAt: time.Time{}, + DeletedAt: time.Time{}, + } + + model, err := NewStakerSharesModel(esm, grm, cfg.Network, cfg.Environment, l, cfg) + + err = model.InitBlockProcessing(blockNumber) + assert.Nil(t, err) + + change, err := model.HandleStateChange(&log) + assert.Nil(t, err) + assert.NotNil(t, change) + + typedChange := change.(*AccumulatedStateChanges) + assert.Equal(t, 1, len(typedChange.Changes)) + assert.Equal(t, "0x9c01148c464cf06d135ad35d3d633ab4b46b9b78", typedChange.Changes[0].Staker) + assert.Equal(t, "0x298afb19a105d59e74658c4c334ff360bade6dd2", typedChange.Changes[0].Strategy) + assert.Equal(t, "246393621132195985", typedChange.Changes[0].Shares.String()) + + preparedChange, err := model.prepareState(blockNumber) + assert.Nil(t, err) + assert.Equal(t, "0x9c01148c464cf06d135ad35d3d633ab4b46b9b78", preparedChange[0].Staker) + assert.Equal(t, "0x298afb19a105d59e74658c4c334ff360bade6dd2", preparedChange[0].Strategy) + assert.Equal(t, "246393621132195985", preparedChange[0].Shares.String()) + + err = model.clonePreviousBlocksToNewBlock(blockNumber) + assert.Nil(t, err) + + err = model.CommitFinalState(blockNumber) + assert.Nil(t, err) - expectedShares, _ := uint256.FromDecimal("32000000000000000000") - assert.Equal(t, expectedShares, typedChange.Shares) - assert.Equal(t, strings.ToLower("0x0808D4689B347D499a96f139A5fC5B5101258406"), typedChange.Staker) - assert.Equal(t, "0xbeac0eeeeeeeeeeeeeeeeeeeeeeeeeeeeeebeac0", typedChange.Strategy) + query := `select * from staker_shares where block_number = ?` + results := []*StakerShares{} + res = model.Db.Raw(query, blockNumber).Scan(&results) + assert.Nil(t, res.Error) + assert.Equal(t, 1, len(results)) teardown(model) }) - t.Run("Should capture M2 migrated withdrawals", func(t *testing.T) { - t.Skip("M2 migration is not yet implemented") + t.Run("Should handle an M1 withdrawal and migration to M2 correctly", func(t *testing.T) { + esm := stateManager.NewEigenStateManager(l, grm) + model, err := NewStakerSharesModel(esm, grm, cfg.Network, cfg.Environment, l, cfg) + assert.Nil(t, err) + + originBlockNumber := uint64(101) + originTxHash := "0x5ff283cb420cdf950036d538e2223d5b504b875828f6e0d243002f429da6faa3" + + block := storage.Block{ + Number: originBlockNumber, + Hash: "some hash", + } + res := grm.Model(storage.Block{}).Create(&block) + if res.Error != nil { + t.Fatal(res.Error) + } + + transaction := storage.Transaction{ + BlockNumber: block.Number, + TransactionHash: originTxHash, + TransactionIndex: big.NewInt(200).Uint64(), + FromAddress: "0x9c01148c464cf06d135ad35d3d633ab4b46b9b78", + } + res = grm.Model(storage.Transaction{}).Create(&transaction) + if res.Error != nil { + t.Fatal(res.Error) + } + + // Insert the M1 withdrawal since we'll need it later + shareWithdrawalQueued := storage.TransactionLog{ + TransactionHash: originTxHash, + TransactionIndex: big.NewInt(1).Uint64(), + BlockNumber: originBlockNumber, + Address: cfg.GetContractsMapForEnvAndNetwork().StrategyManager, + Arguments: `[{"Name": "depositor", "Type": "address", "Value": null, "Indexed": false}, {"Name": "nonce", "Type": "uint96", "Value": null, "Indexed": false}, {"Name": "strategy", "Type": "address", "Value": null, "Indexed": false}, {"Name": "shares", "Type": "uint256", "Value": null, "Indexed": false}]`, + EventName: "ShareWithdrawalQueued", + LogIndex: big.NewInt(1).Uint64(), + OutputData: `{"nonce": 0, "shares": 246393621132195985, "strategy": "0x298afb19a105d59e74658c4c334ff360bade6dd2", "depositor": "0x9c01148c464cf06d135ad35d3d633ab4b46b9b78"}`, + CreatedAt: time.Time{}, + UpdatedAt: time.Time{}, + DeletedAt: time.Time{}, + } + res = grm.Model(storage.TransactionLog{}).Create(&shareWithdrawalQueued) + if res.Error != nil { + t.Fatal(res.Error) + } + + // init processing for the M1 withdrawal + err = model.InitBlockProcessing(originBlockNumber) + assert.Nil(t, err) + + change, err := model.HandleStateChange(&shareWithdrawalQueued) + assert.Nil(t, err) + assert.NotNil(t, change) + + typedChange := change.(*AccumulatedStateChanges) + + assert.Equal(t, 1, len(typedChange.Changes)) + assert.Equal(t, "0x9c01148c464cf06d135ad35d3d633ab4b46b9b78", typedChange.Changes[0].Staker) + assert.Equal(t, "0x298afb19a105d59e74658c4c334ff360bade6dd2", typedChange.Changes[0].Strategy) + assert.Equal(t, "-246393621132195985", typedChange.Changes[0].Shares.String()) + + slotId := NewSlotId(typedChange.Changes[0].Staker, typedChange.Changes[0].Strategy) + + accumulatedState, ok := model.stateAccumulator[originBlockNumber][slotId] + assert.True(t, ok) + assert.NotNil(t, accumulatedState) + assert.Equal(t, "0x9c01148c464cf06d135ad35d3d633ab4b46b9b78", accumulatedState.Staker) + assert.Equal(t, "0x298afb19a105d59e74658c4c334ff360bade6dd2", accumulatedState.Strategy) + assert.Equal(t, "-246393621132195985", accumulatedState.Shares.String()) + + // Insert the other half of the M1 event that captures the withdrawalRoot associated with the M1 withdrawal + // No need to process this event, we just need it to be present in the DB + withdrawalQueued := storage.TransactionLog{ + TransactionHash: originTxHash, + TransactionIndex: big.NewInt(200).Uint64(), + BlockNumber: originBlockNumber, + Address: cfg.GetContractsMapForEnvAndNetwork().StrategyManager, + Arguments: `[{"Name": "depositor", "Type": "address", "Value": null, "Indexed": false}, {"Name": "nonce", "Type": "uint96", "Value": null, "Indexed": false}, {"Name": "withdrawer", "Type": "address", "Value": null, "Indexed": false}, {"Name": "delegatedAddress", "Type": "address", "Value": null, "Indexed": false}, {"Name": "withdrawalRoot", "Type": "bytes32", "Value": null, "Indexed": false}]`, + EventName: "WithdrawalQueued", + LogIndex: big.NewInt(2).Uint64(), + OutputData: `{"nonce": 0, "depositor": "0x9c01148c464cf06d135ad35d3d633ab4b46b9b78", "withdrawer": "0x9c01148c464cf06d135ad35d3d633ab4b46b9b78", "withdrawalRoot": [31, 200, 156, 159, 43, 41, 112, 204, 139, 225, 142, 72, 58, 63, 194, 149, 59, 254, 218, 227, 162, 25, 237, 7, 103, 240, 24, 255, 31, 152, 236, 84], "delegatedAddress": "0x0000000000000000000000000000000000000000"}`, + CreatedAt: time.Time{}, + UpdatedAt: time.Time{}, + DeletedAt: time.Time{}, + } + res = grm.Model(storage.TransactionLog{}).Create(&withdrawalQueued) + if res.Error != nil { + t.Fatal(res.Error) + } + + change, err = model.HandleStateChange(&withdrawalQueued) + assert.Nil(t, err) + assert.Nil(t, change) // should be nil since the handler doesnt care about this event + + err = model.CommitFinalState(originBlockNumber) + assert.Nil(t, err) + + // verify the M1 withdrawal was processed correctly + query := `select * from staker_shares where block_number = ?` + results := []*StakerShares{} + res = model.Db.Raw(query, originBlockNumber).Scan(&results) + + assert.Nil(t, res.Error) + assert.Equal(t, 1, len(results)) + assert.Equal(t, "0x9c01148c464cf06d135ad35d3d633ab4b46b9b78", results[0].Staker) + assert.Equal(t, "0x298afb19a105d59e74658c4c334ff360bade6dd2", results[0].Strategy) + assert.Equal(t, "-246393621132195985", results[0].Shares) + + // setup M2 migration + blockNumber := uint64(102) + err = model.InitBlockProcessing(blockNumber) + assert.Nil(t, err) + + // M2 WithdrawalQueued comes before the M2 WithdrawalMigrated event + log := storage.TransactionLog{ + TransactionHash: "some hash", + TransactionIndex: big.NewInt(1).Uint64(), + BlockNumber: blockNumber, + Address: cfg.GetContractsMapForEnvAndNetwork().DelegationManager, + Arguments: `[{"Name": "withdrawalRoot", "Type": "bytes32", "Value": ""}, {"Name": "withdrawal", "Type": "(address,address,address,uint256,uint32,address[],uint256[])", "Value": ""}]`, + EventName: "WithdrawalQueued", + LogIndex: big.NewInt(600).Uint64(), + OutputData: `{"withdrawal": {"nonce": 0, "shares": [246393621132195985], "staker": "0x9c01148c464cf06d135ad35d3d633ab4b46b9b78", "startBlock": 1215690, "strategies": ["0x298afb19a105d59e74658c4c334ff360bade6dd2"], "withdrawer": "0x9c01148c464cf06d135ad35d3d633ab4b46b9b78", "delegatedTo": "0x2177dee1f66d6dbfbf517d9c4f316024c6a21aeb"}, "withdrawalRoot": [24, 23, 49, 137, 14, 63, 119, 12, 234, 225, 63, 35, 109, 249, 112, 24, 241, 118, 212, 52, 22, 107, 202, 56, 105, 37, 68, 47, 169, 23, 142, 135]}`, + CreatedAt: time.Time{}, + UpdatedAt: time.Time{}, + DeletedAt: time.Time{}, + } + + change, err = model.HandleStateChange(&log) + assert.Nil(t, err) + assert.NotNil(t, change) + + typedChange = change.(*AccumulatedStateChanges) + assert.Equal(t, 1, len(typedChange.Changes)) + assert.Equal(t, "0x9c01148c464cf06d135ad35d3d633ab4b46b9b78", typedChange.Changes[0].Staker) + assert.Equal(t, "0x298afb19a105d59e74658c4c334ff360bade6dd2", typedChange.Changes[0].Strategy) + assert.Equal(t, "-246393621132195985", typedChange.Changes[0].Shares.String()) + + // M2 WithdrawalMigrated event. Typically occurs in the same block as the M2 WithdrawalQueued event + withdrawalMigratedLog := storage.TransactionLog{ + TransactionHash: "some hash", + TransactionIndex: big.NewInt(2).Uint64(), + BlockNumber: blockNumber, + Address: cfg.GetContractsMapForEnvAndNetwork().DelegationManager, + Arguments: `[{"Name": "oldWithdrawalRoot", "Type": "bytes32", "Value": ""}, {"Name": "newWithdrawalRoot", "Type": "bytes32", "Value": ""}]`, + EventName: "WithdrawalMigrated", + LogIndex: big.NewInt(600).Uint64(), + OutputData: `{"newWithdrawalRoot": [24, 23, 49, 137, 14, 63, 119, 12, 234, 225, 63, 35, 109, 249, 112, 24, 241, 118, 212, 52, 22, 107, 202, 56, 105, 37, 68, 47, 169, 23, 142, 135], "oldWithdrawalRoot": [31, 200, 156, 159, 43, 41, 112, 204, 139, 225, 142, 72, 58, 63, 194, 149, 59, 254, 218, 227, 162, 25, 237, 7, 103, 240, 24, 255, 31, 152, 236, 84]}`, + CreatedAt: time.Time{}, + UpdatedAt: time.Time{}, + DeletedAt: time.Time{}, + } + + change, err = model.HandleStateChange(&withdrawalMigratedLog) + assert.Nil(t, err) + assert.NotNil(t, change) + + typedChange = change.(*AccumulatedStateChanges) + assert.Equal(t, 1, len(typedChange.Changes)) + assert.Equal(t, "0x9c01148c464cf06d135ad35d3d633ab4b46b9b78", typedChange.Changes[0].Staker) + assert.Equal(t, "0x298afb19a105d59e74658c4c334ff360bade6dd2", typedChange.Changes[0].Strategy) + assert.Equal(t, "246393621132195985", typedChange.Changes[0].Shares.String()) + + slotId = NewSlotId(typedChange.Changes[0].Staker, typedChange.Changes[0].Strategy) + + accumulatedState, ok = model.stateAccumulator[blockNumber][slotId] + assert.True(t, ok) + assert.NotNil(t, accumulatedState) + assert.Equal(t, "0x9c01148c464cf06d135ad35d3d633ab4b46b9b78", accumulatedState.Staker) + assert.Equal(t, "0x298afb19a105d59e74658c4c334ff360bade6dd2", accumulatedState.Strategy) + assert.Equal(t, "0", accumulatedState.Shares.String()) + + err = model.CommitFinalState(blockNumber) + assert.Nil(t, err) + + // Get the state at the new block and verify the shares amount is correct + query = ` + select * from staker_shares + where block_number = ? + ` + results = []*StakerShares{} + res = model.Db.Raw(query, blockNumber).Scan(&results) + assert.Nil(t, res.Error) + + assert.Equal(t, 1, len(results)) + assert.Equal(t, "0x9c01148c464cf06d135ad35d3d633ab4b46b9b78", results[0].Staker) + assert.Equal(t, "0x298afb19a105d59e74658c4c334ff360bade6dd2", results[0].Strategy) + assert.Equal(t, "-246393621132195985", results[0].Shares) + assert.Equal(t, blockNumber, results[0].BlockNumber) + + teardown(model) }) } diff --git a/internal/indexer/transactionLogs.go b/internal/indexer/transactionLogs.go index 56024fc6..60a1c15b 100644 --- a/internal/indexer/transactionLogs.go +++ b/internal/indexer/transactionLogs.go @@ -236,7 +236,7 @@ func (idx *Indexer) DecodeLogWithAbi( // If the address of the log is not the same as the contract address, we need to load the ABI for the log // // The typical case is when a contract interacts with another contract that emits an event - if utils.AreAddressesEqual(logAddress.String(), txReceipt.GetTargetAddress().Value()) { + if utils.AreAddressesEqual(logAddress.String(), txReceipt.GetTargetAddress().Value()) && a != nil { return idx.DecodeLog(a, lg) } else { idx.Logger.Sugar().Debugw("Log address does not match contract address", zap.String("logAddress", logAddress.String()), zap.String("contractAddress", txReceipt.GetTargetAddress().Value())) diff --git a/internal/sqlite/sqlite.go b/internal/sqlite/sqlite.go index 562c7728..45c0ef85 100644 --- a/internal/sqlite/sqlite.go +++ b/internal/sqlite/sqlite.go @@ -1,15 +1,39 @@ package sqlite import ( + "database/sql" + "encoding/hex" + "encoding/json" "fmt" + goSqlite "github.com/mattn/go-sqlite3" "gorm.io/driver/sqlite" "gorm.io/gorm" "gorm.io/gorm/logger" ) +// bytesToHex is a custom SQLite function that converts a JSON byte array to a hex string. +// +// @param jsonByteArray: a JSON byte array, e.g. [1, 2, 3, ...] +// @return: a hex string without a leading 0x, e.g. 78cc56f0700e7ba5055f12... +func bytesToHex(jsonByteArray string) (string, error) { + jsonBytes := make([]byte, 0) + err := json.Unmarshal([]byte(jsonByteArray), &jsonBytes) + if err != nil { + return "", err + } + return hex.EncodeToString(jsonBytes), nil +} + func NewSqlite(path string) gorm.Dialector { - db := sqlite.Open(path) - return db + sql.Register("sqlite3_with_extensions", &goSqlite.SQLiteDriver{ + ConnectHook: func(conn *goSqlite.SQLiteConn) error { + return conn.RegisterFunc("bytes_to_hex", bytesToHex, true) + }, + }) + return &sqlite.Dialector{ + DriverName: "sqlite3_with_extensions", + DSN: path, + } } func NewGormSqliteFromSqlite(sqlite gorm.Dialector) (*gorm.DB, error) { @@ -34,6 +58,7 @@ func NewGormSqliteFromSqlite(sqlite gorm.Dialector) (*gorm.DB, error) { return nil, res.Error } } + return db, nil } diff --git a/internal/sqlite/sqlite_test.go b/internal/sqlite/sqlite_test.go new file mode 100644 index 00000000..736b02f9 --- /dev/null +++ b/internal/sqlite/sqlite_test.go @@ -0,0 +1,39 @@ +package sqlite + +import ( + "encoding/hex" + "github.com/stretchr/testify/assert" + "strings" + "testing" +) + +func Test_Sqlite(t *testing.T) { + t.Run("Should use the bytesToHex function", func(t *testing.T) { + query := ` + with json_values as ( + select + cast('{"newWithdrawalRoot": [218, 200, 138, 86, 38, 9, 156, 119, 73, 13, 168, 40, 209, 43, 238, 83, 234, 177, 230, 73, 120, 205, 255, 143, 255, 216, 51, 209, 137, 100, 163, 233] }' as text) as json_col + from (select 1) + ) + select + bytes_to_hex(json_extract(json_col, '$.newWithdrawalRoot')) AS withdrawal_hex + from json_values + limit 1 + ` + s := NewSqlite("file::memory:?cache=shared") + grm, err := NewGormSqliteFromSqlite(s) + assert.Nil(t, err) + + type results struct { + WithdrawalHex string + } + + hexValue := &results{} + res := grm.Raw(query).Scan(&hexValue) + + expectedBytes := []byte{218, 200, 138, 86, 38, 9, 156, 119, 73, 13, 168, 40, 209, 43, 238, 83, 234, 177, 230, 73, 120, 205, 255, 143, 255, 216, 51, 209, 137, 100, 163, 233} + + assert.Nil(t, res.Error) + assert.Equal(t, strings.ToLower(hex.EncodeToString(expectedBytes)), hexValue.WithdrawalHex) + }) +} diff --git a/internal/types/numbers/numbers.go b/internal/types/numbers/numbers.go new file mode 100644 index 00000000..3e9d62c0 --- /dev/null +++ b/internal/types/numbers/numbers.go @@ -0,0 +1,10 @@ +package numbers + +import "math/big" + +// NewBig257 returns a new big.Int with a size of 257 bits +// This allows us to fully support math on uint256 numbers +// as well as int256 numbers used for EigenPods +func NewBig257() *big.Int { + return big.NewInt(257) +} diff --git a/internal/types/numbers/numbers_test.go b/internal/types/numbers/numbers_test.go new file mode 100644 index 00000000..681a0f88 --- /dev/null +++ b/internal/types/numbers/numbers_test.go @@ -0,0 +1,48 @@ +package numbers + +import ( + "github.com/stretchr/testify/assert" + "math/big" + "testing" +) + +func Test_numbers(t *testing.T) { + t.Run("Test that big.Int can produce negative numbers", func(t *testing.T) { + startingNum := big.Int{} + startingNum.SetString("10", 10) + + amountToSubtract := big.Int{} + amountToSubtract.SetString("20", 10) + + assert.Equal(t, "-10", amountToSubtract.Sub(&startingNum, &amountToSubtract).String()) + }) + t.Run("Test that big.NewInt(257) can produce negative numbers", func(t *testing.T) { + startingNum := big.NewInt(257) + startingNum.SetString("10", 10) + + amountToSubtract := big.NewInt(257) + amountToSubtract.SetString("20", 10) + + assert.Equal(t, "-10", amountToSubtract.Sub(startingNum, amountToSubtract).String()) + }) + t.Run("Test that Big257 can produce negative numbers", func(t *testing.T) { + startingNum, success := NewBig257().SetString("10", 10) + assert.True(t, success) + + amountToSubtract, success := NewBig257().SetString("20", 10) + assert.True(t, success) + + assert.Equal(t, "-10", amountToSubtract.Sub(startingNum, amountToSubtract).String()) + }) + t.Run("Test a really big number", func(t *testing.T) { + startingNum, success := NewBig257().SetString("13389173346000000000000000", 10) + assert.True(t, success) + + assert.Equal(t, "13389173346000000000000000", startingNum.String()) + + amountToSubtract, success := NewBig257().SetString("20", 10) + assert.True(t, success) + + assert.Equal(t, "13389173345999999999999980", amountToSubtract.Sub(startingNum, amountToSubtract).String()) + }) +}