Skip to content

Commit

Permalink
[TT-1934] event hash collision (#1544)
Browse files Browse the repository at this point in the history
  • Loading branch information
Tofel authored Jan 9, 2025
1 parent d9665c9 commit cdc37af
Show file tree
Hide file tree
Showing 4 changed files with 137 additions and 28 deletions.
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

0 comments on commit cdc37af

Please sign in to comment.