diff --git a/go.mod b/go.mod index 7b85cebad..8e688ed58 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( buf.build/gen/go/astria/execution-apis/grpc/go v1.3.0-00000000000000-84e5e35facb9.3 buf.build/gen/go/astria/execution-apis/protocolbuffers/go v1.34.1-00000000000000-84e5e35facb9.1 buf.build/gen/go/astria/primitives/protocolbuffers/go v1.34.1-20240529204957-2697e2110d78.1 + buf.build/gen/go/astria/sequencerblock-apis/protocolbuffers/go v1.34.1-20240529204957-1b3cb2034833.1 github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.2.0 github.com/Microsoft/go-winio v0.6.1 github.com/VictoriaMetrics/fastcache v1.12.1 @@ -80,7 +81,6 @@ require ( ) require ( - buf.build/gen/go/astria/sequencerblock-apis/protocolbuffers/go v1.34.1-20240529204957-1b3cb2034833.1 // indirect github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 // indirect github.com/DataDog/zstd v1.4.5 // indirect diff --git a/grpc/execution/server.go b/grpc/execution/server.go index 53eeb0303..cf4927004 100644 --- a/grpc/execution/server.go +++ b/grpc/execution/server.go @@ -239,55 +239,12 @@ func (s *ExecutionServiceServerV1Alpha2) ExecuteBlock(ctx context.Context, req * txsToProcess := types.Transactions{} for _, tx := range req.Transactions { - if deposit := tx.GetDeposit(); deposit != nil { - bridgeAddress := string(deposit.BridgeAddress.GetInner()) - bac, ok := s.bridgeAddresses[bridgeAddress] - if !ok { - log.Debug("ignoring deposit tx from unknown bridge", "bridgeAddress", bridgeAddress) - continue - } - - if len(deposit.AssetId) != 32 { - log.Debug("ignoring deposit tx with invalid asset ID", "assetID", deposit.AssetId) - continue - } - assetID := [32]byte{} - copy(assetID[:], deposit.AssetId[:32]) - if _, ok := s.bridgeAllowedAssetIDs[assetID]; !ok { - log.Debug("ignoring deposit tx with disallowed asset ID", "assetID", deposit.AssetId) - continue - } - - amount := protoU128ToBigInt(deposit.Amount) - address := common.HexToAddress(deposit.DestinationChainAddress) - txdata := types.DepositTx{ - From: address, - Value: bac.ScaledDepositAmount(amount), - Gas: 0, - } - - tx := types.NewTx(&txdata) - txsToProcess = append(txsToProcess, tx) - } else { - ethTx := new(types.Transaction) - err := ethTx.UnmarshalBinary(tx.GetSequencedData()) - if err != nil { - log.Error("failed to unmarshal sequenced data into transaction, ignoring", "tx hash", sha256.Sum256(tx.GetSequencedData()), "err", err) - continue - } - - if ethTx.Type() == types.DepositTxType { - log.Debug("ignoring deposit tx in sequenced data", "tx hash", sha256.Sum256(tx.GetSequencedData())) - continue - } - - if ethTx.Type() == types.BlobTxType { - log.Debug("ignoring blob tx in sequenced data", "tx hash", sha256.Sum256(tx.GetSequencedData())) - continue - } - - txsToProcess = append(txsToProcess, ethTx) + unmarshalledTx, err := validateAndUnmarshalSequencerTx(tx, s.bridgeAddresses, s.bridgeAllowedAssetIDs) + if err != nil { + log.Debug("failed to validate sequencer tx, ignoring", "tx", tx, "err", err) + continue } + txsToProcess = append(txsToProcess, unmarshalledTx) } // This set of ordered TXs on the TxPool is has been configured to be used by diff --git a/grpc/execution/server_test.go b/grpc/execution/server_test.go new file mode 100644 index 000000000..7af30fc4e --- /dev/null +++ b/grpc/execution/server_test.go @@ -0,0 +1,463 @@ +package execution + +import ( + astriaPb "buf.build/gen/go/astria/execution-apis/protocolbuffers/go/astria/execution/v1alpha2" + primitivev1 "buf.build/gen/go/astria/primitives/protocolbuffers/go/astria/primitive/v1" + sequencerblockv1alpha1 "buf.build/gen/go/astria/sequencerblock-apis/protocolbuffers/go/astria/sequencerblock/v1alpha1" + "bytes" + "context" + "crypto/sha256" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/params" + "github.com/stretchr/testify/require" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/timestamppb" + "math/big" + "testing" +) + +func TestExecutionService_GetGenesisInfo(t *testing.T) { + ethservice, serviceV1Alpha1 := setupExecutionService(t, 10) + + genesisInfo, err := serviceV1Alpha1.GetGenesisInfo(context.Background(), &astriaPb.GetGenesisInfoRequest{}) + require.Nil(t, err, "GetGenesisInfo failed") + + hashedRollupId := sha256.Sum256([]byte(ethservice.BlockChain().Config().AstriaRollupName)) + + require.True(t, bytes.Equal(genesisInfo.RollupId, hashedRollupId[:]), "RollupId is not correct") + require.Equal(t, genesisInfo.GetSequencerGenesisBlockHeight(), ethservice.BlockChain().Config().AstriaSequencerInitialHeight, "SequencerInitialHeight is not correct") + require.Equal(t, genesisInfo.GetCelestiaBaseBlockHeight(), ethservice.BlockChain().Config().AstriaCelestiaInitialHeight, "CelestiaInitialHeight is not correct") + require.Equal(t, genesisInfo.GetCelestiaBlockVariance(), ethservice.BlockChain().Config().AstriaCelestiaHeightVariance, "CelestiaHeightVariance is not correct") + require.True(t, serviceV1Alpha1.genesisInfoCalled, "GetGenesisInfo should be called") +} + +func TestExecutionServiceServerV1Alpha2_GetCommitmentState(t *testing.T) { + ethservice, serviceV1Alpha1 := setupExecutionService(t, 10) + + commitmentState, err := serviceV1Alpha1.GetCommitmentState(context.Background(), &astriaPb.GetCommitmentStateRequest{}) + require.Nil(t, err, "GetCommitmentState failed") + + require.NotNil(t, commitmentState, "CommitmentState is nil") + + softBlock := ethservice.BlockChain().CurrentSafeBlock() + require.NotNil(t, softBlock, "SoftBlock is nil") + + firmBlock := ethservice.BlockChain().CurrentFinalBlock() + require.NotNil(t, firmBlock, "FirmBlock is nil") + + require.True(t, bytes.Equal(commitmentState.Soft.Hash, softBlock.Hash().Bytes()), "Soft Block Hashes do not match") + require.True(t, bytes.Equal(commitmentState.Soft.ParentBlockHash, softBlock.ParentHash.Bytes()), "Soft Block Parent Hash do not match") + require.Equal(t, uint64(commitmentState.Soft.Number), softBlock.Number.Uint64(), "Soft Block Number do not match") + + require.True(t, bytes.Equal(commitmentState.Firm.Hash, firmBlock.Hash().Bytes()), "Firm Block Hashes do not match") + require.True(t, bytes.Equal(commitmentState.Firm.ParentBlockHash, firmBlock.ParentHash.Bytes()), "Firm Block Parent Hash do not match") + require.Equal(t, uint64(commitmentState.Firm.Number), firmBlock.Number.Uint64(), "Firm Block Number do not match") + + require.True(t, serviceV1Alpha1.getCommitmentStateCalled, "GetCommitmentState should be called") +} + +func TestExecutionService_GetBlock(t *testing.T) { + ethservice, serviceV1Alpha1 := setupExecutionService(t, 10) + + tests := []struct { + description string + getBlockRequst *astriaPb.GetBlockRequest + expectedReturnCode codes.Code + }{ + { + description: "Get block by block number 1", + getBlockRequst: &astriaPb.GetBlockRequest{ + Identifier: &astriaPb.BlockIdentifier{Identifier: &astriaPb.BlockIdentifier_BlockNumber{BlockNumber: 1}}, + }, + expectedReturnCode: 0, + }, + { + description: "Get block by block hash", + getBlockRequst: &astriaPb.GetBlockRequest{ + Identifier: &astriaPb.BlockIdentifier{Identifier: &astriaPb.BlockIdentifier_BlockHash{BlockHash: ethservice.BlockChain().GetBlockByNumber(4).Hash().Bytes()}}, + }, + expectedReturnCode: 0, + }, + { + description: "Get block which is not present", + getBlockRequst: &astriaPb.GetBlockRequest{ + Identifier: &astriaPb.BlockIdentifier{Identifier: &astriaPb.BlockIdentifier_BlockNumber{BlockNumber: 100}}, + }, + expectedReturnCode: codes.NotFound, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + blockInfo, err := serviceV1Alpha1.GetBlock(context.Background(), tt.getBlockRequst) + if tt.expectedReturnCode > 0 { + require.NotNil(t, err, "GetBlock should return an error") + require.Equal(t, tt.expectedReturnCode, status.Code(err), "GetBlock failed") + } + if err == nil { + require.NotNil(t, blockInfo, "Block not found") + var block *types.Block + if tt.getBlockRequst.Identifier.GetBlockNumber() != 0 { + // get block by number + block = ethservice.BlockChain().GetBlockByNumber(uint64(tt.getBlockRequst.Identifier.GetBlockNumber())) + } + if tt.getBlockRequst.Identifier.GetBlockHash() != nil { + block = ethservice.BlockChain().GetBlockByHash(common.Hash(tt.getBlockRequst.Identifier.GetBlockHash())) + } + require.NotNil(t, block, "Block not found") + + require.Equal(t, uint64(blockInfo.Number), block.NumberU64(), "Block number is not correct") + require.Equal(t, block.ParentHash().Bytes(), blockInfo.ParentBlockHash, "Parent Block Hash is not correct") + require.Equal(t, block.Hash().Bytes(), blockInfo.Hash, "BlockHash is not correct") + } + }) + + } +} + +func TestExecutionServiceServerV1Alpha2_BatchGetBlocks(t *testing.T) { + ethservice, serviceV1Alpha1 := setupExecutionService(t, 10) + + tests := []struct { + description string + batchGetBlockRequest *astriaPb.BatchGetBlocksRequest + expectedReturnCode codes.Code + }{ + { + description: "BatchGetBlocks with block hashes", + batchGetBlockRequest: &astriaPb.BatchGetBlocksRequest{ + Identifiers: []*astriaPb.BlockIdentifier{ + {Identifier: &astriaPb.BlockIdentifier_BlockHash{BlockHash: ethservice.BlockChain().GetBlockByNumber(1).Hash().Bytes()}}, + {Identifier: &astriaPb.BlockIdentifier_BlockHash{BlockHash: ethservice.BlockChain().GetBlockByNumber(2).Hash().Bytes()}}, + {Identifier: &astriaPb.BlockIdentifier_BlockHash{BlockHash: ethservice.BlockChain().GetBlockByNumber(3).Hash().Bytes()}}, + {Identifier: &astriaPb.BlockIdentifier_BlockHash{BlockHash: ethservice.BlockChain().GetBlockByNumber(4).Hash().Bytes()}}, + {Identifier: &astriaPb.BlockIdentifier_BlockHash{BlockHash: ethservice.BlockChain().GetBlockByNumber(5).Hash().Bytes()}}, + }, + }, + expectedReturnCode: 0, + }, + { + description: "BatchGetBlocks with block numbers", + batchGetBlockRequest: &astriaPb.BatchGetBlocksRequest{ + Identifiers: []*astriaPb.BlockIdentifier{ + {Identifier: &astriaPb.BlockIdentifier_BlockNumber{BlockNumber: 1}}, + {Identifier: &astriaPb.BlockIdentifier_BlockNumber{BlockNumber: 2}}, + {Identifier: &astriaPb.BlockIdentifier_BlockNumber{BlockNumber: 3}}, + {Identifier: &astriaPb.BlockIdentifier_BlockNumber{BlockNumber: 4}}, + {Identifier: &astriaPb.BlockIdentifier_BlockNumber{BlockNumber: 5}}, + }, + }, + expectedReturnCode: 0, + }, + { + description: "BatchGetBlocks block not found", + batchGetBlockRequest: &astriaPb.BatchGetBlocksRequest{ + Identifiers: []*astriaPb.BlockIdentifier{ + {Identifier: &astriaPb.BlockIdentifier_BlockNumber{BlockNumber: 1}}, + {Identifier: &astriaPb.BlockIdentifier_BlockNumber{BlockNumber: 2}}, + {Identifier: &astriaPb.BlockIdentifier_BlockNumber{BlockNumber: 3}}, + {Identifier: &astriaPb.BlockIdentifier_BlockNumber{BlockNumber: 4}}, + {Identifier: &astriaPb.BlockIdentifier_BlockNumber{BlockNumber: 100}}, + }, + }, + expectedReturnCode: codes.NotFound, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + batchBlocksRes, err := serviceV1Alpha1.BatchGetBlocks(context.Background(), tt.batchGetBlockRequest) + if tt.expectedReturnCode > 0 { + require.NotNil(t, err, "BatchGetBlocks should return an error") + require.Equal(t, tt.expectedReturnCode, status.Code(err), "BatchGetBlocks failed") + } + + for _, batchBlock := range batchBlocksRes.GetBlocks() { + require.NotNil(t, batchBlock, "Block not found in batch blocks response") + + block := ethservice.BlockChain().GetBlockByNumber(uint64(batchBlock.Number)) + require.NotNil(t, block, "Block not found in blockchain") + + require.Equal(t, uint64(batchBlock.Number), block.NumberU64(), "Block number is not correct") + require.Equal(t, block.ParentHash().Bytes(), batchBlock.ParentBlockHash, "Parent Block Hash is not correct") + require.Equal(t, block.Hash().Bytes(), batchBlock.Hash, "BlockHash is not correct") + } + }) + } +} + +func bigIntToProtoU128(i *big.Int) *primitivev1.Uint128 { + lo := i.Uint64() + hi := new(big.Int).Rsh(i, 64).Uint64() + return &primitivev1.Uint128{Lo: lo, Hi: hi} +} + +func TestExecutionServiceServerV1Alpha2_ExecuteBlock(t *testing.T) { + ethservice, _ := setupExecutionService(t, 10) + + tests := []struct { + description string + callGenesisInfoAndGetCommitmentState bool + numberOfTxs int + prevBlockHash []byte + timestamp uint64 + depositTxAmount *big.Int // if this is non zero then we send a deposit tx + expectedReturnCode codes.Code + }{ + { + description: "ExecuteBlock without calling GetGenesisInfo and GetCommitmentState", + callGenesisInfoAndGetCommitmentState: false, + numberOfTxs: 5, + prevBlockHash: ethservice.BlockChain().GetBlockByNumber(2).Hash().Bytes(), + timestamp: ethservice.BlockChain().GetBlockByNumber(2).Time() + 2, + depositTxAmount: big.NewInt(0), + expectedReturnCode: codes.PermissionDenied, + }, + { + description: "ExecuteBlock with 5 txs and no deposit tx", + callGenesisInfoAndGetCommitmentState: true, + numberOfTxs: 5, + prevBlockHash: ethservice.BlockChain().CurrentSafeBlock().Hash().Bytes(), + timestamp: ethservice.BlockChain().CurrentSafeBlock().Time + 2, + depositTxAmount: big.NewInt(0), + expectedReturnCode: 0, + }, + { + description: "ExecuteBlock with 5 txs and a deposit tx", + callGenesisInfoAndGetCommitmentState: true, + numberOfTxs: 5, + prevBlockHash: ethservice.BlockChain().CurrentSafeBlock().Hash().Bytes(), + timestamp: ethservice.BlockChain().CurrentSafeBlock().Time + 2, + depositTxAmount: big.NewInt(1000000000000000000), + expectedReturnCode: 0, + }, + { + description: "ExecuteBlock with incorrect previous block hash", + callGenesisInfoAndGetCommitmentState: true, + numberOfTxs: 5, + prevBlockHash: ethservice.BlockChain().GetBlockByNumber(2).Hash().Bytes(), + timestamp: ethservice.BlockChain().GetBlockByNumber(2).Time() + 2, + depositTxAmount: big.NewInt(0), + expectedReturnCode: codes.FailedPrecondition, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + // reset the blockchain with each test + ethservice, serviceV1Alpha1 := setupExecutionService(t, 10) + + var err error // adding this to prevent shadowing of genesisInfo in the below if branch + var genesisInfo *astriaPb.GenesisInfo + var commitmentStateBeforeExecuteBlock *astriaPb.CommitmentState + if tt.callGenesisInfoAndGetCommitmentState { + // call getGenesisInfo and getCommitmentState before calling executeBlock + genesisInfo, err = serviceV1Alpha1.GetGenesisInfo(context.Background(), &astriaPb.GetGenesisInfoRequest{}) + require.Nil(t, err, "GetGenesisInfo failed") + require.NotNil(t, genesisInfo, "GenesisInfo is nil") + + commitmentStateBeforeExecuteBlock, err = serviceV1Alpha1.GetCommitmentState(context.Background(), &astriaPb.GetCommitmentStateRequest{}) + require.Nil(t, err, "GetCommitmentState failed") + require.NotNil(t, commitmentStateBeforeExecuteBlock, "CommitmentState is nil") + } + + // create the txs to send + // create 5 txs + txs := []*types.Transaction{} + marshalledTxs := []*sequencerblockv1alpha1.RollupData{} + for i := 0; i < 5; i++ { + unsignedTx := types.NewTransaction(uint64(i), testToAddress, big.NewInt(1), params.TxGas, big.NewInt(params.InitialBaseFee*2), nil) + tx, err := types.SignTx(unsignedTx, types.LatestSigner(ethservice.BlockChain().Config()), testKey) + require.Nil(t, err, "Failed to sign tx") + txs = append(txs, tx) + + marshalledTx, err := tx.MarshalBinary() + require.Nil(t, err, "Failed to marshal tx") + marshalledTxs = append(marshalledTxs, &sequencerblockv1alpha1.RollupData{ + Value: &sequencerblockv1alpha1.RollupData_SequencedData{SequencedData: marshalledTx}, + }) + } + + // create deposit tx if depositTxAmount is non zero + if tt.depositTxAmount.Cmp(big.NewInt(0)) != 0 { + depositAmount := bigIntToProtoU128(tt.depositTxAmount) + bridgeAddress := ethservice.BlockChain().Config().AstriaBridgeAddressConfigs[0].BridgeAddress + bridgeAssetDenom := sha256.Sum256([]byte(ethservice.BlockChain().Config().AstriaBridgeAddressConfigs[0].AssetDenom)) + + // create new chain destination address for better testing + chainDestinationAddressPrivKey, err := crypto.GenerateKey() + require.Nil(t, err, "Failed to generate chain destination address") + + chainDestinationAddress := crypto.PubkeyToAddress(chainDestinationAddressPrivKey.PublicKey) + + depositTx := &sequencerblockv1alpha1.RollupData{Value: &sequencerblockv1alpha1.RollupData_Deposit{Deposit: &sequencerblockv1alpha1.Deposit{ + BridgeAddress: &primitivev1.Address{ + Inner: bridgeAddress, + }, + AssetId: bridgeAssetDenom[:], + Amount: depositAmount, + RollupId: &primitivev1.RollupId{Inner: genesisInfo.RollupId}, + DestinationChainAddress: chainDestinationAddress.String(), + }}} + + marshalledTxs = append(marshalledTxs, depositTx) + } + + executeBlockReq := &astriaPb.ExecuteBlockRequest{ + PrevBlockHash: tt.prevBlockHash, + Timestamp: ×tamppb.Timestamp{ + Seconds: int64(tt.timestamp), + }, + Transactions: marshalledTxs, + } + + executeBlockRes, err := serviceV1Alpha1.ExecuteBlock(context.Background(), executeBlockReq) + if tt.expectedReturnCode > 0 { + require.NotNil(t, err, "ExecuteBlock should return an error") + require.Equal(t, tt.expectedReturnCode, status.Code(err), "ExecuteBlock failed") + } + if err == nil { + require.NotNil(t, executeBlockRes, "ExecuteBlock response is nil") + + astriaOrdered := ethservice.TxPool().AstriaOrdered() + require.Equal(t, 0, astriaOrdered.Len(), "AstriaOrdered should be empty") + + // check if commitment state is not updated + commitmentStateAfterExecuteBlock, err := serviceV1Alpha1.GetCommitmentState(context.Background(), &astriaPb.GetCommitmentStateRequest{}) + require.Nil(t, err, "GetCommitmentState failed") + + require.Exactly(t, commitmentStateBeforeExecuteBlock, commitmentStateAfterExecuteBlock, "Commitment state should not be updated") + } + + }) + } +} + +func TestExecutionServiceServerV1Alpha2_ExecuteBlockAndUpdateCommitment(t *testing.T) { + ethservice, serviceV1Alpha1 := setupExecutionService(t, 10) + + // call genesis info + genesisInfo, err := serviceV1Alpha1.GetGenesisInfo(context.Background(), &astriaPb.GetGenesisInfoRequest{}) + require.Nil(t, err, "GetGenesisInfo failed") + require.NotNil(t, genesisInfo, "GenesisInfo is nil") + + // call get commitment state + commitmentState, err := serviceV1Alpha1.GetCommitmentState(context.Background(), &astriaPb.GetCommitmentStateRequest{}) + require.Nil(t, err, "GetCommitmentState failed") + require.NotNil(t, commitmentState, "CommitmentState is nil") + + // get previous block hash + previousBlock := ethservice.BlockChain().CurrentSafeBlock() + require.NotNil(t, previousBlock, "Previous block not found") + + // create 5 txs + txs := []*types.Transaction{} + marshalledTxs := []*sequencerblockv1alpha1.RollupData{} + for i := 0; i < 5; i++ { + unsignedTx := types.NewTransaction(uint64(i), testToAddress, big.NewInt(1), params.TxGas, big.NewInt(params.InitialBaseFee*2), nil) + tx, err := types.SignTx(unsignedTx, types.LatestSigner(ethservice.BlockChain().Config()), testKey) + require.Nil(t, err, "Failed to sign tx") + txs = append(txs, tx) + + marshalledTx, err := tx.MarshalBinary() + require.Nil(t, err, "Failed to marshal tx") + marshalledTxs = append(marshalledTxs, &sequencerblockv1alpha1.RollupData{ + Value: &sequencerblockv1alpha1.RollupData_SequencedData{SequencedData: marshalledTx}, + }) + } + + amountToDeposit := big.NewInt(1000000000000000000) + depositAmount := bigIntToProtoU128(amountToDeposit) + bridgeAddress := ethservice.BlockChain().Config().AstriaBridgeAddressConfigs[0].BridgeAddress + bridgeAssetDenom := sha256.Sum256([]byte(ethservice.BlockChain().Config().AstriaBridgeAddressConfigs[0].AssetDenom)) + + // create new chain destination address for better testing + chainDestinationAddressPrivKey, err := crypto.GenerateKey() + require.Nil(t, err, "Failed to generate chain destination address") + + chainDestinationAddress := crypto.PubkeyToAddress(chainDestinationAddressPrivKey.PublicKey) + + stateDb, err := ethservice.BlockChain().State() + require.Nil(t, err, "Failed to get state db") + require.NotNil(t, stateDb, "State db is nil") + + chainDestinationAddressBalanceBefore := stateDb.GetBalance(chainDestinationAddress) + + depositTx := &sequencerblockv1alpha1.RollupData{Value: &sequencerblockv1alpha1.RollupData_Deposit{Deposit: &sequencerblockv1alpha1.Deposit{ + BridgeAddress: &primitivev1.Address{ + Inner: bridgeAddress, + }, + AssetId: bridgeAssetDenom[:], + Amount: depositAmount, + RollupId: &primitivev1.RollupId{Inner: genesisInfo.RollupId}, + DestinationChainAddress: chainDestinationAddress.String(), + }}} + + marshalledTxs = append(marshalledTxs, depositTx) + + executeBlockReq := &astriaPb.ExecuteBlockRequest{ + PrevBlockHash: previousBlock.Hash().Bytes(), + Timestamp: ×tamppb.Timestamp{ + Seconds: int64(previousBlock.Time + 2), + }, + Transactions: marshalledTxs, + } + + executeBlockRes, err := serviceV1Alpha1.ExecuteBlock(context.Background(), executeBlockReq) + require.Nil(t, err, "ExecuteBlock failed") + + require.NotNil(t, executeBlockRes, "ExecuteBlock response is nil") + + // check if astria ordered txs are cleared + astriaOrdered := ethservice.TxPool().AstriaOrdered() + require.Equal(t, 0, astriaOrdered.Len(), "AstriaOrdered should be empty") + + // call update commitment state to set the block we executed as soft and firm + updateCommitmentStateReq := &astriaPb.UpdateCommitmentStateRequest{ + CommitmentState: &astriaPb.CommitmentState{ + Soft: &astriaPb.Block{ + Hash: executeBlockRes.Hash, + ParentBlockHash: executeBlockRes.ParentBlockHash, + Number: executeBlockRes.Number, + Timestamp: executeBlockRes.Timestamp, + }, + Firm: &astriaPb.Block{ + Hash: executeBlockRes.Hash, + ParentBlockHash: executeBlockRes.ParentBlockHash, + Number: executeBlockRes.Number, + Timestamp: executeBlockRes.Timestamp, + }, + }, + } + + updateCommitmentStateRes, err := serviceV1Alpha1.UpdateCommitmentState(context.Background(), updateCommitmentStateReq) + require.Nil(t, err, "UpdateCommitmentState failed") + require.NotNil(t, updateCommitmentStateRes, "UpdateCommitmentState response should not be nil") + + // get the soft and firm block + softBlock := ethservice.BlockChain().CurrentSafeBlock() + require.NotNil(t, softBlock, "SoftBlock is nil") + firmBlock := ethservice.BlockChain().CurrentFinalBlock() + require.NotNil(t, firmBlock, "FirmBlock is nil") + + // check if the soft and firm block are set correctly + require.True(t, bytes.Equal(softBlock.Hash().Bytes(), updateCommitmentStateRes.Soft.Hash), "Soft Block Hashes do not match") + require.True(t, bytes.Equal(softBlock.ParentHash.Bytes(), updateCommitmentStateRes.Soft.ParentBlockHash), "Soft Block Parent Hash do not match") + require.Equal(t, softBlock.Number.Uint64(), uint64(updateCommitmentStateRes.Soft.Number), "Soft Block Number do not match") + + require.True(t, bytes.Equal(firmBlock.Hash().Bytes(), updateCommitmentStateRes.Firm.Hash), "Firm Block Hashes do not match") + require.True(t, bytes.Equal(firmBlock.ParentHash.Bytes(), updateCommitmentStateRes.Firm.ParentBlockHash), "Firm Block Parent Hash do not match") + require.Equal(t, firmBlock.Number.Uint64(), uint64(updateCommitmentStateRes.Firm.Number), "Firm Block Number do not match") + + // check the difference in balances after deposit tx + stateDb, err = ethservice.BlockChain().State() + require.Nil(t, err, "Failed to get state db") + require.NotNil(t, stateDb, "State db is nil") + chainDestinationAddressBalanceAfter := stateDb.GetBalance(chainDestinationAddress) + + balanceDiff := new(big.Int).Sub(chainDestinationAddressBalanceAfter, chainDestinationAddressBalanceBefore) + require.True(t, balanceDiff.Cmp(big.NewInt(1000000000000000000)) == 0, "Chain destination address balance is not correct") +} diff --git a/grpc/execution/test_utils.go b/grpc/execution/test_utils.go new file mode 100644 index 000000000..794d0ece3 --- /dev/null +++ b/grpc/execution/test_utils.go @@ -0,0 +1,145 @@ +package execution + +import ( + "crypto/ecdsa" + "crypto/sha256" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/consensus" + beaconConsensus "github.com/ethereum/go-ethereum/consensus/beacon" + "github.com/ethereum/go-ethereum/consensus/ethash" + "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" + "github.com/ethereum/go-ethereum/eth/downloader" + "github.com/ethereum/go-ethereum/eth/ethconfig" + "github.com/ethereum/go-ethereum/node" + "github.com/ethereum/go-ethereum/params" + "github.com/stretchr/testify/require" + "math/big" + "testing" + "time" +) + +var ( + // testKey is a private key to use for funding a tester account. + testKey, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") + + // testAddr is the Ethereum address of the tester account. + testAddr = crypto.PubkeyToAddress(testKey.PublicKey) + + testToAddress = common.HexToAddress("0x9a9070028361F7AAbeB3f2F2Dc07F82C4a98A02a") + + testBalance = big.NewInt(2e18) +) + +func generateMergeChain(n int, merged bool) (*core.Genesis, []*types.Block, *ecdsa.PrivateKey, *ecdsa.PrivateKey) { + config := *params.AllEthashProtocolChanges + engine := consensus.Engine(beaconConsensus.New(ethash.NewFaker())) + if merged { + config.TerminalTotalDifficulty = common.Big0 + config.TerminalTotalDifficultyPassed = true + engine = beaconConsensus.NewFaker() + } + + bridgeAddressKey, err := crypto.GenerateKey() + if err != nil { + panic(err) + } + bridgeAddress := crypto.PubkeyToAddress(bridgeAddressKey.PublicKey) + + config.AstriaRollupName = "astria" + config.AstriaSequencerInitialHeight = 10 + config.AstriaCelestiaInitialHeight = 10 + config.AstriaCelestiaHeightVariance = 10 + config.AstriaBridgeAddressConfigs = []params.AstriaBridgeAddressConfig{ + { + BridgeAddress: bridgeAddress.Bytes(), + StartHeight: 2, + AssetDenom: "nria", + AssetPrecision: 18, + Erc20Asset: nil, + }, + } + + feeCollectorKey, err := crypto.GenerateKey() + if err != nil { + panic(err) + } + feeCollector := crypto.PubkeyToAddress(feeCollectorKey.PublicKey) + + astriaFeeCollectors := make(map[uint32]common.Address) + astriaFeeCollectors[1] = feeCollector + config.AstriaFeeCollectors = astriaFeeCollectors + + genesis := &core.Genesis{ + Config: &config, + Alloc: core.GenesisAlloc{ + testAddr: {Balance: testBalance}, + }, + ExtraData: []byte("test genesis"), + Timestamp: 9000, + BaseFee: big.NewInt(params.InitialBaseFee), + Difficulty: big.NewInt(0), + } + testNonce := uint64(0) + generate := func(i int, g *core.BlockGen) { + g.OffsetTime(5) + g.SetExtra([]byte("test")) + tx, _ := types.SignTx(types.NewTransaction(testNonce, testToAddress, big.NewInt(1), params.TxGas, big.NewInt(params.InitialBaseFee*2), nil), types.LatestSigner(&config), testKey) + g.AddTx(tx) + testNonce++ + } + _, blocks, _ := core.GenerateChainWithGenesis(genesis, engine, n, generate) + + if !merged { + totalDifficulty := big.NewInt(0) + for _, b := range blocks { + totalDifficulty.Add(totalDifficulty, b.Difficulty()) + } + config.TerminalTotalDifficulty = totalDifficulty + } + + return genesis, blocks, bridgeAddressKey, feeCollectorKey +} + +// startEthService creates a full node instance for testing. +func startEthService(t *testing.T, genesis *core.Genesis) *eth.Ethereum { + n, err := node.New(&node.Config{}) + require.Nil(t, err, "can't create node") + + ethcfg := ðconfig.Config{Genesis: genesis, SyncMode: downloader.FullSync, TrieTimeout: time.Minute, TrieDirtyCache: 256, TrieCleanCache: 256} + ethservice, err := eth.New(n, ethcfg) + require.Nil(t, err, "can't create eth service") + + ethservice.SetEtherbase(testAddr) + ethservice.SetSynced() + + return ethservice +} + +func setupExecutionService(t *testing.T, noOfBlocksToGenerate int) (*eth.Ethereum, *ExecutionServiceServerV1Alpha2) { + t.Helper() + genesis, blocks, bridgeAddressKey, feeCollectorKey := generateMergeChain(noOfBlocksToGenerate, true) + ethservice := startEthService(t, genesis) + + serviceV1Alpha1, err := NewExecutionServiceServerV1Alpha2(ethservice) + require.Nil(t, err, "can't create execution service") + + feeCollector := crypto.PubkeyToAddress(feeCollectorKey.PublicKey) + require.Equal(t, feeCollector, serviceV1Alpha1.nextFeeRecipient, "nextFeeRecipient not set correctly") + + bridgeAsset := sha256.Sum256([]byte(genesis.Config.AstriaBridgeAddressConfigs[0].AssetDenom)) + _, ok := serviceV1Alpha1.bridgeAllowedAssetIDs[bridgeAsset] + require.True(t, ok, "bridgeAllowedAssetIDs does not contain bridge asset id") + + bridgeAddress := crypto.PubkeyToAddress(bridgeAddressKey.PublicKey) + _, ok = serviceV1Alpha1.bridgeAddresses[string(bridgeAddress.Bytes())] + require.True(t, ok, "bridgeAddress not set correctly") + + _, err = ethservice.BlockChain().InsertChain(blocks) + require.Nil(t, err, "can't insert blocks") + + return ethservice, serviceV1Alpha1 + +} diff --git a/grpc/execution/validation.go b/grpc/execution/validation.go new file mode 100644 index 000000000..ddfb9e255 --- /dev/null +++ b/grpc/execution/validation.go @@ -0,0 +1,60 @@ +package execution + +import ( + sequencerblockv1alpha1 "buf.build/gen/go/astria/sequencerblock-apis/protocolbuffers/go/astria/sequencerblock/v1alpha1" + "crypto/sha256" + "fmt" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/params" +) + +// `validateAndUnmarshalSequencerTx` validates and unmarshals the given rollup sequencer transaction. +// If the sequencer transaction is a deposit tx, we ensure that the asset ID is allowed and the bridge address is known. +// If the sequencer transaction is not a deposit tx, we unmarshal the sequenced data into an Ethereum transaction. We ensure that the +// tx is not a blob tx or a deposit tx. +func validateAndUnmarshalSequencerTx(tx *sequencerblockv1alpha1.RollupData, bridgeAddresses map[string]*params.AstriaBridgeAddressConfig, bridgeAllowedAssetIDs map[[32]byte]struct{}) (*types.Transaction, error) { + if deposit := tx.GetDeposit(); deposit != nil { + bridgeAddress := string(deposit.BridgeAddress.GetInner()) + bac, ok := bridgeAddresses[bridgeAddress] + if !ok { + return nil, fmt.Errorf("unknown bridge address: %s", bridgeAddress) + } + + if len(deposit.AssetId) != 32 { + return nil, fmt.Errorf("invalid asset ID: %x", deposit.AssetId) + } + assetID := [32]byte{} + copy(assetID[:], deposit.AssetId[:32]) + if _, ok := bridgeAllowedAssetIDs[assetID]; !ok { + return nil, fmt.Errorf("disallowed asset ID: %x", deposit.AssetId) + } + + amount := protoU128ToBigInt(deposit.Amount) + address := common.HexToAddress(deposit.DestinationChainAddress) + txdata := types.DepositTx{ + From: address, + Value: bac.ScaledDepositAmount(amount), + Gas: 0, + } + + tx := types.NewTx(&txdata) + return tx, nil + } else { + ethTx := new(types.Transaction) + err := ethTx.UnmarshalBinary(tx.GetSequencedData()) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal sequenced data into transaction: %w. tx hash: %s", err, sha256.Sum256(tx.GetSequencedData())) + } + + if ethTx.Type() == types.DepositTxType { + return nil, fmt.Errorf("deposit tx not allowed in sequenced data. tx hash: %s", sha256.Sum256(tx.GetSequencedData())) + } + + if ethTx.Type() == types.BlobTxType { + return nil, fmt.Errorf("blob tx not allowed in sequenced data. tx hash: %s", sha256.Sum256(tx.GetSequencedData())) + } + + return ethTx, nil + } +} diff --git a/grpc/execution/validation_test.go b/grpc/execution/validation_test.go new file mode 100644 index 000000000..c9e247d8b --- /dev/null +++ b/grpc/execution/validation_test.go @@ -0,0 +1,164 @@ +package execution + +import ( + primitivev1 "buf.build/gen/go/astria/primitives/protocolbuffers/go/astria/primitive/v1" + sequencerblockv1alpha1 "buf.build/gen/go/astria/sequencerblock-apis/protocolbuffers/go/astria/sequencerblock/v1alpha1" + "crypto/sha256" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/params" + "github.com/holiman/uint256" + "github.com/stretchr/testify/require" + "math/big" + "testing" +) + +func testBlobTx() *types.Transaction { + return types.NewTx(&types.BlobTx{ + Nonce: 1, + To: testAddr, + Value: uint256.NewInt(1000), + Gas: 1000, + Data: []byte("data"), + }) +} + +func testDepositTx() *types.Transaction { + return types.NewTx(&types.DepositTx{ + From: testAddr, + Value: big.NewInt(1000), + Gas: 1000, + }) +} + +func TestSequenceTxValidation(t *testing.T) { + ethservice, serviceV1Alpha1 := setupExecutionService(t, 10) + + blobTx, err := testBlobTx().MarshalBinary() + require.Nil(t, err, "failed to marshal random blob tx: %v", err) + + depositTx, err := testDepositTx().MarshalBinary() + require.Nil(t, err, "failed to marshal random deposit tx: %v", err) + + unsignedTx := types.NewTransaction(uint64(0), common.HexToAddress("0x9a9070028361F7AAbeB3f2F2Dc07F82C4a98A02a"), big.NewInt(1), params.TxGas, big.NewInt(params.InitialBaseFee*2), nil) + tx, err := types.SignTx(unsignedTx, types.LatestSigner(ethservice.BlockChain().Config()), testKey) + require.Nil(t, err, "failed to sign tx: %v", err) + + validMarshalledTx, err := tx.MarshalBinary() + require.Nil(t, err, "failed to marshal valid tx: %v", err) + + chainDestinationKey, err := crypto.GenerateKey() + require.Nil(t, err, "failed to generate chain destination key: %v", err) + chainDestinationAddress := crypto.PubkeyToAddress(chainDestinationKey.PublicKey) + + bridgeAssetDenom := sha256.Sum256([]byte(ethservice.BlockChain().Config().AstriaBridgeAddressConfigs[0].AssetDenom)) + invalidBridgeAssetDenom := sha256.Sum256([]byte("invalid-asset-denom")) + + bridgeAddress := ethservice.BlockChain().Config().AstriaBridgeAddressConfigs[0].BridgeAddress + + tests := []struct { + description string + sequencerTx *sequencerblockv1alpha1.RollupData + // just check if error contains the string since error contains other details + wantErr string + }{ + { + description: "unmarshallable sequencer tx", + sequencerTx: &sequencerblockv1alpha1.RollupData{ + Value: &sequencerblockv1alpha1.RollupData_SequencedData{ + SequencedData: []byte("unmarshallable tx"), + }, + }, + wantErr: "failed to unmarshal sequenced data into transaction", + }, + { + description: "blob type sequence tx", + sequencerTx: &sequencerblockv1alpha1.RollupData{ + Value: &sequencerblockv1alpha1.RollupData_SequencedData{ + SequencedData: blobTx, + }, + }, + wantErr: "blob tx not allowed in sequenced data", + }, + { + description: "deposit type sequence tx", + sequencerTx: &sequencerblockv1alpha1.RollupData{ + Value: &sequencerblockv1alpha1.RollupData_SequencedData{ + SequencedData: depositTx, + }, + }, + wantErr: "deposit tx not allowed in sequenced data", + }, + { + description: "deposit tx with an unknown bridge address", + sequencerTx: &sequencerblockv1alpha1.RollupData{Value: &sequencerblockv1alpha1.RollupData_Deposit{Deposit: &sequencerblockv1alpha1.Deposit{ + BridgeAddress: &primitivev1.Address{ + Inner: []byte("unknown-bridge-address"), + }, + AssetId: bridgeAssetDenom[:], + Amount: bigIntToProtoU128(big.NewInt(1000000000000000000)), + RollupId: &primitivev1.RollupId{Inner: make([]byte, 0)}, + DestinationChainAddress: chainDestinationAddress.String(), + }}}, + wantErr: "unknown bridge address", + }, + { + description: "deposit tx with an invalid asset id", + sequencerTx: &sequencerblockv1alpha1.RollupData{Value: &sequencerblockv1alpha1.RollupData_Deposit{Deposit: &sequencerblockv1alpha1.Deposit{ + BridgeAddress: &primitivev1.Address{ + Inner: bridgeAddress, + }, + AssetId: []byte("invalid-asset-id"), + Amount: bigIntToProtoU128(big.NewInt(1000000000000000000)), + RollupId: &primitivev1.RollupId{Inner: make([]byte, 0)}, + DestinationChainAddress: chainDestinationAddress.String(), + }}}, + wantErr: "invalid asset ID", + }, + { + description: "deposit tx with a disallowed asset id", + sequencerTx: &sequencerblockv1alpha1.RollupData{Value: &sequencerblockv1alpha1.RollupData_Deposit{Deposit: &sequencerblockv1alpha1.Deposit{ + BridgeAddress: &primitivev1.Address{ + Inner: bridgeAddress, + }, + AssetId: invalidBridgeAssetDenom[:], + Amount: bigIntToProtoU128(big.NewInt(1000000000000000000)), + RollupId: &primitivev1.RollupId{Inner: make([]byte, 0)}, + DestinationChainAddress: chainDestinationAddress.String(), + }}}, + wantErr: "disallowed asset ID", + }, + { + description: "valid deposit tx", + sequencerTx: &sequencerblockv1alpha1.RollupData{Value: &sequencerblockv1alpha1.RollupData_Deposit{Deposit: &sequencerblockv1alpha1.Deposit{ + BridgeAddress: &primitivev1.Address{ + Inner: bridgeAddress, + }, + AssetId: bridgeAssetDenom[:], + Amount: bigIntToProtoU128(big.NewInt(1000000000000000000)), + RollupId: &primitivev1.RollupId{Inner: make([]byte, 0)}, + DestinationChainAddress: chainDestinationAddress.String(), + }}}, + wantErr: "", + }, + { + description: "valid sequencer tx", + sequencerTx: &sequencerblockv1alpha1.RollupData{ + Value: &sequencerblockv1alpha1.RollupData_SequencedData{SequencedData: validMarshalledTx}, + }, + wantErr: "", + }, + } + + for _, test := range tests { + t.Run(test.description, func(t *testing.T) { + _, err := validateAndUnmarshalSequencerTx(test.sequencerTx, serviceV1Alpha1.bridgeAddresses, serviceV1Alpha1.bridgeAllowedAssetIDs) + if test.wantErr == "" && err == nil { + return + } + require.False(t, test.wantErr == "" && err != nil, "expected error, got nil") + require.Contains(t, err.Error(), test.wantErr) + }) + } +} diff --git a/params/astria_config_test.go b/params/astria_config_test.go index f2b100a87..5d94c0c62 100644 --- a/params/astria_config_test.go +++ b/params/astria_config_test.go @@ -2,6 +2,8 @@ package params import ( "encoding/json" + "fmt" + "github.com/ethereum/go-ethereum/crypto" "math/big" "reflect" "testing" @@ -95,3 +97,123 @@ func TestAstriaEIP1559Params(t *testing.T) { } } } + +func TestAstriaBridgeConfigValidation(t *testing.T) { + bridgeAddressKey, err := crypto.GenerateKey() + if err != nil { + panic(err) + } + bridgeAddress := crypto.PubkeyToAddress(bridgeAddressKey.PublicKey) + + erc20AssetKey, err := crypto.GenerateKey() + if err != nil { + panic(err) + } + erc20Asset := crypto.PubkeyToAddress(erc20AssetKey.PublicKey) + + tests := []struct { + description string + config AstriaBridgeAddressConfig + wantErr error + }{ + { + description: "invalid bridge address", + config: AstriaBridgeAddressConfig{ + BridgeAddress: []byte("rand address"), + StartHeight: 2, + AssetDenom: "nria", + AssetPrecision: 18, + Erc20Asset: nil, + }, + wantErr: fmt.Errorf("bridge address must be 20 bytes"), + }, + { + description: "invalid start height", + config: AstriaBridgeAddressConfig{ + BridgeAddress: bridgeAddress.Bytes(), + StartHeight: 0, + AssetDenom: "nria", + AssetPrecision: 18, + Erc20Asset: nil, + }, + wantErr: fmt.Errorf("start height must be greater than 0"), + }, + { + description: "invalid asset denom", + config: AstriaBridgeAddressConfig{ + BridgeAddress: bridgeAddress.Bytes(), + StartHeight: 2, + AssetDenom: "", + AssetPrecision: 18, + Erc20Asset: nil, + }, + wantErr: fmt.Errorf("asset denom must be set"), + }, + { + description: "invalid asset precision", + config: AstriaBridgeAddressConfig{ + BridgeAddress: bridgeAddress.Bytes(), + StartHeight: 2, + AssetDenom: "nria", + AssetPrecision: 22, + Erc20Asset: nil, + }, + wantErr: fmt.Errorf("asset precision of native asset must be less than or equal to 18"), + }, + { + description: "invalid contract precision", + config: AstriaBridgeAddressConfig{ + BridgeAddress: bridgeAddress.Bytes(), + StartHeight: 2, + AssetDenom: "nria", + AssetPrecision: 22, + Erc20Asset: &AstriaErc20AssetConfig{ + Erc20Address: erc20Asset, + ContractPrecision: 18, + }, + }, + wantErr: fmt.Errorf("asset precision must be less than or equal to contract precision"), + }, + { + description: "erc20 assets not supported", + config: AstriaBridgeAddressConfig{ + BridgeAddress: bridgeAddress.Bytes(), + StartHeight: 2, + AssetDenom: "nria", + AssetPrecision: 18, + Erc20Asset: &AstriaErc20AssetConfig{ + Erc20Address: erc20Asset, + ContractPrecision: 18, + }, + }, + wantErr: fmt.Errorf("cannot currently process erc20 bridged assets"), + }, + { + description: "valid config", + config: AstriaBridgeAddressConfig{ + BridgeAddress: bridgeAddress.Bytes(), + StartHeight: 2, + AssetDenom: "nria", + AssetPrecision: 18, + Erc20Asset: nil, + }, + wantErr: nil, + }, + } + + for _, test := range tests { + t.Run(test.description, func(t *testing.T) { + err := test.config.Validate() + if test.wantErr != nil && err == nil { + t.Errorf("expected error, got nil") + } + if test.wantErr == nil && err != nil { + t.Errorf("unexpected error %v", err) + } + + if !reflect.DeepEqual(err, test.wantErr) { + t.Errorf("error mismatch:\nconfig: %v\nerr: %v\nwant: %v", test.config, err, test.wantErr) + } + }) + } +}