Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[TT-1934] event hash collision #1544

Merged
merged 3 commits into from
Jan 9, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions seth/.changeset/v1.50.11.md
Original file line number Diff line number Diff line change
@@ -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)
153 changes: 130 additions & 23 deletions seth/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"math/big"
"net/http"
"path/filepath"
"reflect"
"strings"
"time"

Expand Down Expand Up @@ -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)
Expand Down
6 changes: 3 additions & 3 deletions seth/contract_map.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Expand Down
4 changes: 2 additions & 2 deletions tools/breakingchanges/cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading