From d1f4b0747c0cc4ea5089ed8cd4209272e71bba02 Mon Sep 17 00:00:00 2001 From: Raul Jordan Date: Tue, 28 May 2024 10:25:00 -0500 Subject: [PATCH] oob reimbursement daemon --- containers/threadsafe/map.go | 10 + testing/endtoend/BUILD.bazel | 2 + testing/endtoend/backend/BUILD.bazel | 2 + testing/endtoend/backend/anvil_local.go | 3 +- testing/endtoend/backend/anvil_local_test.go | 3 +- testing/endtoend/backend/simulated.go | 3 + testing/endtoend/e2e_test.go | 3 +- testing/endtoend/expectations.go | 3 +- testing/endtoend/helpers_test.go | 3 +- testing/mocks/BUILD.bazel | 1 - testing/mocks/mocks.go | 17 +- testing/setup/BUILD.bazel | 3 + testing/setup/rollup_stack.go | 17 +- tools/reimbursement-service/BUILD.bazel | 35 ++ .../challenge_protocol_graph.go | 236 ++++++++++++ tools/reimbursement-service/event_scraper.go | 252 ++++++++++++ tools/reimbursement-service/main.go | 297 ++++++++++++++ tools/reimbursement-service/payments.go | 362 ++++++++++++++++++ .../reimbursement_computation.go | 112 ++++++ 19 files changed, 1341 insertions(+), 23 deletions(-) create mode 100644 tools/reimbursement-service/BUILD.bazel create mode 100644 tools/reimbursement-service/challenge_protocol_graph.go create mode 100644 tools/reimbursement-service/event_scraper.go create mode 100644 tools/reimbursement-service/main.go create mode 100644 tools/reimbursement-service/payments.go create mode 100644 tools/reimbursement-service/reimbursement_computation.go diff --git a/containers/threadsafe/map.go b/containers/threadsafe/map.go index 81c771e5f..1c3c9dad8 100644 --- a/containers/threadsafe/map.go +++ b/containers/threadsafe/map.go @@ -86,6 +86,16 @@ func (s *Map[K, V]) Delete(k K) { } } +func (s *Map[K, V]) Copy() map[K]V { + s.RLock() + defer s.RUnlock() + m := make(map[K]V) + for k, v := range s.items { + m[k] = v + } + return m +} + func (s *Map[K, V]) ForEach(fn func(k K, v V) error) error { s.RLock() defer s.RUnlock() diff --git a/testing/endtoend/BUILD.bazel b/testing/endtoend/BUILD.bazel index 21b07cefc..7e074c5dd 100644 --- a/testing/endtoend/BUILD.bazel +++ b/testing/endtoend/BUILD.bazel @@ -35,6 +35,7 @@ go_test( "//testing/endtoend/backend", "//testing/mocks/state-provider", "//testing/setup:setup_lib", + "//util", "@com_github_ethereum_go_ethereum//:go-ethereum", "@com_github_ethereum_go_ethereum//accounts/abi", "@com_github_ethereum_go_ethereum//accounts/abi/bind", @@ -57,6 +58,7 @@ go_library( "//runtime", "//solgen/go/rollupgen", "//testing/setup:setup_lib", + "//util", "@com_github_ethereum_go_ethereum//accounts/abi/bind", "@com_github_stretchr_testify//require", ], diff --git a/testing/endtoend/backend/BUILD.bazel b/testing/endtoend/backend/BUILD.bazel index 3285dbbd1..550e58a32 100644 --- a/testing/endtoend/backend/BUILD.bazel +++ b/testing/endtoend/backend/BUILD.bazel @@ -17,6 +17,7 @@ go_library( "//solgen/go/rollupgen", "//testing", "//testing/setup:setup_lib", + "//util", "@com_github_ethereum_go_ethereum//accounts/abi/bind", "@com_github_ethereum_go_ethereum//common", "@com_github_ethereum_go_ethereum//common/hexutil", @@ -39,6 +40,7 @@ go_test( visibility = ["//testing/endtoend:__subpackages__"], deps = [ "//runtime", + "//util", "@com_github_stretchr_testify//require", ], ) diff --git a/testing/endtoend/backend/anvil_local.go b/testing/endtoend/backend/anvil_local.go index e4f81c512..3cd16c0b3 100644 --- a/testing/endtoend/backend/anvil_local.go +++ b/testing/endtoend/backend/anvil_local.go @@ -17,6 +17,7 @@ import ( "github.com/OffchainLabs/bold/solgen/go/rollupgen" challenge_testing "github.com/OffchainLabs/bold/testing" "github.com/OffchainLabs/bold/testing/setup" + "github.com/OffchainLabs/bold/util" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" @@ -250,7 +251,7 @@ func (a *AnvilLocal) DeployRollup(ctx context.Context, opts ...challenge_testing if err != nil { return nil, err } - chalManagerAddr, err := rollupCaller.ChallengeManager(&bind.CallOpts{}) + chalManagerAddr, err := rollupCaller.ChallengeManager(util.GetSafeCallOpts(&bind.CallOpts{})) if err != nil { return nil, err } diff --git a/testing/endtoend/backend/anvil_local_test.go b/testing/endtoend/backend/anvil_local_test.go index 175caaf41..fe7b3d15d 100644 --- a/testing/endtoend/backend/anvil_local_test.go +++ b/testing/endtoend/backend/anvil_local_test.go @@ -9,6 +9,7 @@ import ( "time" retry "github.com/OffchainLabs/bold/runtime" + "github.com/OffchainLabs/bold/util" "github.com/stretchr/testify/require" ) @@ -45,7 +46,7 @@ func TestLocalAnvilStarts(t *testing.T) { require.NoError(t, err) // There should be at least 100 blocks - bn, err2 := a.Client().HeaderByNumber(ctx, nil) + bn, err2 := a.Client().HeaderByNumber(ctx, util.GetSafeBlockNumber()) if err2 != nil { t.Fatal(err2) } diff --git a/testing/endtoend/backend/simulated.go b/testing/endtoend/backend/simulated.go index 1753d9ea9..592556a49 100644 --- a/testing/endtoend/backend/simulated.go +++ b/testing/endtoend/backend/simulated.go @@ -2,6 +2,7 @@ package backend import ( "context" + "fmt" "time" protocol "github.com/OffchainLabs/bold/chain-abstraction" @@ -69,5 +70,7 @@ func NewSimulated(blockTime time.Duration, opts ...setup.Opt) (*LocalSimulatedBa if err != nil { return nil, err } + fmt.Println("Rollup addr", setup.Addrs.Rollup) + time.Sleep(time.Second * 10) return &LocalSimulatedBackend{blockTime: blockTime, setup: setup}, nil } diff --git a/testing/endtoend/e2e_test.go b/testing/endtoend/e2e_test.go index 97b235626..78c853ce6 100644 --- a/testing/endtoend/e2e_test.go +++ b/testing/endtoend/e2e_test.go @@ -105,7 +105,7 @@ type protocolParams struct { func defaultProtocolParams() protocolParams { return protocolParams{ numBigStepLevels: 1, - challengePeriodBlocks: 60, + challengePeriodBlocks: 50, layerZeroHeights: protocol.LayerZeroHeights{ BlockChallengeHeight: 1 << 5, BigStepChallengeHeight: 1 << 5, @@ -336,6 +336,7 @@ func runEndToEndTest(t *testing.T, cfg *e2eConfig) { }) } require.NoError(t, g.Wait()) + time.Sleep(time.Hour) } type seqMessage struct { diff --git a/testing/endtoend/expectations.go b/testing/endtoend/expectations.go index 45fe0d405..8979cc6d9 100644 --- a/testing/endtoend/expectations.go +++ b/testing/endtoend/expectations.go @@ -9,6 +9,7 @@ import ( retry "github.com/OffchainLabs/bold/runtime" "github.com/OffchainLabs/bold/solgen/go/rollupgen" "github.com/OffchainLabs/bold/testing/setup" + "github.com/OffchainLabs/bold/util" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/stretchr/testify/require" ) @@ -36,7 +37,7 @@ func expectAssertionConfirmedByChallengeWin( require.NoError(t, err) for i.Next() { assertionNode, err := retry.UntilSucceeds(ctx, func() (rollupgen.AssertionNode, error) { - return rc.GetAssertion(&bind.CallOpts{Context: ctx}, i.Event.AssertionHash) + return rc.GetAssertion(util.GetSafeCallOpts(&bind.CallOpts{Context: ctx}), i.Event.AssertionHash) }) require.NoError(t, err) if assertionNode.Status != uint8(protocol.AssertionConfirmed) { diff --git a/testing/endtoend/helpers_test.go b/testing/endtoend/helpers_test.go index c3e4fd1a1..17f4707c1 100644 --- a/testing/endtoend/helpers_test.go +++ b/testing/endtoend/helpers_test.go @@ -12,6 +12,7 @@ import ( challengemanager "github.com/OffchainLabs/bold/challenge-manager" l2stateprovider "github.com/OffchainLabs/bold/layer2-state-provider" "github.com/OffchainLabs/bold/solgen/go/rollupgen" + "github.com/OffchainLabs/bold/util" "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" @@ -35,7 +36,7 @@ func setupChallengeManager( ) require.NoError(t, err) challengeManagerAddr, err := assertionChainBinding.RollupUserLogicCaller.ChallengeManager( - &bind.CallOpts{Context: ctx}, + util.GetSafeCallOpts(&bind.CallOpts{Context: ctx}), ) require.NoError(t, err) chain, err := solimpl.NewAssertionChain( diff --git a/testing/mocks/BUILD.bazel b/testing/mocks/BUILD.bazel index b6d221df3..4bac95737 100644 --- a/testing/mocks/BUILD.bazel +++ b/testing/mocks/BUILD.bazel @@ -12,7 +12,6 @@ go_library( "//layer2-state-provider", "//solgen/go/rollupgen", "//state-commitments/history", - "@com_github_ethereum_go_ethereum//accounts/abi/bind", "@com_github_ethereum_go_ethereum//common", "@com_github_ethereum_go_ethereum//core/types", "@com_github_stretchr_testify//mock", diff --git a/testing/mocks/mocks.go b/testing/mocks/mocks.go index 263be37c8..8749167e6 100644 --- a/testing/mocks/mocks.go +++ b/testing/mocks/mocks.go @@ -6,8 +6,6 @@ package mocks import ( "context" - "github.com/ethereum/go-ethereum/accounts/abi/bind" - "math/big" protocol "github.com/OffchainLabs/bold/chain-abstraction" "github.com/OffchainLabs/bold/containers/option" @@ -168,8 +166,8 @@ func (m *MockSpecChallengeManager) ChallengePeriodBlocks(ctx context.Context) (u args := m.Called(ctx) return args.Get(0).(uint64), args.Error(1) } -func (m *MockSpecChallengeManager) MultiUpdateInheritedTimers(ctx context.Context, branch []protocol.ReadOnlyEdge, desiredTimerForLastEdge uint64) (*types.Transaction, error) { - args := m.Called(ctx, branch, desiredTimerForLastEdge) +func (m *MockSpecChallengeManager) MultiUpdateInheritedTimers(ctx context.Context, branch []protocol.ReadOnlyEdge) (*types.Transaction, error) { + args := m.Called(ctx, branch) return args.Get(0).(*types.Transaction), args.Error(1) } func (m *MockSpecChallengeManager) GetEdge( @@ -379,17 +377,6 @@ type MockProtocol struct { mock.Mock } -func (m *MockProtocol) GetCallOptsWithDesiredRpcHeadBlockNumber(opts *bind.CallOpts) *bind.CallOpts { - if opts == nil { - opts = &bind.CallOpts{} - } - return opts -} - -func (m *MockProtocol) GetDesiredRpcHeadBlockNumber() *big.Int { - return nil -} - // Read-only methods. func (m *MockProtocol) Backend() protocol.ChainBackend { args := m.Called() diff --git a/testing/setup/BUILD.bazel b/testing/setup/BUILD.bazel index d79c8df08..3196dfc14 100644 --- a/testing/setup/BUILD.bazel +++ b/testing/setup/BUILD.bazel @@ -22,6 +22,7 @@ go_library( "//solgen/go/yulgen", "//testing", "//testing/mocks/state-provider", + "//util", "@com_github_ethereum_go_ethereum//:go-ethereum", "@com_github_ethereum_go_ethereum//accounts/abi", "@com_github_ethereum_go_ethereum//accounts/abi/bind", @@ -29,8 +30,10 @@ go_library( "@com_github_ethereum_go_ethereum//core", "@com_github_ethereum_go_ethereum//core/types", "@com_github_ethereum_go_ethereum//crypto", + "@com_github_ethereum_go_ethereum//eth/ethconfig", "@com_github_ethereum_go_ethereum//ethclient/simulated", "@com_github_ethereum_go_ethereum//log", + "@com_github_ethereum_go_ethereum//node", "@com_github_pkg_errors//:errors", ], ) diff --git a/testing/setup/rollup_stack.go b/testing/setup/rollup_stack.go index 30d45cefb..5fbb2f66c 100644 --- a/testing/setup/rollup_stack.go +++ b/testing/setup/rollup_stack.go @@ -23,14 +23,17 @@ import ( "github.com/OffchainLabs/bold/solgen/go/yulgen" challenge_testing "github.com/OffchainLabs/bold/testing" statemanager "github.com/OffchainLabs/bold/testing/mocks/state-provider" + "github.com/OffchainLabs/bold/util" "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/eth/ethconfig" "github.com/ethereum/go-ethereum/ethclient/simulated" "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/node" "github.com/pkg/errors" ) @@ -320,7 +323,7 @@ func ChainsWithEdgeChallengeManager(opts ...Opt) (*ChainSetup, error) { } var challengeManagerAddr common.Address challengeManagerAddr, err = assertionChainBinding.RollupUserLogicCaller.ChallengeManager( - &bind.CallOpts{Context: ctx}, + util.GetSafeCallOpts(&bind.CallOpts{Context: ctx}), ) if err != nil { return nil, err @@ -1032,6 +1035,16 @@ func Accounts(numAccounts uint64) ([]*TestAccount, *SimulatedBackendWrapper, err TxOpts: txOpts, } } - backend := NewSimulatedBackendWrapper(simulated.NewBackend(genesis, simulated.WithBlockGasLimit(gasLimit))) + backend := NewSimulatedBackendWrapper(simulated.NewBackend(genesis, simulated.WithBlockGasLimit(gasLimit), WithHttpApi(8545))) return accs, backend, nil } + +// WithBlockGasLimit configures the simulated backend to target a specific gas limit +// when producing blocks. +func WithHttpApi(port int) func(nodeConf *node.Config, ethConf *ethconfig.Config) { + return func(nodeConf *node.Config, ethConf *ethconfig.Config) { + nodeConf.HTTPHost = "localhost" + nodeConf.HTTPPort = port + nodeConf.HTTPModules = append(nodeConf.HTTPModules, "eth") + } +} diff --git a/tools/reimbursement-service/BUILD.bazel b/tools/reimbursement-service/BUILD.bazel new file mode 100644 index 000000000..63c2ee8b2 --- /dev/null +++ b/tools/reimbursement-service/BUILD.bazel @@ -0,0 +1,35 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library") + +go_library( + name = "reimbursement-service_lib", + srcs = [ + "challenge_protocol_graph.go", + "event_scraper.go", + "main.go", + "payments.go", + "reimbursement_computation.go", + ], + importpath = "github.com/OffchainLabs/bold/tools/reimbursement-service", + visibility = ["//visibility:private"], + deps = [ + "//chain-abstraction:protocol", + "//containers/option", + "//containers/threadsafe", + "//solgen/go/challengeV2gen", + "//solgen/go/rollupgen", + "//util", + "@com_github_ethereum_go_ethereum//:go-ethereum", + "@com_github_ethereum_go_ethereum//accounts/abi/bind", + "@com_github_ethereum_go_ethereum//common", + "@com_github_ethereum_go_ethereum//core/types", + "@com_github_ethereum_go_ethereum//ethclient", + "@com_github_ethereum_go_ethereum//log", + "@com_github_ethereum_go_ethereum//rpc", + ], +) + +go_binary( + name = "reimbursement-service", + embed = [":reimbursement-service_lib"], + visibility = ["//visibility:public"], +) diff --git a/tools/reimbursement-service/challenge_protocol_graph.go b/tools/reimbursement-service/challenge_protocol_graph.go new file mode 100644 index 000000000..15153b0b8 --- /dev/null +++ b/tools/reimbursement-service/challenge_protocol_graph.go @@ -0,0 +1,236 @@ +package main + +import ( + "container/list" + "context" + "errors" + + protocol "github.com/OffchainLabs/bold/chain-abstraction" + "github.com/OffchainLabs/bold/containers/option" + "github.com/OffchainLabs/bold/solgen/go/challengeV2gen" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/log" +) + +type edge struct { + id common.Hash + txHash common.Hash + *challengeV2gen.ChallengeEdge +} + +type protocolGraph struct { + edgesByLevel map[protocol.ChallengeLevel]map[common.Hash]*edge + chalManager *challengeV2gen.EdgeChallengeManager + challengePeriodBlocks uint64 +} + +func (s *service) getEdgeClaimingAssertion( + ctx context.Context, + challengeCreationBlock uint64, + claimingAssertion common.Hash, +) *edge { + it, err := s.chalManager.FilterEdgeAdded(&bind.FilterOpts{ + Start: challengeCreationBlock, + End: nil, + Context: ctx, + }, nil, nil, nil) + defer func() { + if err = it.Close(); err != nil { + log.Error("Could not close filter iterator", "err", err) + } + }() + for it.Next() { + if it.Error() != nil { + panic(err) + } + if it.Event.ClaimId == claimingAssertion { + eg, err := s.chalManager.GetEdge(&bind.CallOpts{}, it.Event.EdgeId) + if err != nil { + panic(err) + } + return &edge{ + id: it.Event.EdgeId, + txHash: it.Event.Raw.TxHash, + ChallengeEdge: &eg, + } + } + } + return nil +} + +func (s *service) buildProtocolGraphForChallenge( + ctx context.Context, claimedAssertion common.Hash, +) (*protocolGraph, error) { + // Scan for edge creations from parent's second child creation block up to and including + // the block at which the item was confirmed. + parentAssertion, err := s.rollupBindings.GetAssertion(&bind.CallOpts{}, claimedAssertion) + if err != nil { + return nil, err + } + + eg := s.getEdgeClaimingAssertion(ctx, parentAssertion.SecondChildBlock, claimedAssertion) + if protocol.EdgeStatus(eg.Status) != protocol.EdgeConfirmed { + return nil, errors.New("edge is not confirmed") + } + upToConfirmedEdge := eg.id + + // Get the first edge that claims the assertion in the challenge, then get all edges + // from that edge up to the block that first edge was confirmed. + confirmedEdge, err := s.chalManager.GetEdge(&bind.CallOpts{}, upToConfirmedEdge) + if err != nil { + return nil, err + } + startBlock := parentAssertion.SecondChildBlock + endBlock := confirmedEdge.ConfirmedAtBlock + if endBlock <= startBlock { + return nil, errors.New("end block is less than or equal to start block") + } + chalPeriodBlocks, err := s.chalManager.ChallengePeriodBlocks(&bind.CallOpts{}) + if err != nil { + return nil, err + } + graph := &protocolGraph{ + edgesByLevel: make(map[protocol.ChallengeLevel]map[common.Hash]*edge), + chalManager: s.chalManager, + challengePeriodBlocks: chalPeriodBlocks, + } + it, err := s.chalManager.FilterEdgeAdded(&bind.FilterOpts{ + Start: startBlock, + End: &endBlock, + Context: ctx, + }, nil, nil, nil) + defer func() { + if err = it.Close(); err != nil { + log.Error("Could not close filter iterator", "err", err) + } + }() + for it.Next() { + if it.Error() != nil { + panic(err) + } + lvl := protocol.ChallengeLevel(it.Event.Level) + eg, err := s.chalManager.GetEdge(&bind.CallOpts{}, it.Event.EdgeId) + if err != nil { + panic(err) + } + if _, ok := graph.edgesByLevel[lvl]; !ok { + graph.edgesByLevel[lvl] = make(map[common.Hash]*edge) + } + graph.edgesByLevel[lvl][it.Event.EdgeId] = &edge{ + id: it.Event.EdgeId, + txHash: it.Event.Raw.TxHash, + ChallengeEdge: &eg, + } + } + return graph, nil +} + +func (pg *protocolGraph) terminalEdgesAtLvl(level protocol.ChallengeLevel) []*edge { + terminalEdges := make([]*edge, 0) + edgesAtLvl, ok := pg.edgesByLevel[level] + if !ok { + return terminalEdges + } + for _, eg := range edgesAtLvl { + start := eg.StartHeight.Uint64() + end := eg.EndHeight.Uint64() + if start > end { + continue + } + if end-start == 1 { + terminalEdges = append(terminalEdges, eg) + } + } + return terminalEdges +} + +func (pg *protocolGraph) getClaimingEdgeAtLvl(level protocol.ChallengeLevel, claimedEdge common.Hash) *edge { + edgesAtLvl, ok := pg.edgesByLevel[level] + if !ok { + return nil + } + for _, eg := range edgesAtLvl { + if eg.ClaimId == claimedEdge { + return eg + } + } + return nil +} + +func (pg *protocolGraph) extractEssentialBranchesAtLvl(level protocol.ChallengeLevel, root *edge) [][]*edge { + allPaths := make([][]*edge, 0) + if pg.onchainTimer(root) < pg.challengePeriodBlocks { + return allPaths + } + edgesAtLvl := pg.edgesByLevel[level] + type visited struct { + essentialEdge *edge + path []*edge + } + stack := newStack[*visited]() + stack.push(&visited{ + essentialEdge: root, + path: []*edge{root}, + }) + for stack.len() > 0 { + curr := stack.pop().Unwrap() + currentNode, path := curr.essentialEdge, curr.path + + // Node not viable for refund. + if pg.onchainTimer(currentNode) < pg.challengePeriodBlocks { + continue + } + hasChildren := currentNode.LowerChildId != common.Hash{} || + currentNode.UpperChildId != common.Hash{} + if hasChildren { + lowerChild := edgesAtLvl[currentNode.LowerChildId] + upperChild := edgesAtLvl[currentNode.UpperChildId] + stack.push(&visited{ + essentialEdge: lowerChild, + path: append(path, lowerChild), + }) + stack.push(&visited{ + essentialEdge: upperChild, + path: append(path, upperChild), + }) + } else { + allPaths = append(allPaths, path) + } + } + return allPaths +} + +func (pg *protocolGraph) onchainTimer(eg *edge) uint64 { + onchainEdge, err := pg.chalManager.GetEdge(&bind.CallOpts{}, eg.id) + if err != nil { + panic(err) + } + return onchainEdge.TotalTimeUnrivaledCache +} + +type stack[T any] struct { + dll *list.List +} + +func newStack[T any]() *stack[T] { + return &stack[T]{dll: list.New()} +} + +func (s *stack[T]) len() int { + return s.dll.Len() +} + +func (s *stack[T]) push(x T) { + s.dll.PushBack(x) +} + +func (s *stack[T]) pop() option.Option[T] { + if s.dll.Len() == 0 { + return option.None[T]() + } + tail := s.dll.Back() + val := tail.Value + s.dll.Remove(tail) + return option.Some(val.(T)) +} diff --git a/tools/reimbursement-service/event_scraper.go b/tools/reimbursement-service/event_scraper.go new file mode 100644 index 000000000..48c0477f6 --- /dev/null +++ b/tools/reimbursement-service/event_scraper.go @@ -0,0 +1,252 @@ +package main + +import ( + "context" + "errors" + "fmt" + "math/big" + "time" + + protocol "github.com/OffchainLabs/bold/chain-abstraction" + "github.com/OffchainLabs/bold/solgen/go/rollupgen" + "github.com/OffchainLabs/bold/util" + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/log" +) + +func (s *service) scanChainEvents(ctx context.Context) { + latestConfirmed, err := s.rollupBindings.LatestConfirmed(&bind.CallOpts{}) + if err != nil { + panic(err) + } + latestConfirmedAssertion, err := s.readAssertionCreationInfo( + ctx, + protocol.AssertionHash{Hash: latestConfirmed}, + ) + if err != nil { + panic(err) + } + + // Gather all challenged assertions. + challengedAssertions := make([]*protocol.AssertionCreatedInfo, 0) + _ = challengedAssertions + + fromBlock := latestConfirmedAssertion.CreationBlock + ticker := time.NewTicker(s.chainScanInterval) + defer ticker.Stop() + for { + select { + case <-ticker.C: + latestBlock, err := s.client.HeaderByNumber(ctx, util.GetSafeBlockNumber()) + if err != nil { + log.Error("Could not get header by number", "err", err) + continue + } + if !latestBlock.Number.IsUint64() { + log.Error("Latest block number was not a uint64") + continue + } + toBlock := latestBlock.Number.Uint64() + if fromBlock == toBlock { + continue + } + filterOpts := &bind.FilterOpts{ + Start: fromBlock, + End: &toBlock, + Context: ctx, + } + if err = s.processAllAssertionsInRange(ctx, s.rollupBindings, filterOpts); err != nil { + log.Error("Could not process assertions in range", "err", err) + continue + } + if err != nil { + log.Error("Could not check for assertion added", "err", err) + continue + } + + // Assertion confirmation. + it, err := s.rollupBindings.FilterAssertionConfirmed(filterOpts, nil) + if err != nil { + panic(err) + } + defer func() { + if err = it.Close(); err != nil { + log.Error("Could not close filter iterator", "err", err) + } + }() + for it.Next() { + if it.Error() != nil { + panic(err) + } + s.confirmedAssertionsObserved <- it.Event.AssertionHash + } + + // Scan for edge confirmations. + it2, err := s.chalManager.FilterEdgeConfirmedByTime(filterOpts, nil, nil) + if err != nil { + panic(err) + } + defer func() { + if err = it2.Close(); err != nil { + log.Error("Could not close filter iterator", "err", err) + } + }() + for it2.Next() { + if it2.Error() != nil { + panic(err) + } + fmt.Println("Edge confirmed by time") + eg, err := s.chalManager.GetEdge(&bind.CallOpts{}, it2.Event.EdgeId) + if err != nil { + panic(err) + } + s.confirmedEdgesObserved <- &edge{ + id: it2.Event.EdgeId, + txHash: it2.Event.Raw.TxHash, + ChallengeEdge: &eg, + } + } + + it3, err := s.chalManager.FilterEdgeConfirmedByOneStepProof(filterOpts, nil, nil) + if err != nil { + panic(err) + } + defer func() { + if err = it3.Close(); err != nil { + log.Error("Could not close filter iterator", "err", err) + } + }() + for it3.Next() { + if it3.Error() != nil { + panic(err) + } + fmt.Println("Edge confirmed by osp") + eg, err := s.chalManager.GetEdge(&bind.CallOpts{}, it3.Event.EdgeId) + if err != nil { + panic(err) + } + s.confirmedEdgesObserved <- &edge{ + id: it3.Event.EdgeId, + txHash: it3.Event.Raw.TxHash, + ChallengeEdge: &eg, + } + } + + fromBlock = toBlock + case <-ctx.Done(): + return + } + } +} + +// This function will scan for all assertion creation events to determine which +// ones are canonical and which ones must be challenged. +func (s *service) processAllAssertionsInRange( + ctx context.Context, + filterer *rollupgen.RollupUserLogic, + filterOpts *bind.FilterOpts, +) error { + it, err := filterer.FilterAssertionCreated(filterOpts, nil, nil) + if err != nil { + return err + } + defer func() { + if err = it.Close(); err != nil { + log.Error("Could not close filter iterator", "err", err) + } + }() + for it.Next() { + if it.Error() != nil { + return it.Error() + } + hash := it.Event.AssertionHash + fmt.Println("Got the creation event", common.Hash(hash).Hex()) + creationInfo, err := s.readAssertionCreationInfo( + ctx, protocol.AssertionHash{Hash: hash}, + ) + if err != nil { + return err + } + parentHash := creationInfo.ParentAssertionHash + if parentHash == (common.Hash{}) { + genesis, err := s.rollupBindings.GenesisAssertionHash(&bind.CallOpts{}) + if err != nil { + return err + } + parentHash = genesis + } + parentAssertion, err := s.rollupBindings.GetAssertion(&bind.CallOpts{}, parentHash) + if err != nil { + return err + } + // If the parent has a second child, that means this assertion is either part of a challenge + // or will be part of a challenge. + if parentAssertion.SecondChildBlock > 0 { + s.claimedChallengeAssertions <- creationInfo + } + } + return nil +} + +func (s *service) readAssertionCreationInfo( + ctx context.Context, + id protocol.AssertionHash, +) (*protocol.AssertionCreatedInfo, error) { + var creationBlock uint64 + var topics [][]common.Hash + if id == (protocol.AssertionHash{}) { + rollupDeploymentBlock, err := s.rollupBindings.RollupDeploymentBlock(util.GetSafeCallOpts(&bind.CallOpts{Context: ctx})) + if err != nil { + return nil, err + } + if !rollupDeploymentBlock.IsUint64() { + return nil, errors.New("rollup deployment block is not a uint64") + } + creationBlock = rollupDeploymentBlock.Uint64() + topics = [][]common.Hash{{assertionCreatedId}} + } else { + var b [32]byte + copy(b[:], id.Bytes()) + node, err := s.rollupBindings.GetAssertion(util.GetSafeCallOpts(&bind.CallOpts{Context: ctx}), b) + if err != nil { + return nil, err + } + creationBlock = node.CreatedAtBlock + topics = [][]common.Hash{{assertionCreatedId}, {id.Hash}} + } + var query = ethereum.FilterQuery{ + FromBlock: new(big.Int).SetUint64(creationBlock), + ToBlock: new(big.Int).SetUint64(creationBlock), + Addresses: []common.Address{s.rollupAddr}, + Topics: topics, + } + logs, err := s.client.FilterLogs(ctx, query) + if err != nil { + return nil, err + } + if len(logs) != 1 { + return nil, errors.New("expected one log") + } + ethLog := logs[0] + parsedLog, err := s.rollupBindings.ParseAssertionCreated(ethLog) + if err != nil { + return nil, err + } + afterState := parsedLog.Assertion.AfterState + return &protocol.AssertionCreatedInfo{ + ConfirmPeriodBlocks: parsedLog.ConfirmPeriodBlocks, + RequiredStake: parsedLog.RequiredStake, + ParentAssertionHash: parsedLog.ParentAssertionHash, + BeforeState: parsedLog.Assertion.BeforeState, + AfterState: afterState, + InboxMaxCount: parsedLog.InboxMaxCount, + AfterInboxBatchAcc: parsedLog.AfterInboxBatchAcc, + AssertionHash: parsedLog.AssertionHash, + WasmModuleRoot: parsedLog.WasmModuleRoot, + ChallengeManager: parsedLog.ChallengeManager, + TransactionHash: ethLog.TxHash, + CreationBlock: ethLog.BlockNumber, + }, nil +} diff --git a/tools/reimbursement-service/main.go b/tools/reimbursement-service/main.go new file mode 100644 index 000000000..80729bc4f --- /dev/null +++ b/tools/reimbursement-service/main.go @@ -0,0 +1,297 @@ +package main + +import ( + "context" + "fmt" + "math/big" + "os" + "path/filepath" + "sync" + "time" + + protocol "github.com/OffchainLabs/bold/chain-abstraction" + "github.com/OffchainLabs/bold/containers/threadsafe" + "github.com/OffchainLabs/bold/solgen/go/challengeV2gen" + "github.com/OffchainLabs/bold/solgen/go/rollupgen" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/ethereum/go-ethereum/rpc" +) + +const ( + defaultChanBufferSize = 100 + defaultChainScanInterval = time.Second +) + +var ( + assertionCreatedId common.Hash + recommendedL1YieldPerBlock = big.NewInt(1) // TODO: Tweak and set in wei. + blockRefinementCreateGasCost = uint64(530703) // TODO: Set these refinement costs. + bigStepRefinementCreateGasCost = uint64(425628) + smallStepRefinementCreateGasCost = uint64(439739) + blockBisectGasCost = uint64(480051) + bigStepBisectGasCost = uint64(648411) + smallStepBisectGasCost = uint64(661328) + confirmByOneStepProofGasCost = uint64(865060) + confirmEdgeByTimeGasCost = uint64(113097) +) + +func init() { + rollupAbi, err := rollupgen.RollupCoreMetaData.GetAbi() + if err != nil { + panic(err) + } + assertionCreatedEvent, ok := rollupAbi.Events["AssertionCreated"] + if !ok { + panic("RollupCore ABI missing AssertionCreated event") + } + assertionCreatedId = assertionCreatedEvent.ID +} + +type claimType uint8 + +const ( + assertionTyp claimType = iota + edgeTyp +) + +func (c claimType) String() string { + if c == edgeTyp { + return "edge" + } + return "assertion" +} + +// TODO: Use a standard block number for rpc requests that need specific timings. +func main() { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + rpcClient, err := rpc.Dial("http://localhost:8545") + if err != nil { + panic(err) + } + client := ethclient.NewClient(rpcClient) + chainId, err := client.ChainID(ctx) + if err != nil { + panic(err) + } + rollupAddr := common.HexToAddress("0x99d322A49EeAb96fE51e26944D54824F3Ef6dedF") + rollupBindings, err := rollupgen.NewRollupUserLogic(rollupAddr, client) + if err != nil { + panic(err) + } + chalManagerAddr, err := rollupBindings.ChallengeManager(&bind.CallOpts{}) + if err != nil { + panic(err) + } + chalManager, err := challengeV2gen.NewEdgeChallengeManager(chalManagerAddr, client) + if err != nil { + panic(err) + } + srv := &service{ + client: client, + rollupBindings: rollupBindings, + rollupAddr: rollupAddr, + chalManagerAddr: chalManagerAddr, + chalManager: chalManager, + claimedChallengeAssertions: make(chan *protocol.AssertionCreatedInfo, defaultChanBufferSize), + confirmedAssertionsObserved: make(chan common.Hash, defaultChanBufferSize), + confirmedEdgesObserved: make(chan *edge, defaultChainScanInterval), + watchList: threadsafe.NewMap[common.Hash, claimType](), + protocolGraphsByClaimedAssertion: make(map[common.Hash]*protocolGraph), + gasPaymentRequests: make(chan *gasPaymentRequest, defaultChanBufferSize), + serviceFeePaymentRequests: make(chan *serviceFeePaymentRequest, defaultChanBufferSize), + recommendedL1YieldPerBlock: recommendedL1YieldPerBlock, + chainId: chainId, + pendingPaymentsOutputDir: filepath.Join(os.TempDir(), "payments"), + chainScanInterval: defaultChainScanInterval, + } + srv.run(ctx) +} + +type service struct { + rollupBindings *rollupgen.RollupUserLogic + rollupAddr common.Address + chalManagerAddr common.Address + chalManager *challengeV2gen.EdgeChallengeManager + client protocol.ChainBackend + claimedChallengeAssertions chan *protocol.AssertionCreatedInfo + confirmedEdgesObserved chan *edge + confirmedAssertionsObserved chan common.Hash + watchList *threadsafe.Map[common.Hash, claimType] + protocolGraphLock sync.RWMutex + protocolGraphsByClaimedAssertion map[common.Hash]*protocolGraph + gasPaymentRequests chan *gasPaymentRequest + serviceFeePaymentRequests chan *serviceFeePaymentRequest + recommendedL1YieldPerBlock *big.Int + chainId *big.Int + pendingPaymentsOutputDir string + chainScanInterval time.Duration +} + +func (s *service) run(ctx context.Context) { + go receiveAsync(ctx, s.claimedChallengeAssertions, s.processAssertionsInChallenges) + go receiveAsync(ctx, s.confirmedAssertionsObserved, s.processAssertionConfirmation) + go receiveAsync(ctx, s.confirmedEdgesObserved, s.processEdgeConfirmation) + go receiveAsync(ctx, s.gasPaymentRequests, s.processGasPaymentRequest) + go receiveAsync(ctx, s.serviceFeePaymentRequests, s.processServiceFeePaymentRequest) + s.scanChainEvents(ctx) +} + +func (s *service) processAssertionsInChallenges(ctx context.Context, assertion *protocol.AssertionCreatedInfo) { + // If the assertion is a not a descendant of the latest confirmed assertion, we ignore it. + if !s.isDescendantOfLatestConfirmed(ctx, assertion.AssertionHash) { + return + } + as, err := s.rollupBindings.GetAssertion(&bind.CallOpts{}, assertion.AssertionHash) + if err != nil { + panic(err) + } + status := protocol.AssertionStatus(as.Status) + if status == protocol.AssertionConfirmed { + // If the assertion is already confirmed, we notify listeners of this. + s.confirmedAssertionsObserved <- assertion.AssertionHash + } else if status == protocol.AssertionPending { + // Otherwise, we add the assertion to the watchlist of hashes to check once they are confirmed. + fmt.Println("Saw a pending assertion that is claimed in a challenge to put in the watchlist") + s.watchList.Put(assertion.AssertionHash, assertionTyp) + } + _ = assertion +} + +func (s *service) processAssertionConfirmation(ctx context.Context, confirmedAssertion common.Hash) { + if !s.watchList.Has(confirmedAssertion) { + // Ignore if the confirmed item is not in the watch list. + fmt.Println("Ignored confirmed assertion not in the watchlist") + return + } + fmt.Println("Got an assertion confirmed that was in the watchlist") + s.executeReimbursement(ctx, reimbursementArgs{ + claimedAssertion: confirmedAssertion, + itemTyp: assertionTyp, + challengeLvl: protocol.NewBlockChallengeLevel(), + }) + + // Prune all items in the watchlist that are not descendants of the latest confirmed assertion. + for hash, typ := range s.watchList.Copy() { + if typ == edgeTyp { + prevAssertionHash, err := s.chalManager.GetPrevAssertionHash(&bind.CallOpts{}, hash) + if err != nil { + panic(err) + } + if !s.isDescendantOfLatestConfirmed(ctx, prevAssertionHash) { + s.watchList.Delete(prevAssertionHash) + s.watchList.Delete(hash) + if typ == edgeTyp { + s.watchList.Delete(hash) + } + } + } else { + if !s.isDescendantOfLatestConfirmed(ctx, hash) { + s.watchList.Delete(hash) + } + } + } +} + +func (s *service) processEdgeConfirmation(ctx context.Context, eg *edge) { + // Check if we should be tracking the edge confirmation. + predecessorAssertion, err := s.chalManager.GetPrevAssertionHash(&bind.CallOpts{}, eg.id) + if err != nil { + panic(err) + } + // Ignore an observed edge that has a predecessor assertion + // that is NOT a descendant of the latest confirmed assertion. + if !s.isDescendantOfLatestConfirmed(ctx, predecessorAssertion) { + return + } + + // Get the claimed assertion of the edge from the protocol graph. It is impossible to figure out + // the claimed information of an assertion just from onchain data when given an edge, + // so we need to loop over all our tracked protocol graphs to see if we have the edge locally. + lvl := protocol.ChallengeLevel(eg.Level) + found := false + var claimedAssertion common.Hash + s.protocolGraphLock.RLock() + for claim, graph := range s.protocolGraphsByClaimedAssertion { + for _, existingEdge := range graph.edgesByLevel[lvl] { + if existingEdge.id == eg.id { + found = true + claimedAssertion = claim + break + } + } + } + s.protocolGraphLock.RUnlock() + if !found { + fmt.Println("Not found in protocol graph", eg.Level, eg.StartHeight.Uint64(), eg.EndHeight.Uint64()) + return + } + s.executeReimbursement(ctx, reimbursementArgs{ + claimedAssertion: claimedAssertion, + itemTyp: edgeTyp, + challengeLvl: lvl, + claimedEdge: eg.id, + }) +} + +func (s *service) isDescendantOfLatestConfirmed(ctx context.Context, assertionHash common.Hash) bool { + latestConf, err := s.rollupBindings.LatestConfirmed(&bind.CallOpts{}) + if err != nil { + panic(err) + } + latestConfirmed, err := s.readAssertionCreationInfo(ctx, protocol.AssertionHash{Hash: latestConf}) + if err != nil { + panic(err) + } + curr, err := s.readAssertionCreationInfo(ctx, protocol.AssertionHash{Hash: assertionHash}) + if err != nil { + panic(err) + } + for { + if curr.AssertionHash == latestConf || curr.ParentAssertionHash == latestConf { + return true + } + // If the cursor's inbox max count is <= the latest confirmed's inbox max count, + // and we still have not reached the "true" condition, then we can exit with false + // as the assertion is not a descendant of the latest confirmed. + if curr.InboxMaxCount.Cmp(latestConfirmed.InboxMaxCount) <= 0 { + return false + } + parent, err := s.readAssertionCreationInfo(ctx, protocol.AssertionHash{Hash: curr.ParentAssertionHash}) + if err != nil { + panic(err) + } + curr = parent + } +} + +type retryFunc[T any] func(...T) (T, error) + +func retry[T any](f retryFunc[T], ctx context.Context, args ...T) T { + var zeroVal T + for { + if ctx.Err() != nil { + fmt.Println("Context timed out waiting to retry func, exiting...") + return zeroVal + } + result, err := f(args...) + if err != nil { + fmt.Printf("error running func, will retry: %w\n", err) + continue + } + return result + } +} + +func receiveAsync[T any](ctx context.Context, channel chan T, f func(context.Context, T)) { + for { + select { + case item := <-channel: + go f(ctx, item) + case <-ctx.Done(): + return + } + } +} diff --git a/tools/reimbursement-service/payments.go b/tools/reimbursement-service/payments.go new file mode 100644 index 000000000..f548cd487 --- /dev/null +++ b/tools/reimbursement-service/payments.go @@ -0,0 +1,362 @@ +package main + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "math/big" + "os" + "path/filepath" + "time" + + protocol "github.com/OffchainLabs/bold/chain-abstraction" + "github.com/OffchainLabs/bold/containers/option" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/log" +) + +var weiToGwei = big.NewInt(1000000000) + +type gasPaymentRequest struct { + confirmedAssertionInChallenge common.Hash + essentialChallengeBranches [][]*edge +} + +type serviceFeePaymentRequest struct { + claimedItemTyp claimType + claimedItem common.Hash + edgeCreationTx common.Hash +} + +type pendingGasPayment struct { + PayableAddresses map[common.Address]*payableGasInfo `json:"payable_addresses"` +} + +type payableGasInfo struct { + EdgeCreationTotalCostGwei *big.Int `json:"creation_total_cost_gwei"` + ConfirmationTotalCostGwei *big.Int `json:"confirmation_total_cost_gwei"` +} + +type pendingServiceFeePayment struct { + NumBlocksPayable uint64 `json:"num_blocks_payable"` + RecommendedYieldGweiPerBlock string `json:"recommended_yield_gwei_per_block"` + Staker common.Address `json:"staker"` + ForStakeType claimType `json:"for_stake_type"` + StakedItemHash common.Hash `json:"staked_item_hash"` + StakedItemChallengeLevel string `json:"staked_item_challenge_level"` + StakedItemCreationTxHash common.Hash `json:"staked_item_creation_tx_hash"` + StakedItemConfirmationTxHash common.Hash `json:"staked_item_confirmation_tx_hash"` +} + +// Will convert a payment request into a pending payment. +// TODO: Allow reimbursement tool to execute pending payments by itself +// through a signing wallet or keystore. +// +// Once a pending payment is created, it can notify the user via their preferred +// means and will output its data as a JSON file for processing. +func (s *service) processServiceFeePaymentRequest(ctx context.Context, request *serviceFeePaymentRequest) { + // If the item is an assertion, its stake is locked up between its creation time + // and min(firstChildCreationTime, confirmationTime). + // Otherwise, if it is an edge, its stake is locked up from creation until confirmation. + var numberOfBlocksToPay uint64 + var creationTxHash, confirmationTxHash common.Hash + signer := types.NewCancunSigner(s.chainId) + challengeLevel := option.None[protocol.ChallengeLevel]() + if request.claimedItemTyp == assertionTyp { + assertion, err := s.rollupBindings.GetAssertion(&bind.CallOpts{}, request.claimedItem) + if err != nil { + panic(err) + } + creationInfo, err := s.readAssertionCreationInfo(ctx, protocol.AssertionHash{Hash: request.claimedItem}) + if err != nil { + panic(err) + } + confirmedAtBlock, confirmTxHash, err := s.fetchAssertionConfirmationBlock(ctx, creationInfo.CreationBlock, request.claimedItem) + if err != nil { + panic(err) + } + start := assertion.CreatedAtBlock + end := assertion.FirstChildBlock + if confirmedAtBlock < end || end == 0 { + end = confirmedAtBlock + } + if start > end { + panic("Invalid block range") + } + numberOfBlocksToPay = end - start + creationTxHash = creationInfo.TransactionHash + confirmationTxHash = confirmTxHash + } else { + eg, err := s.chalManager.GetEdge(&bind.CallOpts{}, request.claimedItem) + if err != nil { + panic(err) + } + numberOfBlocksToPay = eg.ConfirmedAtBlock - eg.CreatedAtBlock + creationTxHash = request.edgeCreationTx + _, confirmTx, err := s.fetchEdgeConfirmationBlockHeader(ctx, eg.ConfirmedAtBlock, &edge{ChallengeEdge: &eg, txHash: creationTxHash, id: request.claimedItem}) + if err != nil { + panic(err) + } + challengeLevel = option.Some(protocol.ChallengeLevel(eg.Level)) + confirmationTxHash = confirmTx.Hash() + } + tx, _, err := s.client.TransactionByHash(ctx, creationTxHash) + if err != nil { + panic(err) + } + staker, err := types.Sender(signer, tx) + if err != nil { + panic(err) + } + pending := &pendingServiceFeePayment{ + NumBlocksPayable: numberOfBlocksToPay, + RecommendedYieldGweiPerBlock: s.recommendedL1YieldPerBlock.String(), + Staker: staker, + StakedItemCreationTxHash: creationTxHash, + StakedItemConfirmationTxHash: confirmationTxHash, + StakedItemHash: request.claimedItem, + ForStakeType: request.claimedItemTyp, + } + if challengeLevel.IsSome() { + pending.StakedItemChallengeLevel = challengeLevel.Unwrap().String() + } + fname := filepath.Join(s.pendingPaymentsOutputDir, fmt.Sprintf("pending_srv_fee_payment_%d_%s_%#x", time.Now().Unix(), request.claimedItemTyp, request.claimedItem)) + f, err := os.Create(fname) + if err != nil { + panic(err) + } + defer func() { + if err = f.Close(); err != nil { + panic(err) + } + }() + if err = json.NewEncoder(f).Encode(pending); err != nil { + panic(err) + } + fmt.Println("Wrote pending service fee payment request", fname) +} + +func (s *service) processGasPaymentRequest(ctx context.Context, request *gasPaymentRequest) { + pending := &pendingGasPayment{ + PayableAddresses: make(map[common.Address]*payableGasInfo), + } + for _, branch := range request.essentialChallengeBranches { + for _, eg := range branch { + tx, _, err := s.client.TransactionByHash(ctx, eg.txHash) + if err != nil { + panic(err) + } + header, err := s.client.HeaderByNumber(ctx, big.NewInt(int64(eg.CreatedAtBlock))) + if err != nil { + panic(err) + } + creationCostGwei, confirmationCostGwei, err := s.determinePayableGasForEdge(ctx, eg, tx, header) + if err != nil { + panic(err) + } + signer := types.NewCancunSigner(s.chainId) + sender, err := signer.Sender(tx) + if err != nil { + panic(err) + } + if _, ok := pending.PayableAddresses[sender]; !ok { + pending.PayableAddresses[sender] = &payableGasInfo{ + EdgeCreationTotalCostGwei: new(big.Int), + ConfirmationTotalCostGwei: new(big.Int), + } + } + existingCreation := pending.PayableAddresses[sender].EdgeCreationTotalCostGwei + existingConf := pending.PayableAddresses[sender].ConfirmationTotalCostGwei + pending.PayableAddresses[sender].EdgeCreationTotalCostGwei = new(big.Int).Add(existingCreation, creationCostGwei) + pending.PayableAddresses[sender].ConfirmationTotalCostGwei = new(big.Int).Add(existingConf, confirmationCostGwei) + } + } + fname := filepath.Join(s.pendingPaymentsOutputDir, fmt.Sprintf("pending_gas_payment_%d_confirmed_claimed_assertion_%#x", time.Now().Unix(), request.confirmedAssertionInChallenge)) + f, err := os.Create(fname) + if err != nil { + panic(err) + } + defer func() { + if err = f.Close(); err != nil { + panic(err) + } + }() + if err = json.NewEncoder(f).Encode(pending); err != nil { + panic(err) + } + fmt.Println("Wrote pending service fee payment request", fname) +} + +// TODO: Is the basefee in gwei? +func (s *service) determinePayableGasForEdge( + ctx context.Context, eg *edge, tx *types.Transaction, header *types.Header, +) (*big.Int, *big.Int, error) { + // If the edge is a refinement move, we compute its creation cost + // using the refinement predetermined gas costs. + var determineConfirmationCost func() uint64 + var determineCreationCost func() uint64 + if eg.ClaimId != ([32]byte{}) { + determineCreationCost = func() uint64 { + switch eg.Level { + case protocol.NewBlockChallengeLevel().Uint8(): + return blockRefinementCreateGasCost + case protocol.NewBlockChallengeLevel().Uint8() + 1: + return bigStepRefinementCreateGasCost + case protocol.NewBlockChallengeLevel().Uint8() + 2: + return smallStepRefinementCreateGasCost + default: + return 1 // TODO: Error instead. + } + } + // Only edges with claim ids are confirmed by time, so we determine + // how much that confirmation transaction cost. + determineConfirmationCost = func() uint64 { + return confirmEdgeByTimeGasCost + } + } else { + // Otherwise, the edge was a bisection edge. + determineCreationCost = func() uint64 { + switch eg.Level { + case protocol.NewBlockChallengeLevel().Uint8(): + return blockBisectGasCost + case protocol.NewBlockChallengeLevel().Uint8() + 1: + return bigStepBisectGasCost + case protocol.NewBlockChallengeLevel().Uint8() + 2: + return smallStepBisectGasCost + default: + return 1 // TODO: Error instead. + } + } + // We check if the edge was one-step-proven, and if so, we check + // how much that one step proof cost. + determineConfirmationCost = func() uint64 { + if isOneStepProven(eg) { + return confirmByOneStepProofGasCost + } + return confirmEdgeByTimeGasCost + } + } + // TODO: Use the min of this and the actual cost onchain. + creationGas := new(big.Int).Mul(header.BaseFee, big.NewInt(int64(determineCreationCost()))) + creationReceipt, err := s.client.TransactionReceipt(ctx, tx.Hash()) + if err != nil { + return nil, nil, err + } + fmt.Printf("Creation block base fee %d\n", header.BaseFee.Uint64()) + fmt.Printf("Creation gas estimated %d vs. actual used %d\n", creationGas.Uint64(), new(big.Int).Mul(creationReceipt.EffectiveGasPrice, big.NewInt(int64(creationReceipt.GasUsed)))) + + confirmationGasGwei := big.NewInt(0) + if protocol.EdgeStatus(eg.Status) == protocol.EdgeConfirmed { + confirmationBlockHeader, confirmationTx, err := s.fetchEdgeConfirmationBlockHeader(ctx, eg.ConfirmedAtBlock, eg) + if err != nil { + return nil, nil, err + } + confirmationReceipt, err := s.client.TransactionReceipt(ctx, confirmationTx.Hash()) + if err != nil { + return nil, nil, err + } + confirmationGas := new(big.Int).Mul(confirmationBlockHeader.BaseFee, big.NewInt(int64(determineConfirmationCost()))) + fmt.Printf("Confirmation block base fee %d\n", confirmationBlockHeader.BaseFee.Uint64()) + fmt.Printf("Confirmation gas estimated %d vs. actual used %d\n", confirmationGas.Uint64(), new(big.Int).Mul(confirmationReceipt.EffectiveGasPrice, big.NewInt(int64(confirmationReceipt.GasUsed)))) + confirmationGasGwei = new(big.Int).Div(confirmationGas, weiToGwei) + } + creationGasGwei := new(big.Int).Div(creationGas, weiToGwei) + return creationGasGwei, confirmationGasGwei, nil +} + +func (s *service) fetchEdgeConfirmationBlockHeader( + ctx context.Context, confirmedAtBlock uint64, eg *edge, +) (*types.Header, *types.Transaction, error) { + var blockNum uint64 + var txHash common.Hash + found := false + if isOneStepProven(eg) { + it, err := s.chalManager.FilterEdgeConfirmedByOneStepProof(&bind.FilterOpts{ + Start: confirmedAtBlock, + End: &confirmedAtBlock, + Context: ctx, + }, [][32]byte{eg.id}, nil) + defer func() { + if err = it.Close(); err != nil { + log.Error("Could not close filter iterator", "err", err) + } + }() + for it.Next() { + if it.Error() != nil { + panic(err) + } + if it.Event.EdgeId == eg.id { + blockNum = it.Event.Raw.BlockNumber + txHash = it.Event.Raw.TxHash + found = true + break + } + } + } else { + it, err := s.chalManager.FilterEdgeConfirmedByTime(&bind.FilterOpts{ + Start: confirmedAtBlock, + End: &confirmedAtBlock, + Context: ctx, + }, [][32]byte{eg.id}, nil) + defer func() { + if err = it.Close(); err != nil { + log.Error("Could not close filter iterator", "err", err) + } + }() + for it.Next() { + if it.Error() != nil { + panic(err) + } + if it.Event.EdgeId == eg.id { + blockNum = it.Event.Raw.BlockNumber + txHash = it.Event.Raw.TxHash + found = true + break + } + } + } + if !found { + return nil, nil, errors.New("no edge confirmation tx found") + } + header, err := s.client.HeaderByNumber(ctx, big.NewInt(int64(blockNum))) + if err != nil { + return nil, nil, err + } + tx, _, err := s.client.TransactionByHash(ctx, txHash) + if err != nil { + return nil, nil, err + } + return header, tx, nil +} + +func (s *service) fetchAssertionConfirmationBlock(ctx context.Context, creationBlock uint64, assertionHash common.Hash) (uint64, common.Hash, error) { + it, err := s.rollupBindings.FilterAssertionConfirmed(&bind.FilterOpts{ + Start: creationBlock, + End: nil, + Context: ctx, + }, [][32]byte{assertionHash}) + defer func() { + if err = it.Close(); err != nil { + log.Error("Could not close filter iterator", "err", err) + } + }() + for it.Next() { + if it.Error() != nil { + panic(err) + } + if it.Event.AssertionHash == assertionHash { + return it.Event.Raw.BlockNumber, it.Event.Raw.TxHash, nil + } + } + return 0, common.Hash{}, fmt.Errorf("assertion %#x not yet confirmed", assertionHash) +} + +func isOneStepProven(eg *edge) bool { + isSmallStep := eg.Level == protocol.NewBlockChallengeLevel().Uint8()+2 + end := eg.EndHeight.Uint64() + start := eg.StartHeight.Uint64() + return isSmallStep && end-start == 1 +} diff --git a/tools/reimbursement-service/reimbursement_computation.go b/tools/reimbursement-service/reimbursement_computation.go new file mode 100644 index 000000000..61ccce02c --- /dev/null +++ b/tools/reimbursement-service/reimbursement_computation.go @@ -0,0 +1,112 @@ +package main + +import ( + "context" + + protocol "github.com/OffchainLabs/bold/chain-abstraction" + "github.com/ethereum/go-ethereum/common" +) + +type reimbursementArgs struct { + claimedAssertion common.Hash + claimedEdge common.Hash + itemTyp claimType + challengeLvl protocol.ChallengeLevel +} + +func (s *service) executeReimbursement(ctx context.Context, args reimbursementArgs) { + var protocolGraph *protocolGraph + var challengeRoot *edge + // If assertion, build the protocol graph for the challenge. + if args.itemTyp == assertionTyp { + graph, err := s.buildProtocolGraphForChallenge(ctx, args.claimedAssertion) + if err != nil { + panic(err) + } + s.protocolGraphLock.Lock() + protocolGraph = graph + s.protocolGraphLock.Unlock() + challengeRoot = protocolGraph.getClaimingEdgeAtLvl(protocol.NewBlockChallengeLevel(), args.claimedAssertion) + } else { + s.protocolGraphLock.RLock() + graph, ok := s.protocolGraphsByClaimedAssertion[args.claimedAssertion] + s.protocolGraphLock.RUnlock() + if !ok { + panic("Could not find protocol graph for claimed assertion") + } + protocolGraph = graph + challengeRoot = protocolGraph.getClaimingEdgeAtLvl(args.challengeLvl, args.claimedEdge) + } + // Get all the terminal nodes descending from item at the current level. + terminalNodes := protocolGraph.terminalEdgesAtLvl(protocol.ChallengeLevel(challengeRoot.Level)) + confirmedRefinements := make([]*edge, 0) + unconfirmedRefinements := make([]*edge, 0) + // Get all the nodes that have a claim id equal to a terminal node extracted above. + for _, terminalNode := range terminalNodes { + refinement := protocolGraph.getClaimingEdgeAtLvl(protocol.ChallengeLevel(challengeRoot.Level)+1, terminalNode.id) + if refinement == nil { + continue + } + // Filter those refinement nodes that are confirmed vs. unconfirmed. + if protocol.EdgeStatus(refinement.Status) == protocol.EdgeConfirmed { + confirmedRefinements = append(confirmedRefinements, refinement) + } else { + unconfirmedRefinements = append(unconfirmedRefinements, refinement) + } + } + + // Add the unconfirmed refinement nodes to the watchset, then remove A from the watchset. + for _, unconfirmed := range unconfirmedRefinements { + s.watchList.Put(unconfirmed.id, edgeTyp) + } + // Process service fee payments and remove items. + if args.itemTyp == assertionTyp { + s.watchList.Delete(args.claimedAssertion) + s.serviceFeePaymentRequests <- &serviceFeePaymentRequest{ + claimedItemTyp: args.itemTyp, + claimedItem: args.claimedAssertion, + } + } else { + s.watchList.Delete(args.claimedEdge) + var eg *edge + s.protocolGraphLock.RLock() + graph := s.protocolGraphsByClaimedAssertion[args.claimedAssertion] + for _, edgesByLvl := range graph.edgesByLevel { + for hash, edg := range edgesByLvl { + if hash == args.claimedEdge { + eg = edg + break + } + } + } + s.protocolGraphLock.RUnlock() + s.serviceFeePaymentRequests <- &serviceFeePaymentRequest{ + claimedItemTyp: args.itemTyp, + claimedItem: args.claimedEdge, + edgeCreationTx: eg.txHash, + } + } + + // For each confirmed refinement, execute the reimbursement function. + for _, refinement := range confirmedRefinements { + s.executeReimbursement(ctx, reimbursementArgs{ + claimedAssertion: args.claimedAssertion, + claimedEdge: refinement.ClaimId, + challengeLvl: protocol.ChallengeLevel(refinement.Level), + itemTyp: edgeTyp, + }) + } + + // Process gas payments. + // Extract all branches descending from item at the current level that have + // big enough timers. They should have their gas refunded. + confirmableBranchesAtLevel := protocolGraph.extractEssentialBranchesAtLvl( + protocol.ChallengeLevel(challengeRoot.Level), + challengeRoot, + ) + // Submit a payment to process. + s.gasPaymentRequests <- &gasPaymentRequest{ + confirmedAssertionInChallenge: args.claimedAssertion, + essentialChallengeBranches: confirmableBranchesAtLevel, + } +}