Skip to content
Draft
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
2 changes: 1 addition & 1 deletion nil/cmd/nild/devnet.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
2 changes: 1 addition & 1 deletion nil/contracts/solidity/system/L1BlockInfo.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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)));
}
}
}
29 changes: 29 additions & 0 deletions nil/contracts/solidity/system/MessageQueue.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.15;

import "../lib/IMessageQueue.sol";

struct Message {
bytes data;
address sender;
Comment on lines +7 to +8
Copy link
Collaborator

Choose a reason for hiding this comment

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

it may be convenient to introduce more fields here (e.g. tokens)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In the first version I'd just want to check that it will work at all. basic test passed. We can discussed it and extends current implementation.

}

contract MessageQueue is IMessageQueue{
function sendRawTransaction(bytes calldata _message) external override {
Copy link
Contributor

Choose a reason for hiding this comment

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

What are thoughts around supporting attaching value?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Value is attached inside this message. It's out InternalTransactionPayload encoded with SSZ.
We can try to send messages encoded in EVM serialization but it's also should be discussed.

Do we need to send SSZ inside EVM at all. Or should be accept only ABI packed data and only then we need to repack into SSZ

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;
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe it is worth to separate this array by shard. So the other shard can fetch only messages addressed to him: getMessages(uint shardId)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Right now we don't have a special interface to send data to specific shard. Our smart accounts operates with raw calldata and we can extract destination shard only after decoding a message. But we don't have a SSZ decoder in Solidity. So here we have single queue for all messages. In future then we will pass destination shards/address explicitly it will be possible.

Copy link
Contributor

Choose a reason for hiding this comment

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

What are the limits on the size of the queue or message size in queue?

Copy link
Contributor

Choose a reason for hiding this comment

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

Also how would this compare to another approach such as emitting an event instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

What are the limits on the size of the queue or message size in queue?

Right now there is no any limits but we can add them

Also how would this compare to another approach such as emitting an event instead?

I didn't think about emitting events but this looks like an attempt to move all magic out of EVM. Sounds interesting but I'm not sure it's what we expect. Should be discussed. Thanks for idea.

}
17 changes: 16 additions & 1 deletion nil/internal/collate/proposer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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)
Copy link
Contributor

Choose a reason for hiding this comment

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

What happens if the tx fails or if the block containing the tx is not finalized?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If such TX fail we fail block validation on consensus and then new block in the next round will be suggested.

return nil
}

func (p *proposer) handleL1Attributes(tx db.RoTx, mainShardHash common.Hash) error {
if !p.params.ShardId.IsMainShard() {
return nil
Expand Down Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions nil/internal/contracts/contract.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const (
NameNilConfigAbi = "NilConfigAbi"
NameL1BlockInfo = "system/L1BlockInfo"
NameGovernance = "system/Governance"
NameMessageQueue = "system/MessageQueue"
)

var (
Expand Down
13 changes: 13 additions & 0 deletions nil/internal/execution/block_generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,19 @@ func (g *BlockGenerator) prepareExecutionState(proposal *Proposal, gasPrices []t
g.executionState.AppendForwardTransaction(txn)
}

messages, err := GetMessageQueueContent(g.executionState)
Copy link
Contributor

Choose a reason for hiding this comment

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

Shouldn't we get messages queue from other shards targeting to our shard?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We should do that by proposer. I suggest to implement step-by-step solution. It's the first step in this directon.

Right now an approach doesn't break existing code. We just copy a message queue content to an existing outbound messages tree. Ideally, we need to drop old approach and then it will be possible to use only queue account state.

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
Expand Down
2 changes: 1 addition & 1 deletion nil/internal/execution/execution_state_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
126 changes: 126 additions & 0 deletions nil/internal/execution/mq_manager.go
Original file line number Diff line number Diff line change
@@ -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
}
2 changes: 1 addition & 1 deletion nil/internal/execution/testaide.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
13 changes: 12 additions & 1 deletion nil/internal/execution/zerostate.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}

Expand Down
4 changes: 2 additions & 2 deletions nil/internal/execution/zerostate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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)
Expand Down
4 changes: 4 additions & 0 deletions nil/internal/types/address.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion nil/services/nilservice/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 6 additions & 5 deletions nil/tests/basic/basic_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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() {
Expand Down
2 changes: 1 addition & 1 deletion nil/tests/rpc_suite.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
2 changes: 1 addition & 1 deletion nil/tests/sharded_suite.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
6 changes: 6 additions & 0 deletions smart-contracts/contracts/IMessageQueue.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.15;

interface IMessageQueue {
function sendRawTransaction(bytes calldata _message) external;
}
7 changes: 6 additions & 1 deletion smart-contracts/contracts/SmartAccount.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
pragma solidity ^0.8.9;

import "./NilTokenBase.sol";
import "./IMessageQueue.sol";

/**
* @title SmartAccount
Expand Down Expand Up @@ -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);
}

/**
Expand Down
Loading