diff --git a/seth/.changeset/v1.50.11.md b/seth/.changeset/v1.50.11.md new file mode 100644 index 000000000..4d963ca4d --- /dev/null +++ b/seth/.changeset/v1.50.11.md @@ -0,0 +1,2 @@ + - improvement: allow to retry gas estimation by providing `gas_price_estimation_attempt_count` setting on a Network level (defaults to 1 -> no retries) + - improvement: better handling of event signature (hash) collisions (although still not 100% bullet-proof due to complexlity) \ No newline at end of file diff --git a/seth/client.go b/seth/client.go index 43a8a0af1..c1c708eee 100644 --- a/seth/client.go +++ b/seth/client.go @@ -7,6 +7,7 @@ import ( "math/big" "net/http" "path/filepath" + "reflect" "strings" "time" @@ -1167,38 +1168,144 @@ func (t TransactionLog) GetData() []byte { } func (m *Client) decodeContractLogs(l zerolog.Logger, logs []types.Log, allABIs []*abi.ABI) ([]DecodedTransactionLog, error) { - l.Trace().Msg("Decoding ALL events") + l.Trace(). + Msg("Decoding events") + sigMap := buildEventSignatureMap(allABIs) + var eventsParsed []DecodedTransactionLog for _, lo := range logs { - ABI_LOOP: - for _, a := range allABIs { - for _, evSpec := range a.Events { - if evSpec.ID.Hex() == lo.Topics[0].Hex() { - d := TransactionLog{lo.Topics, lo.Data} - l.Trace().Str("Name", evSpec.RawName).Str("Signature", evSpec.Sig).Msg("Unpacking event") - eventsMap, topicsMap, err := decodeEventFromLog(l, *a, evSpec, d) - if err != nil { - return nil, errors.Wrap(err, ErrDecodeLog) - } - parsedEvent := decodedLogFromMaps(&DecodedTransactionLog{}, eventsMap, topicsMap) - decodedTransactionLog, ok := parsedEvent.(*DecodedTransactionLog) - if ok { - decodedTransactionLog.Signature = evSpec.Sig - m.mergeLogMeta(decodedTransactionLog, lo) - eventsParsed = append(eventsParsed, *decodedTransactionLog) - l.Trace().Interface("Log", parsedEvent).Msg("Transaction log") - break ABI_LOOP - } - l.Trace(). - Str("Actual type", fmt.Sprintf("%T", decodedTransactionLog)). - Msg("Failed to cast decoded event to DecodedCommonLog") + if len(lo.Topics) == 0 { + l.Debug(). + Msg("Log has no topics; skipping") + continue + } + + eventSig := lo.Topics[0].Hex() + possibleEvents, exists := sigMap[eventSig] + if !exists { + l.Trace(). + Str("Event signature", eventSig). + Msg("No matching events found for signature") + continue + } + + // Check if we know what contract is this log from and if we do, get its ABI to skip unnecessary iterations + var knownContractABI *abi.ABI + if contractName := m.ContractAddressToNameMap.GetContractName(lo.Address.Hex()); contractName != "" { + maybeABI, ok := m.ContractStore.GetABI(contractName) + if !ok { + l.Trace(). + Str("Event signature", eventSig). + Str("Contract name", contractName). + Str("Contract address", lo.Address.Hex()). + Msg("No ABI found for known contract; this is unexpected. Continuing with step-by-step ABI search") + } else { + knownContractABI = maybeABI + } + } + + // Iterate over possible events with the same signature + matched := false + for _, evWithABI := range possibleEvents { + evSpec := evWithABI.EventSpec + contractABI := evWithABI.ContractABI + + // Check if known contract ABI matches candidate ABI and if not, skip this ABI and try the next one + if knownContractABI != nil && !reflect.DeepEqual(knownContractABI, contractABI) { + l.Trace(). + Str("Event signature", eventSig). + Str("Contract address", lo.Address.Hex()). + Msg("ABI doesn't match known ABI for this address; trying next ABI") + continue + } + + // Validate indexed parameters count + // Non-indexed parameters are stored in the Data field, + // and much harder to validate due to dynamic types, + // so we skip them for now + var indexedParams abi.Arguments + for _, input := range evSpec.Inputs { + if input.Indexed { + indexedParams = append(indexedParams, input) } } + + expectedIndexed := len(indexedParams) + actualIndexed := len(lo.Topics) - 1 // First topic is the event signature + + if expectedIndexed != actualIndexed { + l.Trace(). + Str("Event", evSpec.Name). + Int("Expected indexed param count", expectedIndexed). + Int("Actual indexed param count", actualIndexed). + Msg("Mismatch in indexed parameters; skipping event") + continue + } + + // Proceed to decode the event + d := TransactionLog{lo.Topics, lo.Data} + l.Trace(). + Str("Name", evSpec.RawName). + Str("Signature", evSpec.Sig). + Msg("Unpacking event") + + eventsMap, topicsMap, err := decodeEventFromLog(l, *contractABI, *evSpec, d) + if err != nil { + l.Error(). + Err(err). + Str("Event", evSpec.Name). + Msg("Failed to decode event; skipping") + continue // Skip this event instead of returning an error + } + + parsedEvent := decodedLogFromMaps(&DecodedTransactionLog{}, eventsMap, topicsMap) + decodedTransactionLog, ok := parsedEvent.(*DecodedTransactionLog) + if ok { + decodedTransactionLog.Signature = evSpec.Sig + m.mergeLogMeta(decodedTransactionLog, lo) + eventsParsed = append(eventsParsed, *decodedTransactionLog) + l.Trace(). + Interface("Log", parsedEvent). + Msg("Transaction log decoded successfully") + matched = true + break // Move to the next log after successful decoding + } + + l.Trace(). + Str("Actual type", fmt.Sprintf("%T", decodedTransactionLog)). + Msg("Failed to cast decoded event to DecodedTransactionLog") + } + + if !matched { + l.Warn(). + Str("Signature", eventSig). + Msg("No matching event with valid indexed parameter count found for log") } } return eventsParsed, nil } +type eventWithABI struct { + ContractABI *abi.ABI + EventSpec *abi.Event +} + +// buildEventSignatureMap precomputes a mapping from event signature to events with their ABIs +func buildEventSignatureMap(allABIs []*abi.ABI) map[string][]*eventWithABI { + sigMap := make(map[string][]*eventWithABI) + for _, a := range allABIs { + for _, ev := range a.Events { + event := ev //nolint:copyloopvar // Explicitly keeping the copy for clarity + sigMap[ev.ID.Hex()] = append(sigMap[ev.ID.Hex()], &eventWithABI{ + ContractABI: a, + EventSpec: &event, + }) + } + } + + return sigMap +} + // WaitUntilNoPendingTxForRootKey waits until there's no pending transaction for root key. If after timeout there are still pending transactions, it returns error. func (m *Client) WaitUntilNoPendingTxForRootKey(timeout time.Duration) error { return m.WaitUntilNoPendingTx(m.MustGetRootKeyAddress(), timeout) diff --git a/seth/contract_map.go b/seth/contract_map.go index 1f629ff5f..cf6d4b8f6 100644 --- a/seth/contract_map.go +++ b/seth/contract_map.go @@ -45,15 +45,15 @@ func (c ContractMap) GetContractName(addr string) string { return c.addressMap[strings.ToLower(addr)] } -func (c ContractMap) GetContractAddress(addr string) string { - if addr == UNKNOWN { +func (c ContractMap) GetContractAddress(name string) string { + if name == UNKNOWN { return UNKNOWN } c.mu.Lock() defer c.mu.Unlock() for k, v := range c.addressMap { - if v == addr { + if v == name { return k } } diff --git a/tools/breakingchanges/cmd/main.go b/tools/breakingchanges/cmd/main.go index 89bb112f0..cfe9e8b07 100644 --- a/tools/breakingchanges/cmd/main.go +++ b/tools/breakingchanges/cmd/main.go @@ -85,8 +85,8 @@ func getRetractedTags(goModPath string) ([]*semver.Constraints, error) { func getLatestTag(pathPrefix string, retractedTags []*semver.Constraints) (string, error) { // use regex to find exact matches, as otherwise might include pre-release versions - // or versions that partially match the path prefix, e.g. when seraching for 'lib' - // we won't make sure we won't include tags like `lib/grafana/v1.0.0` + // or versions that partially match the path prefix, e.g. when searching for 'lib' + // we want to make sure we won't include tags like `lib/grafana/v1.0.0` grepRegex := fmt.Sprintf("^%s/v[0-9]+\\.[0-9]+\\.[0-9]+$", pathPrefix) //nolint