-
Notifications
You must be signed in to change notification settings - Fork 43
[PoC] Message Queue contract #789
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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; | ||
| } | ||
|
|
||
| contract MessageQueue is IMessageQueue{ | ||
| function sendRawTransaction(bytes calldata _message) external override { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What are thoughts around supporting attaching
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. 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; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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:
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Right now there is no any limits but we can add them
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. |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
|
@@ -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) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -260,6 +260,19 @@ func (g *BlockGenerator) prepareExecutionState(proposal *Proposal, gasPrices []t | |
| g.executionState.AppendForwardTransaction(txn) | ||
| } | ||
|
|
||
| messages, err := GetMessageQueueContent(g.executionState) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
|
||
| 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 | ||
| } |
| 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; | ||
| } |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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.