diff --git a/.vscode/launch.json b/.vscode/launch.json index 7fbed18..6a9cea9 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,22 +1,22 @@ { - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "name": "Run evmconnect", - "type": "go", - "request": "launch", - "mode": "auto", - "program": "${workspaceFolder}/evmconnect/main.go", - "args": [ - "-f", - "${workspaceFolder}/evmconnect_config.yml" - ], - "env": { - "FIREFLY_PERSISTENCE_LEVELDB_PATH": "${workspaceFolder}/.leveldb" - } + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Run evmconnect", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "${workspaceFolder}/evmconnect/main.go", + "args": [ + "-f", + "${workspaceFolder}/evmconnect_config.yml" + ], + "env": { + "FIREFLY_PERSISTENCE_LEVELDB_PATH": "${workspaceFolder}/.leveldb" } - ] + } + ] } \ No newline at end of file diff --git a/README.md b/README.md index 4ea1e92..ea7f2d8 100644 --- a/README.md +++ b/README.md @@ -61,9 +61,6 @@ For EVM connector to function properly, you should check the blockchain node sup - `eth_getBalance` - `eth_gasPrice`[^1] - - - ### Transaction submission - `eth_estimateGas` - `eth_sendTransaction` diff --git a/config.md b/config.md index 975d352..6f485a5 100644 --- a/config.md +++ b/config.md @@ -68,6 +68,7 @@ |expectContinueTimeout|See [ExpectContinueTimeout in the Go docs](https://pkg.go.dev/net/http#Transport)|[`time.Duration`](https://pkg.go.dev/time#Duration)|`1s` |gasEstimationFactor|The factor to apply to the gas estimation to determine the gas limit|float|`1.5` |headers|Adds custom headers to HTTP requests|`map[string]string`|`` +|hederaCompatibilityMode|Compatibility mode for Hedera, allowing non-standard block header hashes to be processed|`boolean`|`false` |idleTimeout|The max duration to hold a HTTP keepalive connection between calls|[`time.Duration`](https://pkg.go.dev/time#Duration)|`475ms` |maxConcurrentRequests|Maximum of concurrent requests to be submitted to the blockchain|`int`|`50` |maxConnsPerHost|The max number of connections, per unique hostname. Zero means no limit|`int`|`0` diff --git a/internal/ethereum/blocklistener.go b/internal/ethereum/blocklistener.go index af07a62..c80885c 100644 --- a/internal/ethereum/blocklistener.go +++ b/internal/ethereum/blocklistener.go @@ -49,6 +49,7 @@ type blockListener struct { blockPollingInterval time.Duration unstableHeadLength int canonicalChain *list.List + hederaCompatibilityMode bool } type minimalBlockInfo struct { @@ -67,6 +68,7 @@ func newBlockListener(ctx context.Context, c *ethConnector, conf config.Section) blockPollingInterval: conf.GetDuration(BlockPollingInterval), canonicalChain: list.New(), unstableHeadLength: int(c.checkpointBlockGap), + hederaCompatibilityMode: conf.GetBool(HederaCompatibilityMode), } return bl } @@ -140,6 +142,22 @@ func (bl *blockListener) listenLoop() { update := &ffcapi.BlockHashEvent{GapPotential: gapPotential} var notifyPos *list.Element for _, h := range blockHashes { + if len(h) != 32 { + if !bl.hederaCompatibilityMode { + log.L(bl.ctx).Errorf("Attempted to index block header with non-standard length: %d", len(h)) + failCount++ + continue + } + + if len(h) < 32 { + log.L(bl.ctx).Errorf("Cannot index block header hash of length: %d", len(h)) + failCount++ + continue + } + + h = h[0:32] + } + // Do a lookup of the block (which will then go into our cache). bi, err := bl.c.getBlockInfoByHash(bl.ctx, h.String()) switch { diff --git a/internal/ethereum/blocklistener_test.go b/internal/ethereum/blocklistener_test.go index 08663ac..8f1be9f 100644 --- a/internal/ethereum/blocklistener_test.go +++ b/internal/ethereum/blocklistener_test.go @@ -861,6 +861,81 @@ func TestBlockListenerBlockHashFailed(t *testing.T) { } +func TestBlockListenerProcessNonStandardHashRejectedWhenNotInHederaCompatibilityMode(t *testing.T) { + + _, c, mRPC, done := newTestConnector(t) + bl := c.blockListener + bl.blockPollingInterval = 1 * time.Microsecond + bl.hederaCompatibilityMode = false + + block1003Hash := ethtypes.MustNewHexBytes0xPrefix("0xef177df3b87beed681b1557e8ba7c3ecbd7e4db83d87b66c1e86aa484937ab93f1fae0eb6d4b24ca30aee13f29c83cc9") + + 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) + bl := c.blockListener + bl.blockPollingInterval = 1 * time.Microsecond + bl.hederaCompatibilityMode = true + + block1003Hash := ethtypes.MustNewHexBytes0xPrefix("0xef177df3b87beed681b1557e8ba7c3ecbd7e4db83d87b66c1e86aa484937ab93f1fae0eb6d4b24ca30aee13f29c83cc9") + truncatedBlock1003Hash := ethtypes.MustNewHexBytes0xPrefix("0xef177df3b87beed681b1557e8ba7c3ecbd7e4db83d87b66c1e86aa484937ab93") + + 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 + }) + + mRPC.On("CallRPC", mock.Anything, mock.Anything, "eth_getBlockByHash", mock.MatchedBy(func(bh string) bool { + return bh == truncatedBlock1003Hash.String() + }), false).Return(&rpcbackend.RPCError{Message: "pop"}) + + bl.checkStartedLocked() + + c.WaitClosed() + + mRPC.AssertExpectations(t) + +} + func TestBlockListenerReestablishBlockFilter(t *testing.T) { _, c, mRPC, done := newTestConnector(t) diff --git a/internal/ethereum/config.go b/internal/ethereum/config.go index 1209173..30fdd9e 100644 --- a/internal/ethereum/config.go +++ b/internal/ethereum/config.go @@ -37,6 +37,7 @@ const ( RetryFactor = "retry.factor" MaxConcurrentRequests = "maxConcurrentRequests" TxCacheSize = "txCacheSize" + HederaCompatibilityMode = "hederaCompatibilityMode" TraceTXForRevertReason = "traceTXForRevertReason" ) @@ -71,5 +72,6 @@ func InitConfig(conf config.Section) { conf.AddKnownKey(RetryMaxDelay, DefaultRetryMaxDelay) conf.AddKnownKey(MaxConcurrentRequests, 50) conf.AddKnownKey(TxCacheSize, 250) + conf.AddKnownKey(HederaCompatibilityMode, false) conf.AddKnownKey(TraceTXForRevertReason, false) } diff --git a/internal/msgs/en_config_descriptions.go b/internal/msgs/en_config_descriptions.go index 1a08bf0..fc5e664 100644 --- a/internal/msgs/en_config_descriptions.go +++ b/internal/msgs/en_config_descriptions.go @@ -40,5 +40,6 @@ var ( ConfigEventsFilterPollingInterval = ffc("config.connector.events.filterPollingInterval", "The interval between polling calls to a filter, when checking for newly arrived events", i18n.TimeDurationType) ConfigTxCacheSize = ffc("config.connector.txCacheSize", "Maximum of transactions to hold in the transaction info cache", i18n.IntType) ConfigMaxConcurrentRequests = ffc("config.connector.maxConcurrentRequests", "Maximum of concurrent requests to be submitted to the blockchain", i18n.IntType) + ConfigHederaCompatibilityMode = ffc("config.connector.hederaCompatibilityMode", "Compatibility mode for Hedera, allowing non-standard block header hashes to be processed", i18n.BooleanType) ConfigTraceTXForRevertReason = ffc("config.connector.traceTXForRevertReason", "Enable the use of transaction trace functions (e.g. debug_traceTransaction) to obtain transaction revert reasons. This can place a high load on the EVM client.", i18n.BooleanType) )