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

feature: Support for Hedera EVM block identifiers #118

Merged
merged 10 commits into from
May 20, 2024
Merged
Show file tree
Hide file tree
Changes from 5 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
38 changes: 19 additions & 19 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -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"
}
]
}
]
}
3 changes: 0 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,5 @@ require (
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

replace github.com/hyperledger/firefly-signer => github.com/SamMayWork/firefly-signer v0.0.0-20240307121035-e654fb9ef55b
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will be removed after the pre-req changes are merged

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please see review comments on hyperledger/firefly-signer#61

4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8
github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10=
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
github.com/SamMayWork/firefly-signer v0.0.0-20240307121035-e654fb9ef55b h1:XMWx/jEupPkbF1cKEvi682R23xteeNf6EZRrc0O23tk=
github.com/SamMayWork/firefly-signer v0.0.0-20240307121035-e654fb9ef55b/go.mod h1:pK6kivzBFSue3zpJSQpH67VasnLLbwBJOBUNv0zHbRA=
github.com/aidarkhanov/nanoid v1.0.8 h1:yxyJkgsEDFXP7+97vc6JevMcjyb03Zw+/9fqhlVXBXA=
github.com/aidarkhanov/nanoid v1.0.8/go.mod h1:vadfZHT+m4uDhttg0yY4wW3GKtl2T6i4d2Age+45pYk=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
Expand Down Expand Up @@ -102,8 +104,6 @@ github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU
github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/hyperledger/firefly-common v1.4.6 h1:qqXoSaRml3WjUnWcWxrrXs5AIOWa+UcMXLCF8yEa4Pk=
github.com/hyperledger/firefly-common v1.4.6/go.mod h1:jkErZdQmC9fsAJZQO427tURdwB9iiW+NMUZSqS3eBIE=
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.9 h1:lysp0ZIxd4zR53Eu+lRjEcJ1hbmHaAsIESBrmBWWito=
github.com/hyperledger/firefly-transaction-manager v1.3.9/go.mod h1:N3BoHh8+dWG710oQKuNiXmJNEOBBeLTsQ8GpZ41vhog=
github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
Expand Down
62 changes: 44 additions & 18 deletions internal/ethereum/blocklistener.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,16 +39,18 @@ type blockUpdateConsumer struct {
// 1) To establish and keep track of what the head block height of the blockchain is, so event streams know how far from the head they are
// 2) To feed new block information to any registered consumers
type blockListener struct {
ctx context.Context
c *ethConnector
listenLoopDone chan struct{}
initialBlockHeightObtained chan struct{}
highestBlock int64
mux sync.Mutex
consumers map[fftypes.UUID]*blockUpdateConsumer
blockPollingInterval time.Duration
unstableHeadLength int
canonicalChain *list.List
ctx context.Context
c *ethConnector
listenLoopDone chan struct{}
initialBlockHeightObtained chan struct{}
highestBlock int64
mux sync.Mutex
consumers map[fftypes.UUID]*blockUpdateConsumer
blockPollingInterval time.Duration
unstableHeadLength int
canonicalChain *list.List
allowNonStandardBlockHashLength bool
nonStandardBlockHashSizeResolutionMethod string
}

type minimalBlockInfo struct {
Expand All @@ -59,14 +61,16 @@ type minimalBlockInfo struct {

func newBlockListener(ctx context.Context, c *ethConnector, conf config.Section) *blockListener {
bl := &blockListener{
ctx: log.WithLogField(ctx, "role", "blocklistener"),
c: c,
initialBlockHeightObtained: make(chan struct{}),
highestBlock: -1,
consumers: make(map[fftypes.UUID]*blockUpdateConsumer),
blockPollingInterval: conf.GetDuration(BlockPollingInterval),
canonicalChain: list.New(),
unstableHeadLength: int(c.checkpointBlockGap),
ctx: log.WithLogField(ctx, "role", "blocklistener"),
c: c,
initialBlockHeightObtained: make(chan struct{}),
highestBlock: -1,
consumers: make(map[fftypes.UUID]*blockUpdateConsumer),
blockPollingInterval: conf.GetDuration(BlockPollingInterval),
canonicalChain: list.New(),
unstableHeadLength: int(c.checkpointBlockGap),
allowNonStandardBlockHashLength: conf.GetBool(AllowNonStandardBlockHashSize),
nonStandardBlockHashSizeResolutionMethod: conf.GetString(NonStandardBlockHashSizeResolutionMethod),
}
return bl
}
Expand Down Expand Up @@ -140,6 +144,28 @@ func (bl *blockListener) listenLoop() {
update := &ffcapi.BlockHashEvent{GapPotential: gapPotential}
var notifyPos *list.Element
for _, h := range blockHashes {
if len(h) != 32 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @SamMayWork I couldn't understand in the end the problem and the solution. Could do with some help.

32 is the length of a "standard" Ethereum block hash. So if we get a standard one, it will be 32 bytes in length.

You are working with a EVM implementation against a ledger that uses a 48 byte block hash - so in that case, the length here would be 48.

Why would we truncate that? Surely we just need to acommodate that it's 48 bytes, and there will be some number of places in the code that need to be relaxed to avoid the assumption of 32 bytes. Maybe for example there are DB migration tables etc.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why would we truncate that? Surely we just need to acommodate that it's 48 bytes, and there will be some number of places in the code that need to be relaxed to avoid the assumption of 32 bytes. Maybe for example there are DB migration tables etc.

There was a brief Discord discussion on this, although in retrospect I don't think we considered what other alternative approaches we could use here.

The reason for using truncation here instead of changing the code everywhere to permit the change in header size was purely to reduce the scope of the changes required. I think the view was that Hedera is EVM compatible but also a little bit weird, and it might make more sense to truncate the hashes so there's no need to change existing code for a single use-case.

We could use this PR to permit non-standard block header sizes everywhere if that's something strategically we think EVM Connect should be able to do.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Had a conversation with @peterbroadhurst on this topic, one thing this PR doesn't call out that it absolutely should be is that it is expected in the Hedera ecosystem to refer to blocks by their truncated hash. Block headers are 48 bytes, truncation down to 32 bytes for EVM compatiblity is baked into the Hedera ecosystem, so this isn't a case of FireFly doing something strange, this is what we're expected to be doing.

Additionally on the spelling of the configuration we've agreed that in this case we're processing blocks slightly differently specifically for Hedera so in that sense we're essentially running compatibility adjustments, as such we've change the name of the configuration to be hederaCompatibilityMode.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if len(h) != 32 {
if len(h) == 48 {

if !bl.allowNonStandardBlockHashLength {
log.L(bl.ctx).Errorf("Tried to index a non-standard size block hash length: %s", h.String())
failCount++
continue
}

if bl.nonStandardBlockHashSizeResolutionMethod == "" {
log.L(bl.ctx).Warnf("No non-standard hash length resolution method was provided, defaulting to truncation")
bl.nonStandardBlockHashSizeResolutionMethod = "truncate"
}

if bl.nonStandardBlockHashSizeResolutionMethod == "truncate" {
h, err = h.Truncate(32)
Copy link
Contributor

@peterbroadhurst peterbroadhurst May 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
h, err = h.Truncate(32)
h = ethtypes.HexBytes0xPrefix(h[0:32])

... related to firefly-signer change proposal. But note the question overall around the intent of this change.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep agreed here - if we go with this approach, we'll do this and then remove the requirement on the firefly-signer PR.

if err != nil {
log.L(bl.ctx).Errorf("Input hash was shorter than the standard block hash length: %s", h.String())
failCount++
continue
}
}
}

// Do a lookup of the block (which will then go into our cache).
bi, err := bl.c.getBlockInfoByHash(bl.ctx, h.String())
switch {
Expand Down
119 changes: 119 additions & 0 deletions internal/ethereum/blocklistener_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -861,6 +861,125 @@ func TestBlockListenerBlockHashFailed(t *testing.T) {

}

func TestBlockListenerProcessNonStandardHashRejected(t *testing.T) {

_, c, mRPC, done := newTestConnector(t)
bl := c.blockListener
bl.blockPollingInterval = 1 * time.Microsecond
bl.allowNonStandardBlockHashLength = 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 TestBlockListenerProcessNonStandardHashAccepted(t *testing.T) {

_, c, mRPC, done := newTestConnector(t)
bl := c.blockListener
bl.blockPollingInterval = 1 * time.Microsecond
bl.allowNonStandardBlockHashLength = true
bl.nonStandardBlockHashSizeResolutionMethod = "truncate"

block1003Hash := ethtypes.MustNewHexBytes0xPrefix("0xef177df3b87beed681b1557e8ba7c3ecbd7e4db83d87b66c1e86aa484937ab93f1fae0eb6d4b24ca30aee13f29c83cc9")
block1004Hash := ethtypes.MustNewHexBytes0xPrefix("0xef177df3b87beed681")
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,
block1004Hash,
}
}).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 TestBlockListenerProcessNonStandardHashNoResolutionMethodSpecified(t *testing.T) {

_, c, mRPC, done := newTestConnector(t)
bl := c.blockListener
bl.blockPollingInterval = 1 * time.Microsecond
bl.allowNonStandardBlockHashLength = true
bl.nonStandardBlockHashSizeResolutionMethod = ""

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)
Expand Down
36 changes: 21 additions & 15 deletions internal/ethereum/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,21 +22,23 @@ import (
)

const (
ConfigGasEstimationFactor = "gasEstimationFactor"
ConfigDataFormat = "dataFormat"
BlockPollingInterval = "blockPollingInterval"
BlockCacheSize = "blockCacheSize"
EventsCatchupPageSize = "events.catchupPageSize"
EventsCatchupThreshold = "events.catchupThreshold"
EventsCatchupDownscaleRegex = "events.catchupDownscaleRegex"
EventsCheckpointBlockGap = "events.checkpointBlockGap"
EventsBlockTimestamps = "events.blockTimestamps"
EventsFilterPollingInterval = "events.filterPollingInterval"
RetryInitDelay = "retry.initialDelay"
RetryMaxDelay = "retry.maxDelay"
RetryFactor = "retry.factor"
MaxConcurrentRequests = "maxConcurrentRequests"
TxCacheSize = "txCacheSize"
ConfigGasEstimationFactor = "gasEstimationFactor"
ConfigDataFormat = "dataFormat"
BlockPollingInterval = "blockPollingInterval"
BlockCacheSize = "blockCacheSize"
EventsCatchupPageSize = "events.catchupPageSize"
EventsCatchupThreshold = "events.catchupThreshold"
EventsCatchupDownscaleRegex = "events.catchupDownscaleRegex"
EventsCheckpointBlockGap = "events.checkpointBlockGap"
EventsBlockTimestamps = "events.blockTimestamps"
EventsFilterPollingInterval = "events.filterPollingInterval"
RetryInitDelay = "retry.initialDelay"
RetryMaxDelay = "retry.maxDelay"
RetryFactor = "retry.factor"
MaxConcurrentRequests = "maxConcurrentRequests"
TxCacheSize = "txCacheSize"
AllowNonStandardBlockHashSize = "allowNonStandardBlockHashSize"
NonStandardBlockHashSizeResolutionMethod = "nonStandardBlockHashSizeResolutionMethod"
)

const (
Expand All @@ -51,6 +53,8 @@ const (
DefaultRetryInitDelay = "100ms"
DefaultRetryMaxDelay = "30s"
DefaultRetryDelayFactor = 2.0

DefaultAllowNonStandardBlockHashSize = false
)

func InitConfig(conf config.Section) {
Expand All @@ -70,4 +74,6 @@ func InitConfig(conf config.Section) {
conf.AddKnownKey(RetryMaxDelay, DefaultRetryMaxDelay)
conf.AddKnownKey(MaxConcurrentRequests, 50)
conf.AddKnownKey(TxCacheSize, 250)
conf.AddKnownKey(AllowNonStandardBlockHashSize, DefaultAllowNonStandardBlockHashSize)
conf.AddKnownKey(NonStandardBlockHashSizeResolutionMethod, "truncate")
}
Loading
Loading