diff --git a/config.md b/config.md index cca5172..61ea695 100644 --- a/config.md +++ b/config.md @@ -47,6 +47,7 @@ |Key|Description|Type|Default Value| |---|-----------|----|-------------| |blockQueueLength|Internal queue length for notifying the confirmations manager of new blocks|`int`|`50` +|fetchReceiptUponEntry|Fetch receipt of new transactions immediately when they are added to the internal queue. When set to false, fetch will only happen when a new block is received or the transaction has been queue for more than the stale receipt timeout|`boolean`|`false` |notificationQueueLength|Internal queue length for notifying the confirmations manager of new transactions/events|`int`|`50` |receiptWorkers|Number of workers to use to query in parallel for receipts|`int`|`10` |required|Number of confirmations required to consider a transaction/event final|`int`|`20` diff --git a/go.mod b/go.mod index 4f2edcd..1c9efee 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/hashicorp/golang-lru v1.0.2 github.com/hyperledger/firefly-common v1.4.8 github.com/hyperledger/firefly-signer v1.1.13 - github.com/hyperledger/firefly-transaction-manager v1.3.14 + github.com/hyperledger/firefly-transaction-manager v1.3.15 github.com/sirupsen/logrus v1.9.3 github.com/spf13/cobra v1.8.0 github.com/stretchr/testify v1.8.4 diff --git a/go.sum b/go.sum index 3b038d9..100669a 100644 --- a/go.sum +++ b/go.sum @@ -104,8 +104,8 @@ github.com/hyperledger/firefly-common v1.4.8 h1:0o1Qp1c5YzQo8nbnX+gAo9SVd2tR4Z9U github.com/hyperledger/firefly-common v1.4.8/go.mod h1:dXewcVMFNON2SvQ1UPvu64OWUt77+M3p8qy61lT1kE4= github.com/hyperledger/firefly-signer v1.1.13 h1:eiHjc6HPRG8AzXUCUgm51qqX1I9BokiuiiqJ89XwK4M= github.com/hyperledger/firefly-signer v1.1.13/go.mod h1:pK6kivzBFSue3zpJSQpH67VasnLLbwBJOBUNv0zHbRA= -github.com/hyperledger/firefly-transaction-manager v1.3.14 h1:qK5wFQhEkZosPd/rvlcVHiSc+5ZCl+LEgpKk2a+9wKw= -github.com/hyperledger/firefly-transaction-manager v1.3.14/go.mod h1:N3BoHh8+dWG710oQKuNiXmJNEOBBeLTsQ8GpZ41vhog= +github.com/hyperledger/firefly-transaction-manager v1.3.15 h1:IyWIId+uytqjIRMxROk5OqOcdHMzJFGFKpQQybiISOU= +github.com/hyperledger/firefly-transaction-manager v1.3.15/go.mod h1:N3BoHh8+dWG710oQKuNiXmJNEOBBeLTsQ8GpZ41vhog= github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= diff --git a/internal/ethereum/blocklistener_test.go b/internal/ethereum/blocklistener_test.go index 261d9be..3cde4bc 100644 --- a/internal/ethereum/blocklistener_test.go +++ b/internal/ethereum/blocklistener_test.go @@ -992,6 +992,41 @@ func TestBlockListenerProcessNonStandardHashRejectedWhenNotInHederaCompatibility } +func TestBlockListenerProcessNonStandardHashRejectedWhenWrongSizeForHedera(t *testing.T) { + + _, c, mRPC, done := newTestConnector(t) + bl := c.blockListener + bl.blockPollingInterval = 1 * time.Microsecond + bl.hederaCompatibilityMode = true + + block1003Hash := ethtypes.MustNewHexBytes0xPrefix("0xef") + + mRPC.On("CallRPC", mock.Anything, mock.Anything, "eth_blockNumber").Return(nil).Run(func(args mock.Arguments) { + hbh := args[1].(*ethtypes.HexInteger) + *hbh = *ethtypes.NewHexInteger64(1000) + }).Once() + mRPC.On("CallRPC", mock.Anything, mock.Anything, "eth_newBlockFilter").Return(nil).Run(func(args mock.Arguments) { + hbh := args[1].(*string) + *hbh = "filter_id1" + }) + mRPC.On("CallRPC", mock.Anything, mock.Anything, "eth_getFilterChanges", "filter_id1").Return(nil).Run(func(args mock.Arguments) { + hbh := args[1].(*[]ethtypes.HexBytes0xPrefix) + *hbh = []ethtypes.HexBytes0xPrefix{ + block1003Hash, + } + }).Once() + mRPC.On("CallRPC", mock.Anything, mock.Anything, "eth_getFilterChanges", mock.Anything).Return(nil).Run(func(args mock.Arguments) { + go done() // Close after we've processed the log + }) + + bl.checkStartedLocked() + + c.WaitClosed() + + mRPC.AssertExpectations(t) + +} + func TestBlockListenerProcessNonStandardHashAcceptedWhenInHederaCompatbilityMode(t *testing.T) { _, c, mRPC, done := newTestConnector(t) diff --git a/internal/ethereum/event_enricher.go b/internal/ethereum/event_enricher.go new file mode 100644 index 0000000..fda38c2 --- /dev/null +++ b/internal/ethereum/event_enricher.go @@ -0,0 +1,144 @@ +// Copyright © 2024 Kaleido, Inl.c. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ethereum + +import ( + "bytes" + "context" + + "github.com/hyperledger/firefly-common/pkg/fftypes" + "github.com/hyperledger/firefly-common/pkg/log" + "github.com/hyperledger/firefly-signer/pkg/abi" + "github.com/hyperledger/firefly-signer/pkg/ethtypes" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" +) + +type eventEnricher struct { + connector *ethConnector + extractSigner bool +} + +func (ee *eventEnricher) filterEnrichEthLog(ctx context.Context, f *eventFilter, methods []*abi.Entry, ethLog *logJSONRPC) (_ *ffcapi.Event, matched bool, decoded bool, err error) { + + // Check the block for this event is at our high water mark, as we might have rewound for other listeners + blockNumber := ethLog.BlockNumber.BigInt().Int64() + transactionIndex := ethLog.TransactionIndex.BigInt().Int64() + logIndex := ethLog.LogIndex.BigInt().Int64() + protoID := getEventProtoID(blockNumber, transactionIndex, logIndex) + + // Apply a post-filter check to the event + topicMatches := len(ethLog.Topics) > 0 && bytes.Equal(ethLog.Topics[0], f.Topic0) + addrMatches := f.Address == nil || bytes.Equal(ethLog.Address[:], f.Address[:]) + if !topicMatches || !addrMatches { + log.L(ctx).Debugf("skipping event '%s' topicMatches=%t addrMatches=%t", protoID, topicMatches, addrMatches) + return nil, matched, decoded, nil + } + matched = true + + log.L(ctx).Infof("detected event '%s'", protoID) + data, decoded := ee.decodeLogData(ctx, f.Event, ethLog.Topics, ethLog.Data) + + info := eventInfo{ + logJSONRPC: *ethLog, + } + + var timestamp *fftypes.FFTime + if ee.connector.eventBlockTimestamps { + bi, err := ee.connector.blockListener.getBlockInfoByHash(ctx, ethLog.BlockHash.String()) + if bi == nil || err != nil { + log.L(ctx).Errorf("Failed to get block info timestamp for block '%s': %v", ethLog.BlockHash, err) + return nil, matched, decoded, err // This is an error condition, rather than just something we cannot enrich + } + timestamp = fftypes.UnixTime(bi.Timestamp.BigInt().Int64()) + } + + if len(methods) > 0 || ee.extractSigner { + txInfo, err := ee.connector.getTransactionInfo(ctx, ethLog.TransactionHash) + if txInfo == nil || err != nil { + if txInfo == nil { + log.L(ctx).Errorf("Failed to get transaction info for TX '%s': transaction hash not found", ethLog.TransactionHash) + } else { + log.L(ctx).Errorf("Failed to get transaction info for TX '%s': %v", ethLog.TransactionHash, err) + } + return nil, matched, decoded, err // This is an error condition, rather than just something we cannot enrich + } + if ee.extractSigner { + info.InputSigner = txInfo.From + } + if len(methods) > 0 { + ee.matchMethod(ctx, methods, txInfo, &info) + } + } + + signature := f.Signature + return &ffcapi.Event{ + ID: ffcapi.EventID{ + Signature: signature, + BlockHash: ethLog.BlockHash.String(), + TransactionHash: ethLog.TransactionHash.String(), + BlockNumber: fftypes.FFuint64(blockNumber), + TransactionIndex: fftypes.FFuint64(transactionIndex), + LogIndex: fftypes.FFuint64(logIndex), + Timestamp: timestamp, + }, + Info: &info, + Data: data, + }, matched, decoded, nil +} + +func (ee *eventEnricher) decodeLogData(ctx context.Context, event *abi.Entry, topics []ethtypes.HexBytes0xPrefix, data ethtypes.HexBytes0xPrefix) (*fftypes.JSONAny, bool) { + var b []byte + v, err := event.DecodeEventDataCtx(ctx, topics, data) + if err == nil { + b, err = ee.connector.serializer.SerializeJSONCtx(ctx, v) + } + if err != nil { + log.L(ctx).Errorf("Failed to process event log: %s", err) + return nil, false + } + return fftypes.JSONAnyPtrBytes(b), true +} + +func (ee *eventEnricher) matchMethod(ctx context.Context, methods []*abi.Entry, txInfo *txInfoJSONRPC, info *eventInfo) { + if len(txInfo.Input) < 4 { + log.L(ctx).Debugf("No function selector available for TX '%s'", txInfo.Hash) + return + } + functionID := txInfo.Input[0:4] + var method *abi.Entry + for _, m := range methods { + if bytes.Equal(m.FunctionSelectorBytes(), functionID) { + method = m + break + } + } + if method == nil { + log.L(ctx).Debugf("Function selector '%s' for TX '%s' does not match any of the supplied methods", functionID.String(), txInfo.Hash) + return + } + info.InputMethod = method.String() + v, err := method.DecodeCallDataCtx(ctx, txInfo.Input) + var b []byte + if err == nil { + b, err = ee.connector.serializer.SerializeJSONCtx(ctx, v) + } + if err != nil { + log.L(ctx).Warnf("Failed to decode input for TX '%s' using '%s'", txInfo.Hash, info.InputMethod) + return + } + info.InputArgs = fftypes.JSONAnyPtrBytes(b) +} diff --git a/internal/ethereum/event_listener.go b/internal/ethereum/event_listener.go index d16aa7d..0559762 100644 --- a/internal/ethereum/event_listener.go +++ b/internal/ethereum/event_listener.go @@ -17,7 +17,6 @@ package ethereum import ( - "bytes" "context" "encoding/json" "math/big" @@ -59,6 +58,7 @@ type listener struct { id *fftypes.UUID c *ethConnector es *eventStream + ee *eventEnricher hwmMux sync.Mutex // Protects checkpoint of an individual listener. May hold ES lock when taking this, must NOT attempt to obtain ES lock while holding this hwmBlock int64 config listenerConfig @@ -239,124 +239,29 @@ func (l *listener) listenerCatchupLoop() { } } -func (l *listener) decodeLogData(ctx context.Context, event *abi.Entry, topics []ethtypes.HexBytes0xPrefix, data ethtypes.HexBytes0xPrefix) *fftypes.JSONAny { - var b []byte - v, err := event.DecodeEventDataCtx(ctx, topics, data) - if err == nil { - b, err = l.c.serializer.SerializeJSONCtx(ctx, v) - } - if err != nil { - log.L(ctx).Errorf("Failed to process event log: %s", err) - return nil - } - return fftypes.JSONAnyPtrBytes(b) -} - -func (l *listener) matchMethod(ctx context.Context, methods []*abi.Entry, txInfo *txInfoJSONRPC, info *eventInfo) { - if len(txInfo.Input) < 4 { - log.L(ctx).Debugf("No function selector available for TX '%s'", txInfo.Hash) - return - } - functionID := txInfo.Input[0:4] - var method *abi.Entry - for _, m := range methods { - if bytes.Equal(m.FunctionSelectorBytes(), functionID) { - method = m - break - } - } - if method == nil { - log.L(ctx).Debugf("Function selector '%s' for TX '%s' does not match any of the supplied methods", functionID.String(), txInfo.Hash) - return - } - info.InputMethod = method.String() - v, err := method.DecodeCallDataCtx(ctx, txInfo.Input) - var b []byte - if err == nil { - b, err = l.c.serializer.SerializeJSONCtx(ctx, v) - } - if err != nil { - log.L(ctx).Warnf("Failed to decode input for TX '%s' using '%s'", txInfo.Hash, info.InputMethod) - return - } - info.InputArgs = fftypes.JSONAnyPtrBytes(b) -} - -func (l *listener) filterEnrichEthLog(ctx context.Context, f *eventFilter, ethLog *logJSONRPC) (*ffcapi.ListenerEvent, bool, error) { +func (l *listener) filterEnrichEthLog(ctx context.Context, f *eventFilter, methods []*abi.Entry, ethLog *logJSONRPC) (*ffcapi.ListenerEvent, bool, error) { // Check the block for this event is at our high water mark, as we might have rewound for other listeners blockNumber := ethLog.BlockNumber.BigInt().Int64() transactionIndex := ethLog.TransactionIndex.BigInt().Int64() logIndex := ethLog.LogIndex.BigInt().Int64() - protoID := getEventProtoID(blockNumber, transactionIndex, logIndex) if blockNumber < l.hwmBlock { - log.L(ctx).Debugf("Listener %s already delivered event '%s' hwm=%d", l.id, protoID, l.hwmBlock) + log.L(ctx).Debugf("Listener %s already delivered event '%s' hwm=%d", l.id, getEventProtoID(blockNumber, transactionIndex, logIndex), l.hwmBlock) return nil, false, nil } - // Apply a post-filter check to the event - topicMatches := len(ethLog.Topics) > 0 && bytes.Equal(ethLog.Topics[0], f.Topic0) - addrMatches := f.Address == nil || bytes.Equal(ethLog.Address[:], f.Address[:]) - if !topicMatches || !addrMatches { - log.L(ctx).Debugf("Listener %s skipping event '%s' topicMatches=%t addrMatches=%t", l.id, protoID, topicMatches, addrMatches) - return nil, false, nil + e, matched, _, err := l.ee.filterEnrichEthLog(ctx, f, methods, ethLog) + if !matched || err != nil { + return nil, false, err } - log.L(ctx).Infof("Listener %s detected event '%s'", l.id, protoID) - data := l.decodeLogData(ctx, f.Event, ethLog.Topics, ethLog.Data) - - info := eventInfo{ - logJSONRPC: *ethLog, - } - - var timestamp *fftypes.FFTime - if l.c.eventBlockTimestamps { - bi, err := l.c.blockListener.getBlockInfoByHash(ctx, ethLog.BlockHash.String()) - if bi == nil || err != nil { - log.L(ctx).Errorf("Failed to get block info timestamp for block '%s': %v", ethLog.BlockHash, err) - return nil, false, err // This is an error condition, rather than just something we cannot enrich - } - timestamp = fftypes.UnixTime(bi.Timestamp.BigInt().Int64()) - } - - if len(l.config.options.Methods) > 0 || l.config.options.Signer { - txInfo, err := l.c.getTransactionInfo(ctx, ethLog.TransactionHash) - if txInfo == nil || err != nil { - if txInfo == nil { - log.L(ctx).Errorf("Failed to get transaction info for TX '%s': transaction hash not found", ethLog.TransactionHash) - } else { - log.L(ctx).Errorf("Failed to get transaction info for TX '%s': %v", ethLog.TransactionHash, err) - } - return nil, false, err // This is an error condition, rather than just something we cannot enrich - } - if l.config.options.Signer { - info.InputSigner = txInfo.From - } - if len(l.config.options.Methods) > 0 { - l.matchMethod(ctx, l.config.options.Methods, txInfo, &info) - } - } - - signature := f.Signature + e.ID.ListenerID = l.id return &ffcapi.ListenerEvent{ Checkpoint: &listenerCheckpoint{ Block: blockNumber, TransactionIndex: transactionIndex, LogIndex: logIndex, }, - Event: &ffcapi.Event{ - ID: ffcapi.EventID{ - ListenerID: l.id, - Signature: signature, - BlockHash: ethLog.BlockHash.String(), - TransactionHash: ethLog.TransactionHash.String(), - BlockNumber: fftypes.FFuint64(blockNumber), - TransactionIndex: fftypes.FFuint64(transactionIndex), - LogIndex: fftypes.FFuint64(logIndex), - Timestamp: timestamp, - }, - Info: &info, - Data: data, - }, + Event: e, }, true, nil } diff --git a/internal/ethereum/event_listener_test.go b/internal/ethereum/event_listener_test.go index ff104f6..f94fd50 100644 --- a/internal/ethereum/event_listener_test.go +++ b/internal/ethereum/event_listener_test.go @@ -392,8 +392,9 @@ func TestDecodeLogDataFail(t *testing.T) { err := json.Unmarshal([]byte(abiTransferEvent), &abiEvent) assert.NoError(t, err) - res := l.decodeLogData(l.es.ctx, abiEvent, []ethtypes.HexBytes0xPrefix{}, nil) + res, decoded := l.ee.decodeLogData(l.es.ctx, abiEvent, []ethtypes.HexBytes0xPrefix{}, nil) assert.Nil(t, res) + assert.False(t, decoded) } @@ -405,8 +406,9 @@ func TestSerializeEventDataFail(t *testing.T) { err := json.Unmarshal([]byte(abiTransferEvent), &abiEvent) assert.NoError(t, err) - res := l.decodeLogData(l.es.ctx, abiEvent, []ethtypes.HexBytes0xPrefix{}, nil) + res, decoded := l.ee.decodeLogData(l.es.ctx, abiEvent, []ethtypes.HexBytes0xPrefix{}, nil) assert.Nil(t, res) + assert.False(t, decoded) } @@ -419,7 +421,7 @@ func TestFilterEnrichEthLogBlockBelowHWM(t *testing.T) { assert.NoError(t, err) l.hwmBlock = 2 - _, ok, err := l.filterEnrichEthLog(context.Background(), l.config.filters[0], &logJSONRPC{ + _, ok, err := l.filterEnrichEthLog(context.Background(), l.config.filters[0], l.config.options.Methods, &logJSONRPC{ BlockNumber: ethtypes.NewHexInteger64(1), }) assert.NoError(t, err) @@ -435,7 +437,7 @@ func TestFilterEnrichEthLogAddressMismatch(t *testing.T) { err := json.Unmarshal([]byte(abiTransferEvent), &abiEvent) assert.NoError(t, err) - _, ok, err := l.filterEnrichEthLog(context.Background(), l.config.filters[0], &logJSONRPC{ + _, ok, err := l.filterEnrichEthLog(context.Background(), l.config.filters[0], l.config.options.Methods, &logJSONRPC{ Address: ethtypes.MustNewAddress("0x20355f3e852d4b6a9944ada8d5399ddd3409a431"), }) assert.NoError(t, err) @@ -467,11 +469,11 @@ func TestFilterEnrichEthLogMethodInputsOk(t *testing.T) { } }).Once() // 1 cache miss and hit - ev, ok, err := l.filterEnrichEthLog(context.Background(), l.config.filters[0], sampleTransferLog()) // cache miss + ev, ok, err := l.filterEnrichEthLog(context.Background(), l.config.filters[0], l.config.options.Methods, sampleTransferLog()) // cache miss assert.True(t, ok) assert.NoError(t, err) - ev, ok, err = l.filterEnrichEthLog(context.Background(), l.config.filters[0], sampleTransferLog()) // cache hit + ev, ok, err = l.filterEnrichEthLog(context.Background(), l.config.filters[0], l.config.options.Methods, sampleTransferLog()) // cache hit assert.True(t, ok) assert.NoError(t, err) ei := ev.Event.Info.(*eventInfo) @@ -506,7 +508,7 @@ func TestFilterEnrichEthLogMethodInputsTxInfoWithErr(t *testing.T) { } }).Once() - _, ok, err := l.filterEnrichEthLog(context.Background(), l.config.filters[0], sampleTransferLog()) + _, ok, err := l.filterEnrichEthLog(context.Background(), l.config.filters[0], l.config.options.Methods, sampleTransferLog()) assert.False(t, ok) assert.Error(t, err) @@ -527,7 +529,7 @@ func TestFilterEnrichEthLogTXInfoFail(t *testing.T) { return th.String() == "0x1a1f797ee000c529b6a2dd330cedd0d081417a30d16a4eecb3f863ab4657246f" })).Return(&rpcbackend.RPCError{Message: "pop2"}) - _, ok, err := l.filterEnrichEthLog(context.Background(), l.config.filters[0], sampleTransferLog()) + _, ok, err := l.filterEnrichEthLog(context.Background(), l.config.filters[0], l.config.options.Methods, sampleTransferLog()) assert.False(t, ok) assert.Regexp(t, "pop1", err) @@ -552,7 +554,7 @@ func TestFilterEnrichEthLogTXTimestampFail(t *testing.T) { return th.String() == "0x1a1f797ee000c529b6a2dd330cedd0d081417a30d16a4eecb3f863ab4657246f" })).Return(&rpcbackend.RPCError{Message: "pop2"}) - _, ok, err := l.filterEnrichEthLog(context.Background(), l.config.filters[0], sampleTransferLog()) + _, ok, err := l.filterEnrichEthLog(context.Background(), l.config.filters[0], l.config.options.Methods, sampleTransferLog()) assert.False(t, ok) assert.Regexp(t, "pop2", err) @@ -582,7 +584,7 @@ func TestFilterEnrichEthLogMethodBadInputTooShort(t *testing.T) { } }) - ev, ok, err := l.filterEnrichEthLog(context.Background(), l.config.filters[0], sampleTransferLog()) + ev, ok, err := l.filterEnrichEthLog(context.Background(), l.config.filters[0], l.config.options.Methods, sampleTransferLog()) assert.True(t, ok) assert.NoError(t, err) ei := ev.Event.Info.(*eventInfo) @@ -614,7 +616,7 @@ func TestFilterEnrichEthLogMethodBadInputTooMismatchFunctionID(t *testing.T) { } }) - ev, ok, err := l.filterEnrichEthLog(context.Background(), l.config.filters[0], sampleTransferLog()) + ev, ok, err := l.filterEnrichEthLog(context.Background(), l.config.filters[0], l.config.options.Methods, sampleTransferLog()) assert.True(t, ok) assert.NoError(t, err) ei := ev.Event.Info.(*eventInfo) @@ -646,7 +648,7 @@ func TestFilterEnrichEthLogMethodBadInputABIData(t *testing.T) { } }) - ev, ok, err := l.filterEnrichEthLog(context.Background(), l.config.filters[0], sampleTransferLog()) + ev, ok, err := l.filterEnrichEthLog(context.Background(), l.config.filters[0], l.config.options.Methods, sampleTransferLog()) assert.NoError(t, err) assert.True(t, ok) ei := ev.Event.Info.(*eventInfo) diff --git a/internal/ethereum/event_stream.go b/internal/ethereum/event_stream.go index 92bf18a..0d4fd57 100644 --- a/internal/ethereum/event_stream.go +++ b/internal/ethereum/event_stream.go @@ -88,10 +88,8 @@ func parseEventFilters(ctx context.Context, filters []fftypes.JSONAny) (string, if ethFilters[i].Event == nil { return "", nil, i18n.NewError(ctx, msgs.MsgMissingEventFilter) } - if err == nil { - ethFilters[i].Topic0, err = ethFilters[i].Event.SignatureHashCtx(ctx) - ethFilters[i].Signature = ethFilters[i].Event.String() - } + ethFilters[i].Topic0, err = ethFilters[i].Event.SignatureHashCtx(ctx) + ethFilters[i].Signature = ethFilters[i].Event.String() if err != nil { return "", nil, i18n.NewError(ctx, msgs.MsgInvalidEventFilter, err) } @@ -147,6 +145,10 @@ func (es *eventStream) addEventListener(ctx context.Context, req *ffcapi.EventLi signature: signature, }, } + l.ee = &eventEnricher{ + connector: l.c, + extractSigner: l.config.options.Signer, + } if checkpoint != nil { l.hwmBlock = checkpoint.Block } @@ -513,7 +515,7 @@ func (es *eventStream) filterEnrichSort(ctx context.Context, ag *aggregatedListe listeners := ag.listenersByTopic0[ethLog.Topics[0].String()] for _, l := range listeners { for _, f := range l.config.filters { - lu, matches, err := l.filterEnrichEthLog(ctx, f, ethLog) + lu, matches, err := l.filterEnrichEthLog(ctx, f, l.config.options.Methods, ethLog) if err != nil { return nil, err } diff --git a/internal/ethereum/get_block_info.go b/internal/ethereum/get_block_info.go index d33015e..a1f7570 100644 --- a/internal/ethereum/get_block_info.go +++ b/internal/ethereum/get_block_info.go @@ -26,7 +26,7 @@ import ( func (c *ethConnector) BlockInfoByNumber(ctx context.Context, req *ffcapi.BlockInfoByNumberRequest) (*ffcapi.BlockInfoByNumberResponse, ffcapi.ErrorReason, error) { - blockInfo, reason, err := c.blockListener.getBlockInfoByNumber(ctx, req.BlockNumber.Int64(), true, req.ExpectedParentHash) + blockInfo, reason, err := c.blockListener.getBlockInfoByNumber(ctx, req.BlockNumber.Int64(), req.AllowCache, req.ExpectedParentHash) if err != nil { return nil, reason, err } diff --git a/internal/ethereum/get_block_info_test.go b/internal/ethereum/get_block_info_test.go index 5e34a38..3f32173 100644 --- a/internal/ethereum/get_block_info_test.go +++ b/internal/ethereum/get_block_info_test.go @@ -88,7 +88,7 @@ func TestGetBlockInfoByNumberOK(t *testing.T) { }). Twice() // two cache misses and a hit - var req ffcapi.BlockInfoByNumberRequest + req := ffcapi.BlockInfoByNumberRequest{AllowCache: true} err := json.Unmarshal([]byte(sampleGetBlockInfoByNumber), &req) assert.NoError(t, err) res, reason, err := c.BlockInfoByNumber(ctx, &req) diff --git a/internal/ethereum/get_receipt.go b/internal/ethereum/get_receipt.go index eea47b0..c04ec36 100644 --- a/internal/ethereum/get_receipt.go +++ b/internal/ethereum/get_receipt.go @@ -28,6 +28,7 @@ import ( "github.com/hyperledger/firefly-common/pkg/i18n" "github.com/hyperledger/firefly-common/pkg/log" "github.com/hyperledger/firefly-evmconnect/internal/msgs" + "github.com/hyperledger/firefly-signer/pkg/abi" "github.com/hyperledger/firefly-signer/pkg/ethtypes" "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" ) @@ -183,7 +184,26 @@ func (c *ethConnector) getErrorInfo(ctx context.Context, transactionHash string, return &revertReason, &errorMessage } -func (c *ethConnector) TransactionReceipt(ctx context.Context, req *ffcapi.TransactionReceiptRequest) (*ffcapi.TransactionReceiptResponse, ffcapi.ErrorReason, error) { +func (c *ethConnector) TransactionReceipt(ctx context.Context, req *ffcapi.TransactionReceiptRequest) (_ *ffcapi.TransactionReceiptResponse, _ ffcapi.ErrorReason, err error) { + + var filters []*eventFilter + var methods []*abi.Entry + if len(req.EventFilters) > 0 { + // We need to post-process the logs and build a list of events + _, filters, err = parseEventFilters(ctx, req.EventFilters) + if err != nil { + return nil, ffcapi.ErrorReasonInvalidInputs, err + } + } + if len(req.Methods) > 0 { + methods = make([]*abi.Entry, len(req.Methods)) + for i, m := range req.Methods { + if err := json.Unmarshal(m.Bytes(), &methods[i]); err != nil { + err = i18n.NewError(ctx, msgs.MsgUnmarshalABIMethodFail, err) + return nil, ffcapi.ErrorReasonInvalidInputs, err + } + } + } // Get the receipt in the back-end JSON/RPC format var ethReceipt *txReceiptJSONRPC @@ -203,7 +223,6 @@ func (c *ethConnector) TransactionReceipt(ctx context.Context, req *ffcapi.Trans returnDataString, transactionErrorMessage = c.getErrorInfo(ctx, req.TransactionHash, ethReceipt.RevertReason) } - ethReceipt.Logs = nil fullReceipt, _ := json.Marshal(&receiptExtraInfo{ ContractAddress: ethReceipt.ContractAddress, CumulativeGasUsed: (*fftypes.FFBigInt)(ethReceipt.CumulativeGasUsed), @@ -220,12 +239,45 @@ func (c *ethConnector) TransactionReceipt(ctx context.Context, req *ffcapi.Trans txIndex = ethReceipt.TransactionIndex.BigInt().Int64() } receiptResponse := &ffcapi.TransactionReceiptResponse{ - BlockNumber: (*fftypes.FFBigInt)(ethReceipt.BlockNumber), - TransactionIndex: fftypes.NewFFBigInt(txIndex), - BlockHash: ethReceipt.BlockHash.String(), - Success: isSuccess, - ProtocolID: ProtocolIDForReceipt((*fftypes.FFBigInt)(ethReceipt.BlockNumber), fftypes.NewFFBigInt(txIndex)), - ExtraInfo: fftypes.JSONAnyPtrBytes(fullReceipt), + TransactionReceiptResponseBase: ffcapi.TransactionReceiptResponseBase{ + + BlockNumber: (*fftypes.FFBigInt)(ethReceipt.BlockNumber), + TransactionIndex: fftypes.NewFFBigInt(txIndex), + BlockHash: ethReceipt.BlockHash.String(), + Success: isSuccess, + ProtocolID: ProtocolIDForReceipt((*fftypes.FFBigInt)(ethReceipt.BlockNumber), fftypes.NewFFBigInt(txIndex)), + ExtraInfo: fftypes.JSONAnyPtrBytes(fullReceipt), + }, + } + if req.IncludeLogs { + receiptResponse.Logs = make([]fftypes.JSONAny, len(ethReceipt.Logs)) + for i, l := range ethReceipt.Logs { + b, _ := json.Marshal(l) // no error injectable here as we unmarshalled to a struct we control + receiptResponse.Logs[i] = *fftypes.JSONAnyPtrBytes(b) + } + } + // Try to decode the events etc. if we have filters supplied + if len(filters) > 0 { + ee := &eventEnricher{ + connector: c, + extractSigner: req.ExtractSigner, + } + for _, ethLog := range ethReceipt.Logs { + var bestMatch *ffcapi.Event + for _, f := range filters { + event, matches, decoded, err := ee.filterEnrichEthLog(ctx, f, methods, ethLog) + // If we matched and decoded, this is our best match (overriding any earlier) + // If we only matched, then don't override a match+decode. + // Example: ERC-20 & ERC-721 ABIs - both match, but only one decodes + if (matches && err == nil) && (decoded || bestMatch == nil) { + bestMatch = event + } + } + if bestMatch != nil { + receiptResponse.Events = append(receiptResponse.Events, bestMatch) + } + } + } if ethReceipt.ContractAddress != nil { location, _ := json.Marshal(map[string]string{ diff --git a/internal/ethereum/get_receipt_test.go b/internal/ethereum/get_receipt_test.go index 4289337..2682a0a 100644 --- a/internal/ethereum/get_receipt_test.go +++ b/internal/ethereum/get_receipt_test.go @@ -18,6 +18,7 @@ package ethereum import ( "encoding/json" + "fmt" "testing" "github.com/hyperledger/firefly-common/pkg/fftypes" @@ -48,9 +49,11 @@ const sampleJSONRPCReceipt = `{ { "address": "0x302259069aaa5b10dc6f29a9a3f72a8e52837cc3", "topics": [ - "0x805721bc246bccc732581be0c0aa2dd8f7ec93e97ba4b307be84428c98b0a12f" + "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000005dae1910885cde875de559333d12722357e69c42" ], - "data": "0x0000000000000000000000002b1c769ef5ad304a4889f2a07a6617cd935849ae00000000000000000000000000000000000000000000000000000000625829cc00000000000000000000000000000000000000000000000000000000000000e01f64cabbf2b44bff810396f2cb08186c2d460c2bd1c44058bc058267d554e724973b16c67dbcade6c509329de6aad8037bb024b7a996129f731b9f68ac5fcd9f00000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000180000000000000000000000000000000000000000000000000000000000000000966665f73797374656d0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002e516d546a587065445154326a377063583145347445764379334665554a71744374737036464c5762535553724a4e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000014a1ad4027f59715bca7fd30dc0121be0542c713f7a2470c415e8b1d9e7df372c", + "data": "0x000000000000000000000000000000000000000000000000016345785d8a0000", "blockNumber": "0x5", "transactionHash": "0x7d48ae971faf089878b57e3c28e3035540d34f38af395958d2c73c36c57c83a2", "transactionIndex": "0x0", @@ -197,6 +200,25 @@ const sampleTransactionTraceBesu = `{ ] }` +const sampleTransactionInputJSONRPC = `{ + "blockHash": "0xa6d9ef8afd65f187e43d1ebd378681bf79663920da0563cd8c06b49dd1db8758", + "blockNumber": "0x6790", + "chainId": "0x3dbb0ab", + "from": "0xa61465d0d19d842d73625cb7a2b6f318c74d304b", + "gas": "0xd900", + "gasPrice": "0x0", + "hash": "0x7d48ae971faf089878b57e3c28e3035540d34f38af395958d2c73c36c57c83a2", + "input": "0x40c10f190000000000000000000000005dae1910885cde875de559333d12722357e69c42000000000000000000000000000000000000000000000000016345785d8a0000", + "nonce": "0xc", + "to": "0xd0685a91ae2d4b0ec4701f7a9787c6633790a65e", + "transactionIndex": "0x0", + "type": "0x0", + "value": "0x0", + "v": "0x7b76179", + "r": "0x963a8620b31ac796dd605f37c7f386d039f40c3a31854931fae5e3c95b1faf7a", + "s": "0x58bb39fa958611c123adbef40eaaba44d36cddf77532064a437f0840242e5d30" +}` + func TestGetReceiptOkSuccess(t *testing.T) { ctx, c, mRPC, done := newTestConnector(t) @@ -544,3 +566,136 @@ func TestProtocolIDForReceipt(t *testing.T) { assert.Equal(t, "000000012345/000042", ProtocolIDForReceipt(fftypes.NewFFBigInt(12345), fftypes.NewFFBigInt(42))) assert.Equal(t, "", ProtocolIDForReceipt(nil, nil)) } + +func TestGetReceiptEventDecodeOK(t *testing.T) { + + ctx, c, mRPC, done := newTestConnector(t) + defer done() + + mRPC.On("CallRPC", mock.Anything, mock.Anything, "eth_getTransactionReceipt", + "0x7d48ae971faf089878b57e3c28e3035540d34f38af395958d2c73c36c57c83a2"). + Return(nil). + Run(func(args mock.Arguments) { + err := json.Unmarshal([]byte(sampleJSONRPCReceipt), args[1]) + assert.NoError(t, err) + }) + mRPC.On("CallRPC", mock.Anything, mock.Anything, "eth_getBlockByHash", + "0x6197ef1a58a2a592bb447efb651f0db7945de21aa8048801b250bd7b7431f9b6", + false). + Return(nil). + Run(func(args mock.Arguments) { + err := json.Unmarshal([]byte(sampleBlockJSONRPC), args[1]) + assert.NoError(t, err) + }) + mRPC.On("CallRPC", mock.Anything, mock.Anything, "eth_getTransactionByHash", mock.Anything). + Return(nil). + Run(func(args mock.Arguments) { + err := json.Unmarshal([]byte(sampleTransactionInputJSONRPC), args[1]) + assert.NoError(t, err) + }) + + req := ffcapi.TransactionReceiptRequest{ + TransactionHash: "0x7d48ae971faf089878b57e3c28e3035540d34f38af395958d2c73c36c57c83a2", + IncludeLogs: true, + Methods: []fftypes.JSONAny{`{ + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "mint", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }`}, + ExtractSigner: true, + EventFilters: []fftypes.JSONAny{*fftypes.JSONAnyPtr(`{ + "event": { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "from", + "type": "address" + }, + { + "indexed": true, + "name": "to", + "type": "address" + }, + { + "indexed": false, + "name": "value", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + } + }`)}, + } + res, reason, err := c.TransactionReceipt(ctx, &req) + assert.NoError(t, err) + assert.Empty(t, reason) + + assert.True(t, res.Success) + assert.Equal(t, int64(1977), res.BlockNumber.Int64()) + assert.Equal(t, int64(30), res.TransactionIndex.Int64()) + + assert.Len(t, res.Logs, 1) + assert.Len(t, res.Events, 1) + b, err := json.Marshal(res.Events[0].Data) + assert.NoError(t, err) + fmt.Println(string(b)) + assert.JSONEq(t, `{ + "from": "0x0000000000000000000000000000000000000000", + "to": "0x5dae1910885cde875de559333d12722357e69c42", + "value": "100000000000000000" + }`, string(b)) + b = res.Events[0].Info.(*eventInfo).InputArgs.Bytes() + assert.JSONEq(t, `{ + "to": "0x5dae1910885cde875de559333d12722357e69c42", + "amount": "100000000000000000" + }`, string(b)) + assert.Equal(t, "0xa61465d0d19d842d73625cb7a2b6f318c74d304b", res.Events[0].Info.(*eventInfo).InputSigner.String()) + +} + +func TestGetReceiptEventInvalidFilters(t *testing.T) { + + ctx, c, _, done := newTestConnector(t) + defer done() + + req := ffcapi.TransactionReceiptRequest{ + TransactionHash: "0x7d48ae971faf089878b57e3c28e3035540d34f38af395958d2c73c36c57c83a2", + IncludeLogs: true, + EventFilters: []fftypes.JSONAny{*fftypes.JSONAnyPtr(`!! wrong`)}, + } + _, reason, err := c.TransactionReceipt(ctx, &req) + assert.Regexp(t, "FF23036", err) + assert.Equal(t, ffcapi.ErrorReasonInvalidInputs, reason) + +} + +func TestGetReceiptEventInvalidMethods(t *testing.T) { + + ctx, c, _, done := newTestConnector(t) + defer done() + + req := ffcapi.TransactionReceiptRequest{ + TransactionHash: "0x7d48ae971faf089878b57e3c28e3035540d34f38af395958d2c73c36c57c83a2", + IncludeLogs: true, + Methods: []fftypes.JSONAny{*fftypes.JSONAnyPtr(`!! wrong`)}, + } + _, reason, err := c.TransactionReceipt(ctx, &req) + assert.Regexp(t, "FF23013", err) + assert.Equal(t, ffcapi.ErrorReasonInvalidInputs, reason) + +}