diff --git a/nil/cmd/nild/devnet.go b/nil/cmd/nild/devnet.go index 158870e5f..4eda89b3d 100644 --- a/nil/cmd/nild/devnet.go +++ b/nil/cmd/nild/devnet.go @@ -139,7 +139,7 @@ func (c *cluster) generateZeroState(nShards uint32, servers []server) (*executio return nil, err } - zeroState, err := execution.CreateDefaultZeroStateConfig(mainPublicKey) + zeroState, err := execution.CreateDefaultZeroStateConfig(mainPublicKey, nShards) if err != nil { return nil, err } diff --git a/nil/contracts/solidity/system/L1BlockInfo.sol b/nil/contracts/solidity/system/L1BlockInfo.sol index 1adf62b45..06d8ba03b 100644 --- a/nil/contracts/solidity/system/L1BlockInfo.sol +++ b/nil/contracts/solidity/system/L1BlockInfo.sol @@ -16,4 +16,4 @@ contract L1BlockInfo { require(msg.sender == SELF_ADDRESS, "setL1BlockInfo: only L1BlockInfo contract can be caller of this function"); Nil.setConfigParam("l1block", abi.encode(Nil.ParamL1BlockInfo(_number, _timestamp, _baseFee, _blobBaseFee, _hash))); } -} \ No newline at end of file +} diff --git a/nil/contracts/solidity/system/MessageQueue.sol b/nil/contracts/solidity/system/MessageQueue.sol new file mode 100644 index 000000000..5a73dac3f --- /dev/null +++ b/nil/contracts/solidity/system/MessageQueue.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.15; + +import "../lib/IMessageQueue.sol"; + +struct Message { + bytes data; + address sender; +} + +contract MessageQueue is IMessageQueue{ + function sendRawTransaction(bytes calldata _message) external override { + queue.push(Message({ + data: _message, + sender: msg.sender + })); + } + + function getMessages() external view returns (Message[] memory) { + return queue; + } + + function clearQueue() external { + require(msg.sender == address(this), "clearQueue: only MessageQueue contract can be caller of this function"); + delete queue; + } + + Message[] private queue; +} diff --git a/nil/internal/collate/proposer.go b/nil/internal/collate/proposer.go index 9c2541887..e70fea12f 100644 --- a/nil/internal/collate/proposer.go +++ b/nil/internal/collate/proposer.go @@ -102,6 +102,10 @@ func (p *proposer) GenerateProposal(ctx context.Context, txFabric db.DB) (*execu return nil, fmt.Errorf("failed to fetch last block hashes: %w", err) } + if err := p.handleMessageQueue(prevBlock.Id); err != nil { + return nil, fmt.Errorf("failed to handle message queue: %w", err) + } + if err := p.handleL1Attributes(tx, prevBlockHash); err != nil { // TODO: change to Error severity once Consensus/Proposer increase time intervals p.logger.Trace().Err(err).Msg("Failed to handle L1 attributes") @@ -165,6 +169,15 @@ func (p *proposer) fetchLastBlockHashes(tx db.RoTx) error { return nil } +func (p *proposer) handleMessageQueue(bn types.BlockNumber) error { + txn, err := execution.CreateMQPruneTransaction(p.params.ShardId, bn) + if err != nil { + return fmt.Errorf("failed to create MQ prune transaction: %w", err) + } + p.proposal.SpecialTxns = append(p.proposal.SpecialTxns, txn) + return nil +} + func (p *proposer) handleL1Attributes(tx db.RoTx, mainShardHash common.Hash) error { if !p.params.ShardId.IsMainShard() { return nil @@ -300,7 +313,9 @@ func (p *proposer) handleTransactionsFromPool() error { return false, res.FatalError } else if res.Failed() { p.logger.Info().Stringer(logging.FieldTransactionHash, txnHash). - Err(res.Error).Msg("External txn validation failed. Saved failure receipt. Dropping...") + Err(res.Error). + Stringer(logging.FieldTransactionTo, txn.To). + Msg("External txn validation failed. Saved failure receipt. Dropping...") execution.AddFailureReceipt(txnHash, txn.To, res) unverified = append(unverified, txnHash) diff --git a/nil/internal/contracts/contract.go b/nil/internal/contracts/contract.go index 2b440405f..bc30c9133 100644 --- a/nil/internal/contracts/contract.go +++ b/nil/internal/contracts/contract.go @@ -21,6 +21,7 @@ const ( NameNilConfigAbi = "NilConfigAbi" NameL1BlockInfo = "system/L1BlockInfo" NameGovernance = "system/Governance" + NameMessageQueue = "system/MessageQueue" ) var ( diff --git a/nil/internal/execution/block_generator.go b/nil/internal/execution/block_generator.go index 3e7c5c53e..e57cc130e 100644 --- a/nil/internal/execution/block_generator.go +++ b/nil/internal/execution/block_generator.go @@ -260,6 +260,19 @@ func (g *BlockGenerator) prepareExecutionState(proposal *Proposal, gasPrices []t g.executionState.AppendForwardTransaction(txn) } + messages, err := GetMessageQueueContent(g.executionState) + if err != nil { + return fmt.Errorf("failed to get message queue content: %w", err) + } + + for _, msg := range messages { + if err := HandleOutMessage(g.executionState, &msg); err != nil { + g.logger.Err(err).Stringer(logging.FieldTransactionFrom, msg.Address). + Msg("Failed to handle out message") + continue + } + } + g.executionState.ChildShardBlocks = make(map[types.ShardId]common.Hash, len(proposal.ShardHashes)) for i, shardHash := range proposal.ShardHashes { g.executionState.ChildShardBlocks[types.ShardId(i+1)] = shardHash diff --git a/nil/internal/execution/execution_state_test.go b/nil/internal/execution/execution_state_test.go index 4ce44cebd..43b5042df 100644 --- a/nil/internal/execution/execution_state_test.go +++ b/nil/internal/execution/execution_state_test.go @@ -239,7 +239,7 @@ func newState(t *testing.T) *ExecutionState { state.BaseFee = types.DefaultGasPrice require.NoError(t, err) - defaultZeroStateConfig, err := CreateDefaultZeroStateConfig(MainPublicKey) + defaultZeroStateConfig, err := CreateDefaultZeroStateConfig(MainPublicKey, 4) require.NoError(t, err) err = state.GenerateZeroState(defaultZeroStateConfig) require.NoError(t, err) diff --git a/nil/internal/execution/mq_manager.go b/nil/internal/execution/mq_manager.go new file mode 100644 index 000000000..ecb129aa2 --- /dev/null +++ b/nil/internal/execution/mq_manager.go @@ -0,0 +1,126 @@ +package execution + +import ( + "fmt" + + "github.com/NilFoundation/nil/nil/internal/config" + "github.com/NilFoundation/nil/nil/internal/contracts" + "github.com/NilFoundation/nil/nil/internal/types" + "github.com/NilFoundation/nil/nil/internal/vm" +) + +type message struct { + Data []byte + Address types.Address +} + +func CreateMQPruneTransaction(shardId types.ShardId, bn types.BlockNumber) (*types.Transaction, error) { + abi, err := contracts.GetAbi(contracts.NameMessageQueue) + if err != nil { + return nil, fmt.Errorf("failed to get MessageQueue ABI: %w", err) + } + + calldata, err := abi.Pack("clearQueue") + if err != nil { + return nil, fmt.Errorf("failed to pack clearQueue calldata: %w", err) + } + + addr := types.GetMessageQueueAddress(shardId) + txn := &types.Transaction{ + TransactionDigest: types.TransactionDigest{ + Flags: types.NewTransactionFlags(types.TransactionFlagInternal), + To: addr, + FeeCredit: types.GasToValue(types.DefaultMaxGasInBlock.Uint64()), + MaxFeePerGas: types.MaxFeePerGasDefault, + MaxPriorityFeePerGas: types.Value0, + Data: calldata, + Seqno: types.Seqno(bn + 1), + }, + RefundTo: addr, + From: addr, + } + + return txn, nil +} + +func GetMessageQueueContent(es *ExecutionState) ([]message, error) { + addr := types.GetMessageQueueAddress(es.ShardId) + account, err := es.GetAccount(addr) + if err != nil { + return nil, fmt.Errorf("failed to get message queue smart contract: %w", err) + } + + abi, err := contracts.GetAbi(contracts.NameMessageQueue) + if err != nil { + return nil, fmt.Errorf("failed to get MessageQueue ABI: %w", err) + } + + calldata, err := abi.Pack("getMessages") + if err != nil { + return nil, fmt.Errorf("failed to pack getMessages calldata: %w", err) + } + + if err := es.newVm(true, addr, nil); err != nil { + return nil, fmt.Errorf("failed to create VM: %w", err) + } + defer es.resetVm() + + ret, _, err := es.evm.StaticCall( + (vm.AccountRef)(account.address), account.address, calldata, types.DefaultMaxGasInBlock.Uint64()) + if err != nil { + return nil, fmt.Errorf("failed to get message queue content: %w", err) + } + + var result []message + if err := abi.UnpackIntoInterface(&result, "getMessages", ret); err != nil { + return nil, fmt.Errorf("failed to unpack getMessages return data: %w", err) + } + + return result, nil +} + +func HandleOutMessage(es *ExecutionState, msg *message) error { + var payload types.InternalTransactionPayload + if err := payload.UnmarshalSSZ(msg.Data); err != nil { + return types.NewWrapError(types.ErrorInvalidTransactionInputUnmarshalFailed, err) + } + + cfgAccessor := es.GetConfigAccessor() + nShards, err := config.GetParamNShards(cfgAccessor) + if err != nil { + return types.NewVmVerboseError(types.ErrorPrecompileConfigGetParamFailed, err.Error()) + } + + if uint32(payload.To.ShardId()) >= nShards { + return vm.ErrShardIdIsTooBig + } + + if payload.To.ShardId().IsMainShard() { + return vm.ErrTransactionToMainShard + } + + // TODO: support estimate fee for such messages + payload.FeeCredit = types.MaxFeePerGasDefault + + // TODO: withdrawFunds should be implemneted + // if err := withdrawFunds(es, msg.Address, payload.Value); err != nil { + // return nil, fmt.Errorf("withdraw value failed: %w", err) + // } + + // if payload.ForwardKind == types.ForwardKindNone { + // if err := withdrawFunds(es, msg.Address, payload.FeeCredit); err != nil { + // return nil, fmt.Errorf("withdraw FeeCredit failed: %w", err) + // } + // } + + // TODO: We should consider non-refundable transactions + if payload.RefundTo == types.EmptyAddress { + payload.RefundTo = msg.Address + } + if payload.BounceTo == types.EmptyAddress { + payload.BounceTo = msg.Address + } + + _, err = es.AddOutTransaction(msg.Address, &payload) + return err +} diff --git a/nil/internal/execution/testaide.go b/nil/internal/execution/testaide.go index a1e0e39e9..16b7698e9 100644 --- a/nil/internal/execution/testaide.go +++ b/nil/internal/execution/testaide.go @@ -43,7 +43,7 @@ func GenerateZeroState(t *testing.T, shardId types.ShardId, txFabric db.DB) *typ require.NoError(t, err) defer g.Rollback() - zerostateCfg, err := CreateDefaultZeroStateConfig(MainPublicKey) + zerostateCfg, err := CreateDefaultZeroStateConfig(MainPublicKey, 4) require.NoError(t, err) zerostateCfg.ConfigParams = ConfigParams{ GasPrice: config.ParamGasPrice{ diff --git a/nil/internal/execution/zerostate.go b/nil/internal/execution/zerostate.go index b912ea171..c32ce70e9 100644 --- a/nil/internal/execution/zerostate.go +++ b/nil/internal/execution/zerostate.go @@ -39,7 +39,7 @@ type ZeroStateConfig struct { Contracts []*ContractDescr `yaml:"contracts"` } -func CreateDefaultZeroStateConfig(mainPublicKey []byte) (*ZeroStateConfig, error) { +func CreateDefaultZeroStateConfig(mainPublicKey []byte, nShards uint32) (*ZeroStateConfig, error) { smartAccountValue, err := types.NewValueFromDecimal("100000000000000000000000000000000000000000000000000") if err != nil { return nil, err @@ -78,6 +78,17 @@ func CreateDefaultZeroStateConfig(mainPublicKey []byte) (*ZeroStateConfig, error }, }, } + + for i := range types.ShardId(nShards) { + zeroStateConfig.Contracts = append(zeroStateConfig.Contracts, &ContractDescr{ + Name: fmt.Sprintf("Shard%d", i), + Contract: "system/MessageQueue", + Address: types.GetMessageQueueAddress(i), + Value: smartAccountValue, + CtorArgs: []any{}, + }) + } + return zeroStateConfig, nil } diff --git a/nil/internal/execution/zerostate_test.go b/nil/internal/execution/zerostate_test.go index 41f713338..73e5b5356 100644 --- a/nil/internal/execution/zerostate_test.go +++ b/nil/internal/execution/zerostate_test.go @@ -35,7 +35,7 @@ func (s *SuiteZeroState) SetupSuite() { var err error s.ctx = context.Background() - defaultZeroStateConfig, err := CreateDefaultZeroStateConfig(MainPublicKey) + defaultZeroStateConfig, err := CreateDefaultZeroStateConfig(MainPublicKey, 2) s.Require().NoError(err) faucetAddress := defaultZeroStateConfig.GetContractAddress("Faucet") @@ -66,7 +66,7 @@ func (s *SuiteZeroState) getBalance(address types.Address) types.Value { } func (s *SuiteZeroState) TestYamlSerialization() { - orig, err := CreateDefaultZeroStateConfig(MainPublicKey) + orig, err := CreateDefaultZeroStateConfig(MainPublicKey, 2) s.Require().NoError(err) yamlData, err := yaml.Marshal(orig) diff --git a/nil/internal/types/address.go b/nil/internal/types/address.go index 2ae0711f4..d7a1d46f2 100644 --- a/nil/internal/types/address.go +++ b/nil/internal/types/address.go @@ -34,6 +34,10 @@ var ( GovernanceAddress = ShardAndHexToAddress(MainShardId, "777777777777777777777777777777777777") ) +func GetMessageQueueAddress(shardId ShardId) Address { + return ShardAndHexToAddress(shardId, "333333333333333333333333333333333333") +} + func GetTokenName(addr TokenId) string { switch Address(addr) { case FaucetAddress: diff --git a/nil/services/nilservice/service.go b/nil/services/nilservice/service.go index 971ddb20a..e9bce49c1 100644 --- a/nil/services/nilservice/service.go +++ b/nil/services/nilservice/service.go @@ -479,7 +479,7 @@ func CreateNode( if cfg.ZeroState == nil { var err error - cfg.ZeroState, err = execution.CreateDefaultZeroStateConfig(nil) + cfg.ZeroState, err = execution.CreateDefaultZeroStateConfig(nil, cfg.NShards) if err != nil { logger.Error().Err(err).Msg("Failed to create default zero state config") return nil, err diff --git a/nil/tests/basic/basic_test.go b/nil/tests/basic/basic_test.go index 9494aa582..2d7232dfe 100644 --- a/nil/tests/basic/basic_test.go +++ b/nil/tests/basic/basic_test.go @@ -53,8 +53,9 @@ func (s *SuiteRpc) SetupSuite() { return s.dbMock } s.Start(&nilservice.Config{ - NShards: 5, - HttpUrl: rpc.GetSockPath(s.T()), + NShards: 5, + HttpUrl: rpc.GetSockPath(s.T()), + DisableConsensus: true, // NOTE: caching won't work with parallel tests in this module, because global cache will be shared EnableConfigCache: true, @@ -721,15 +722,15 @@ func (s *SuiteRpc) TestRpcBlockContent() { block, err = s.Client.GetBlock(s.Context, types.BaseShardId, "latest", false) s.Require().NoError(err) - return len(block.TransactionHashes) > 0 + return len(block.TransactionHashes) > 1 }, 6*time.Second, 50*time.Millisecond) block, err = s.Client.GetBlock(s.Context, types.BaseShardId, block.Hash, true) s.Require().NoError(err) s.Require().NotNil(block.Hash) - s.Require().Len(block.Transactions, 1) - s.Equal(hash, block.Transactions[0].Hash) + s.Require().Len(block.Transactions, 2) + s.Equal(hash, block.Transactions[1].Hash) } func (s *SuiteRpc) TestRpcTransactionContent() { diff --git a/nil/tests/rpc_suite.go b/nil/tests/rpc_suite.go index 942461393..9cd46d416 100644 --- a/nil/tests/rpc_suite.go +++ b/nil/tests/rpc_suite.go @@ -84,7 +84,7 @@ func (s *RpcSuite) Start(cfg *nilservice.Config) { if cfg.ZeroState == nil { var err error - cfg.ZeroState, err = execution.CreateDefaultZeroStateConfig(execution.MainPublicKey) + cfg.ZeroState, err = execution.CreateDefaultZeroStateConfig(execution.MainPublicKey, cfg.NShards) s.Require().NoError(err) } diff --git a/nil/tests/sharded_suite.go b/nil/tests/sharded_suite.go index 4164567a1..b39fb136f 100644 --- a/nil/tests/sharded_suite.go +++ b/nil/tests/sharded_suite.go @@ -210,7 +210,7 @@ func (s *ShardedSuite) start( if cfg.ZeroState == nil { var err error - cfg.ZeroState, err = execution.CreateDefaultZeroStateConfig(execution.MainPublicKey) + cfg.ZeroState, err = execution.CreateDefaultZeroStateConfig(execution.MainPublicKey, cfg.NShards) s.Require().NoError(err) } diff --git a/smart-contracts/contracts/IMessageQueue.sol b/smart-contracts/contracts/IMessageQueue.sol new file mode 100644 index 000000000..7fcda2ab6 --- /dev/null +++ b/smart-contracts/contracts/IMessageQueue.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.15; + +interface IMessageQueue { + function sendRawTransaction(bytes calldata _message) external; +} diff --git a/smart-contracts/contracts/SmartAccount.sol b/smart-contracts/contracts/SmartAccount.sol index 7e9d0c56c..e22ecc6ac 100644 --- a/smart-contracts/contracts/SmartAccount.sol +++ b/smart-contracts/contracts/SmartAccount.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.9; import "./NilTokenBase.sol"; +import "./IMessageQueue.sol"; /** * @title SmartAccount @@ -39,7 +40,11 @@ contract SmartAccount is NilTokenBase { * @param transaction The raw transaction to send. */ function send(bytes calldata transaction) public onlyExternal { - Nil.sendTransaction(transaction); + bytes20 addrBytes = bytes20(address(this)); + bytes2 prefix = bytes2(addrBytes); + bytes18 suffix = hex"333333333333333333333333333333333333"; + bytes20 result = bytes20(abi.encodePacked(prefix, suffix)); + IMessageQueue(address(result)).sendRawTransaction(transaction); } /**