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

feat(MSFDG): add getSubValues function #84

Merged
merged 1 commit into from
Nov 8, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
3 changes: 2 additions & 1 deletion op-challenger2/game/fault/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ type Responder interface {

type ClaimLoader interface {
GetAllClaims(ctx context.Context, block rpcblock.Block) ([]types.Claim, error)
GetAllClaimsWithSubValues(ctx context.Context) ([]types.Claim, error)
IsL2BlockNumberChallenged(ctx context.Context, block rpcblock.Block) (bool, error)
GetMaxGameDepth(ctx context.Context) (types.Depth, error)
GetSplitDepth(ctx context.Context) (types.Depth, error)
Expand Down Expand Up @@ -235,7 +236,7 @@ func (a *Agent) resolveClaims(ctx context.Context) error {

// newGameFromContracts initializes a new game state from the state in the contract
func (a *Agent) newGameFromContracts(ctx context.Context) (types.Game, error) {
claims, err := a.loader.GetAllClaims(ctx, rpcblock.Latest)
claims, err := a.loader.GetAllClaimsWithSubValues(ctx)
if err != nil {
return nil, fmt.Errorf("failed to fetch claims: %w", err)
}
Expand Down
8 changes: 8 additions & 0 deletions op-challenger2/game/fault/agent_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,14 @@ func (s *stubClaimLoader) GetAllClaims(_ context.Context, _ rpcblock.Block) ([]t
return s.claims, nil
}

func (s *stubClaimLoader) GetAllClaimsWithSubValues(_ context.Context) ([]types.Claim, error) {
s.callCount++
if s.callCount > s.maxLoads && s.maxLoads != 0 {
return []types.Claim{}, nil
}
return s.claims, nil
}

func (s *stubClaimLoader) GetMaxGameDepth(_ context.Context) (types.Depth, error) {
return s.maxDepth, nil
}
Expand Down
97 changes: 97 additions & 0 deletions op-challenger2/game/fault/contracts/faultdisputegame.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,15 @@ import (
"github.com/ethereum-optimism/optimism/op-challenger2/game/fault/contracts/metrics"
"github.com/ethereum-optimism/optimism/op-challenger2/game/fault/types"
gameTypes "github.com/ethereum-optimism/optimism/op-challenger2/game/types"
"github.com/ethereum-optimism/optimism/op-e2e/bindings"
"github.com/ethereum-optimism/optimism/op-service/sources/batching"
"github.com/ethereum-optimism/optimism/op-service/sources/batching/rpcblock"
"github.com/ethereum-optimism/optimism/op-service/txmgr"
"github.com/ethereum-optimism/optimism/packages/contracts-bedrock/snapshots"
"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/crypto"
"github.com/ethereum/go-ethereum/rlp"
)

Expand Down Expand Up @@ -58,6 +61,9 @@ var (
methodMaxAttackBranch = "maxAttackBranch"
methodAttackV2 = "attackV2"
methodStepV2 = "stepV2"
fieldSubValues = "_claims"
fieldAttackBranch = "_attackBranch"
eventMove = "Move"
)

var (
Expand Down Expand Up @@ -140,6 +146,23 @@ func mustParseAbi(json []byte) *abi.ABI {
return &loaded
}

func SubValuesHash(values []common.Hash) common.Hash {
nelem := len(values)
hashes := make([]common.Hash, nelem)
copy(hashes, values)
for nelem != 1 {
for i := 0; i < nelem/2; i++ {
hashes[i] = crypto.Keccak256Hash(hashes[i*2][:], hashes[i*2+1][:])
}
// directly copy the last item
if nelem%2 == 1 {
hashes[nelem/2] = hashes[nelem-1]
}
nelem = (nelem + 1) / 2
}
return hashes[0]
}

// GetBalance returns the total amount of ETH controlled by this contract.
// Note that the ETH is actually held by the DelayedWETH contract which may be shared by multiple games.
// Returns the balance and the address of the contract that actually holds the balance.
Expand Down Expand Up @@ -447,6 +470,79 @@ func (f *FaultDisputeGameContractLatest) GetAllClaims(ctx context.Context, block
return claims, nil
}

func (f *FaultDisputeGameContractLatest) GetAllClaimsWithSubValues(ctx context.Context) ([]types.Claim, error) {
claims, err := f.GetAllClaims(ctx, rpcblock.Latest)
if err != nil {
return nil, err
}
for idx, claim := range claims {
subValues, attackBranch, err := f.getSubValuesAndAttackBranch(ctx, &claim)
if err != nil {
return nil, fmt.Errorf("failed to load subValues: %w", err)
}
claim.SubValues = &subValues
claim.AttackBranch = attackBranch
claims[idx] = claim
}
return claims, nil
}

func (f *FaultDisputeGameContractLatest) getSubValuesAndAttackBranch(ctx context.Context, aggClaim *types.Claim) ([]common.Hash, uint64, error) {
defer f.metrics.StartContractRequest("GetSubValues")()

filter, err := bindings.NewFaultDisputeGameFilterer(f.contract.Addr(), f.multiCaller)
if err != nil {
return nil, 0, err
}

parentIndex := [...]*big.Int{big.NewInt(int64(aggClaim.ParentContractIndex))}
claim := [...][32]byte{aggClaim.ClaimData.ValueBytes()}
claimant := [...]common.Address{aggClaim.Claimant}
moveIter, err := filter.FilterMove(&bind.FilterOpts{Context: ctx}, parentIndex[:], claim[:], claimant[:])
if err != nil {
return nil, 0, fmt.Errorf("failed to filter move event log: %w", err)
}
ok := moveIter.Next()
if !ok {
return nil, 0, fmt.Errorf("failed to get move event log: %w", moveIter.Error())
}
txHash := moveIter.Event.Raw.TxHash

getTxByHashCall := batching.NewTxGetByHash(f.contract.Abi(), txHash, methodAttackV2)
result, err := f.multiCaller.SingleCall(ctx, rpcblock.Latest, getTxByHashCall)
if err != nil {
return nil, 0, fmt.Errorf("failed to load attackV2's calldata: %w", err)
}

txn := result.GetTx()
var subValues []common.Hash
var attackBranch uint64
if len(txn.BlobHashes()) > 0 {
// todo: fetch Blobs and unpack it into subValues and attackBranch
return nil, 0, fmt.Errorf("blob tx hasn't been supported")
} else {
inputMap, err := getTxByHashCall.UnpackCallData(&txn)
if err != nil {
return nil, 0, fmt.Errorf("failed to unpack tx resp: %w", err)
}
attackBranchU256 := *abi.ConvertType(inputMap[fieldAttackBranch], new(big.Int)).(*big.Int)
attackBranch = attackBranchU256.Uint64()

maxAttackBranch, err := f.GetMaxAttackBranch(ctx)
if err != nil {
return nil, 0, err
}
valuesBytesLen := uint(32 * maxAttackBranch)
bytes := abi.ConvertType(inputMap[fieldSubValues], make([]byte, valuesBytesLen)).([]byte)
for i := uint64(0); i < maxAttackBranch; i++ {
hash := common.BytesToHash(bytes[i*32 : (i+1)*32])
subValues = append(subValues, hash)
}
}

return subValues, attackBranch, nil
}

func (f *FaultDisputeGameContractLatest) IsResolved(ctx context.Context, block rpcblock.Block, claims ...types.Claim) ([]bool, error) {
defer f.metrics.StartContractRequest("IsResolved")()
calls := make([]batching.Call, 0, len(claims))
Expand Down Expand Up @@ -670,4 +766,5 @@ type FaultDisputeGameContract interface {
StepV2Tx(claimIdx uint64, attackBranch uint64, stateData []byte, proof types.StepProof) (txmgr.TxCandidate, error)
GetNBits(ctx context.Context) (uint64, error)
GetMaxAttackBranch(ctx context.Context) (uint64, error)
GetAllClaimsWithSubValues(ctx context.Context) ([]types.Claim, error)
}
126 changes: 126 additions & 0 deletions op-challenger2/game/fault/contracts/faultdisputegame_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import (
"github.com/ethereum-optimism/optimism/packages/contracts-bedrock/snapshots"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
coreTypes "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/rlp"
"github.com/stretchr/testify/require"
)
Expand Down Expand Up @@ -851,3 +853,127 @@ func TestStepV2Tx(t *testing.T) {
})
}
}

func TestGetAllClaimsWithSubValues(t *testing.T) {
for _, version := range versions {
t.Run(version.version, func(t *testing.T) {
stubRpc, game := setupFaultDisputeGameTest(t, version)
block := rpcblock.Latest

claimant := common.Address{0xbb}
bond := big.NewInt(1044)
nBits := uint64(2)
claimsBytes := make([]byte, ((1<<nBits)-1)*32)
subValues := []common.Hash{{0x10}, {0x11}, {0x12}}
for i := range claimsBytes {
claimsBytes[i] = subValues[i/32][i%32]
}
daType := faultTypes.CallDataType
parentPos := faultTypes.NewPosition(0, big.NewInt(0))
// attackBranch must be 0, because we can only attach the first branch of rootClaim
attackBranch := big.NewInt(0)
parent := faultTypes.Claim{ClaimData: faultTypes.ClaimData{Value: common.Hash{0xbb}, Position: parentPos}, ContractIndex: 111}
stubRpc.SetResponse(fdgAddr, methodNBits, block, nil, []interface{}{new(big.Int).SetUint64(nBits)})
stubRpc.SetResponse(fdgAddr, methodRequiredBond, block, []interface{}{parent.Position.MoveN(nBits, attackBranch.Uint64()).ToGIndex()}, []interface{}{bond})
stubRpc.SetResponse(fdgAddr, methodAttackV2, block, []interface{}{parent.Value, big.NewInt(111), attackBranch, daType, claimsBytes[:]}, nil)
txCandidate, err := game.AttackV2Tx(context.Background(), parent, attackBranch.Uint64(), daType.Uint64(), claimsBytes[:])
require.NoError(t, err)
tx := coreTypes.NewTx(&coreTypes.LegacyTx{
Nonce: 0,
GasPrice: big.NewInt(11111),
Gas: 1111,
To: &claimant,
Value: big.NewInt(111),
Data: txCandidate.TxData,
})
packed, err := tx.MarshalBinary()
require.NoError(t, err)
txHash := tx.Hash()
stubRpc.SetGetTxByHashResponse(txHash, packed)

// mock eventLog
eventName := eventMove
fdgAbi := version.loadAbi()
challgenIndex := []interface{}{big.NewInt(int64(parent.ContractIndex))}
claim := []interface{}{SubValuesHash(subValues)}
address := []interface{}{claimant}
query := [][]interface{}{challgenIndex, claim, address}

query = append([][]interface{}{{fdgAbi.Events[eventName].ID}}, query...)
topics, err := abi.MakeTopics(query...)
require.NoError(t, err)
var queryTopics []common.Hash
for _, item := range topics {
queryTopics = append(queryTopics, item[0])
}
out := []coreTypes.Log{
{
Address: fdgAddr,
Topics: queryTopics,
Data: []byte{},
TxHash: txHash,
},
}

stubRpc.SetFilterLogResponse(topics, fdgAddr, block, out)
stubRpc.SetResponse(fdgAddr, methodClaimCount, block, nil, []interface{}{big.NewInt(1)})
stubRpc.SetResponse(fdgAddr, methodMaxAttackBranch, block, nil, []interface{}{big.NewInt(1<<nBits - 1)})
claim0 := faultTypes.Claim{
ClaimData: faultTypes.ClaimData{
Value: SubValuesHash(subValues),
Position: parent.Position.MoveN(nBits, attackBranch.Uint64()),
Bond: bond,
},
CounteredBy: common.Address{0x00},
Claimant: claimant,
Clock: decodeClock(big.NewInt(1234)),
// contractIndex is mocked to 0, because there is only one claim for GetAllClaimsWithSubValues
ContractIndex: 0,
ParentContractIndex: parent.ContractIndex,
SubValues: &subValues,
AttackBranch: attackBranch.Uint64(),
}
expectedClaims := []faultTypes.Claim{claim0}
for _, claim := range expectedClaims {
expectGetClaim(stubRpc, block, claim)
}

claims, err := game.GetAllClaimsWithSubValues(context.Background())
require.NoError(t, err)
require.Equal(t, 1, len(claims))
require.Equal(t, claim0.SubValues, claims[0].SubValues)
claim0.SubValues = nil
claims[0].SubValues = nil
require.Equal(t, 0, claim0.ClaimData.IndexAtDepth().Cmp(claims[0].ClaimData.IndexAtDepth()))
claim0.ClaimData = faultTypes.ClaimData{}
claims[0].ClaimData = faultTypes.ClaimData{}
require.Equal(t, claim0, claims[0])
})
}
}

func TestSubValuesHash(t *testing.T) {
allClaims := [][]common.Hash{
{{0x01}, {0x02}, {0x03}},
{{0x01}, {0x02}, {0x03}, {0x04}},
}
Hash := crypto.Keccak256Hash
tests := []struct {
claims []common.Hash
expectedHash common.Hash
}{
{
claims: allClaims[0],
expectedHash: Hash(Hash(allClaims[0][0][:], allClaims[0][1][:]).Bytes(), allClaims[0][2][:]),
},
{
claims: allClaims[1],
expectedHash: Hash(Hash(allClaims[1][0][:], allClaims[1][1][:]).Bytes(), Hash(allClaims[1][2][:], allClaims[1][3][:]).Bytes()),
},
}

for _, test := range tests {
actualHash := SubValuesHash(test.claims)
require.Equal(t, test.expectedHash, actualHash)
}
}
2 changes: 1 addition & 1 deletion op-challenger2/game/fault/types/position.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ func bigMSB(x *big.Int) Depth {
func (p Position) MoveN(nbits uint64, attackingBranch uint64) Position {
return Position{
depth: p.depth + Depth(nbits),
indexAtDepth: new(big.Int).Lsh(new(big.Int).Add(p.indexAtDepth, big.NewInt(int64(attackingBranch))), uint(nbits)),
indexAtDepth: new(big.Int).Lsh(new(big.Int).Add(p.IndexAtDepth(), big.NewInt(int64(attackingBranch))), uint(nbits)),
}
}

Expand Down
4 changes: 4 additions & 0 deletions op-service/sources/batching/bound.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ func (b *BoundContract) Addr() common.Address {
return b.addr
}

func (b *BoundContract) Abi() *abi.ABI {
return b.abi
}

func (b *BoundContract) Call(method string, args ...interface{}) *ContractCall {
return NewContractCall(b.abi, b.addr, method, args...)
}
Expand Down
5 changes: 5 additions & 0 deletions op-service/sources/batching/call.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"github.com/ethereum-optimism/optimism/op-service/sources/batching/rpcblock"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/rpc"
)

Expand Down Expand Up @@ -67,3 +68,7 @@ func (c *CallResult) GetBytes32Slice(i int) [][32]byte {
func (c *CallResult) GetString(i int) string {
return *abi.ConvertType(c.out[i], new(string)).(*string)
}

func (c *CallResult) GetTx() types.Transaction {
return *abi.ConvertType(c.out[0], new(types.Transaction)).(*types.Transaction)
}
37 changes: 37 additions & 0 deletions op-service/sources/batching/event_call.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package batching

import (
"github.com/ethereum-optimism/optimism/op-service/sources/batching/rpcblock"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/rpc"
)

type EventCall struct {
topics [][]common.Hash
to []common.Address
}

func NewEventCall(q ethereum.FilterQuery) *EventCall {
return &EventCall{
topics: q.Topics,
to: q.Addresses,
}
}

func (b *EventCall) ToBatchElemCreator() (BatchElementCreator, error) {
return func(block rpcblock.Block) (any, rpc.BatchElem) {
out := new([]types.Log)
return out, rpc.BatchElem{
Method: "eth_getFilterLogs",
Args: []interface{}{b.topics, b.to[0], block.ArgValue()},
Result: &out,
}
}, nil
}

func (c *EventCall) HandleResult(result interface{}) (*CallResult, error) {
res := result.(*[]types.Log)
return &CallResult{out: []interface{}{*res}}, nil
}
Loading
Loading