diff --git a/nil/cmd/nild/devnet.go b/nil/cmd/nild/devnet.go index eba29ea5b..49df979cd 100644 --- a/nil/cmd/nild/devnet.go +++ b/nil/cmd/nild/devnet.go @@ -2,7 +2,6 @@ package main import ( - "crypto/ecdsa" "errors" "fmt" "os" @@ -131,25 +130,13 @@ func (devnet devnet) generateZeroState(nShards uint32, servers []server) (*execu }) } } + mainKeyPath := devnet.spec.NildCredentialsDir + "/keys.yaml" - mainPrivateKey, err := execution.LoadMainKeys(mainKeyPath) - if err != nil && !errors.Is(err, os.ErrNotExist) { + mainPublicKey, err := ensurePublicKey(mainKeyPath) + if err != nil { return nil, err } - var mainPublicKey []byte - if err != nil { - var mainPrivateKey *ecdsa.PrivateKey - mainPrivateKey, mainPublicKey, err = nilcrypto.GenerateKeyPair() - if err != nil { - return nil, err - } - if err := execution.DumpMainKeys(mainKeyPath, mainPrivateKey); err != nil { - return nil, err - } - } else { - mainPublicKey = crypto.CompressPubkey(&mainPrivateKey.PublicKey) - } zeroState, err := execution.CreateDefaultZeroStateConfig(mainPublicKey) if err != nil { return nil, err @@ -159,6 +146,27 @@ func (devnet devnet) generateZeroState(nShards uint32, servers []server) (*execu return zeroState, nil } +func ensurePublicKey(keyPath string) ([]byte, error) { + privateKey, err := execution.LoadMainKeys(keyPath) + if err == nil { + publicKey := crypto.CompressPubkey(&privateKey.PublicKey) + return publicKey, nil + } + if !errors.Is(err, os.ErrNotExist) { + // if the file exists but is invalid, return the error + return nil, err + } + + privateKey, publicKey, err := nilcrypto.GenerateKeyPair() + if err != nil { + return nil, err + } + if err := execution.DumpMainKeys(keyPath, privateKey); err != nil { + return nil, err + } + return publicKey, nil +} + func genDevnet(cmd *cobra.Command, args []string) error { baseDir, err := cmd.Flags().GetString("basedir") if err != nil { diff --git a/nil/contracts/solidity/system/Governance.sol b/nil/contracts/solidity/system/Governance.sol new file mode 100644 index 000000000..50937cb17 --- /dev/null +++ b/nil/contracts/solidity/system/Governance.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.15; + +import "../lib/Nil.sol"; + +contract Governance is NilBase { + address public constant SELF_ADDRESS = + address(0x777777777777777777777777777777777777); + + function rollback( + uint32 version, + uint32 counter, + uint32 patchLevel, + uint64 mainBlockId, + uint32 /*replayDepth*/, + uint32 /*searchDepth*/ + ) external onlyExternal { + Nil.rollback( + version, + counter, + patchLevel, + mainBlockId /*, + replayDepth, + searchDepth */ + ); + } + + bytes pubkey; + + constructor(bytes memory _pubkey) payable { + pubkey = _pubkey; + } + + function verifyExternal( + uint256 hash, + bytes memory authData + ) external view returns (bool) { + return Nil.validateSignature(pubkey, hash, authData); + } +} diff --git a/nil/internal/collate/proposer.go b/nil/internal/collate/proposer.go index 66ea190fe..37cc76d8d 100644 --- a/nil/internal/collate/proposer.go +++ b/nil/internal/collate/proposer.go @@ -26,6 +26,8 @@ const ( defaultMaxGasInBlock = types.DefaultMaxGasInBlock maxTxnsFromPool = 1000 defaultMaxForwardTransactionsInBlock = 200 + + validatorPatchLevel = 1 ) type proposer struct { @@ -77,6 +79,13 @@ func (p *proposer) GenerateProposal(ctx context.Context, txFabric db.DB) (*execu return nil, fmt.Errorf("failed to fetch previous block: %w", err) } + if block.PatchLevel > validatorPatchLevel { + return nil, fmt.Errorf("block patch level %d is higher than supported %d", block.PatchLevel, validatorPatchLevel) + } + + p.proposal.PatchLevel = block.PatchLevel + p.proposal.RollbackCounter = block.RollbackCounter + configAccessor, err := config.NewConfigAccessorFromBlockWithTx(tx, block, p.params.ShardId) if err != nil { return nil, fmt.Errorf("failed to create config accessor: %w", err) @@ -110,6 +119,12 @@ func (p *proposer) GenerateProposal(ctx context.Context, txFabric db.DB) (*execu return nil, fmt.Errorf("failed to handle transactions from pool: %w", err) } + if rollback := p.executionState.GetRollback(); rollback != nil { + // TODO: verify mainBlockId, actually perform rollback + p.proposal.PatchLevel = rollback.PatchLevel + p.proposal.RollbackCounter = rollback.Counter + 1 + } + if len(p.proposal.InternalTxnRefs) == 0 && len(p.proposal.ExternalTxns) == 0 && len(p.proposal.ForwardTxnRefs) == 0 { p.logger.Trace().Msg("No transactions collected") } else { @@ -195,6 +210,24 @@ func (p *proposer) handleL1Attributes(tx db.RoTx) error { return nil } +func CreateRollbackCalldata(params *execution.RollbackParams) ([]byte, error) { + abi, err := contracts.GetAbi(contracts.NameGovernance) + if err != nil { + return nil, fmt.Errorf("failed to get Governance ABI: %w", err) + } + calldata, err := abi.Pack("rollback", + params.Version, + params.Counter, + params.PatchLevel, + params.MainBlockId, + params.ReplayDepth, + params.SearchDepth) + if err != nil { + return nil, fmt.Errorf("failed to pack rollback calldata: %w", err) + } + return calldata, nil +} + func CreateL1BlockUpdateTransaction(header *l1types.Header) (*types.Transaction, error) { abi, err := contracts.GetAbi(contracts.NameL1BlockInfo) if err != nil { diff --git a/nil/internal/config/generate.go b/nil/internal/config/generate.go index ff31ba00b..975fd35c1 100644 --- a/nil/internal/config/generate.go +++ b/nil/internal/config/generate.go @@ -1,3 +1,3 @@ package config -//go:generate go run github.com/NilFoundation/fastssz/sszgen --path params.go -include ../types/address.go,../types/uint256.go,../types/transaction.go,../../common/hash.go,../../common/length.go --objs ListValidators,ParamValidators,ValidatorInfo,ParamGasPrice,ParamFees,ParamL1BlockInfo,WorkaroundToImportTypes +//go:generate go run github.com/NilFoundation/fastssz/sszgen --path params.go -include ../types/address.go,../types/uint256.go,../types/transaction.go,../../common/hash.go,../../common/length.go --objs ListValidators,ParamValidators,ValidatorInfo,ParamGasPrice,ParamFees,ParamL1BlockInfo,ParamSudoKey,WorkaroundToImportTypes diff --git a/nil/internal/contracts/contract.go b/nil/internal/contracts/contract.go index b6e09b4ee..b084c018a 100644 --- a/nil/internal/contracts/contract.go +++ b/nil/internal/contracts/contract.go @@ -20,6 +20,7 @@ const ( NameNilBounceable = "NilBounceable" NameNilConfigAbi = "NilConfigAbi" NameL1BlockInfo = "system/L1BlockInfo" + NameGovernance = "system/Governance" ) var ( diff --git a/nil/internal/crypto/secp256k1.go b/nil/internal/crypto/secp256k1.go index e1157bb2d..613aadfc6 100644 --- a/nil/internal/crypto/secp256k1.go +++ b/nil/internal/crypto/secp256k1.go @@ -1,6 +1,7 @@ package crypto import ( + "github.com/NilFoundation/nil/nil/common" "github.com/holiman/uint256" ) @@ -14,7 +15,7 @@ func TransactionSignatureIsValid(v byte, r, s *uint256.Int) bool { } func TransactionSignatureIsValidBytes(sign []byte) bool { - if len(sign) != 65 { + if len(sign) != common.SignatureSize { return false } diff --git a/nil/internal/execution/block_generator.go b/nil/internal/execution/block_generator.go index 03b11359a..6bb35a647 100644 --- a/nil/internal/execution/block_generator.go +++ b/nil/internal/execution/block_generator.go @@ -222,6 +222,8 @@ func (g *BlockGenerator) prepareExecutionState(proposal *Proposal, gasPrices []t } g.executionState.MainChainHash = proposal.MainChainHash + g.executionState.PatchLevel = proposal.PatchLevel + g.executionState.RollbackCounter = proposal.RollbackCounter for _, txn := range proposal.InternalTxns { if err := g.handleTxn(txn); err != nil { diff --git a/nil/internal/execution/proposal.go b/nil/internal/execution/proposal.go index e0e2c2cfb..b834917eb 100644 --- a/nil/internal/execution/proposal.go +++ b/nil/internal/execution/proposal.go @@ -31,11 +31,13 @@ type InternalTxnReference struct { } type Proposal struct { - PrevBlockId types.BlockNumber `json:"prevBlockId"` - PrevBlockHash common.Hash `json:"prevBlockHash"` - CollatorState types.CollatorState `json:"collatorState"` - MainChainHash common.Hash `json:"mainChainHash"` - ShardHashes []common.Hash `json:"shardHashes"` + PrevBlockId types.BlockNumber `json:"prevBlockId"` + PrevBlockHash common.Hash `json:"prevBlockHash"` + PatchLevel uint32 `json:"patchLevel"` + RollbackCounter uint32 `json:"rollbackCounter"` + CollatorState types.CollatorState `json:"collatorState"` + MainChainHash common.Hash `json:"mainChainHash"` + ShardHashes []common.Hash `json:"shardHashes"` InternalTxns []*types.Transaction `json:"internalTxns"` ExternalTxns []*types.Transaction `json:"externalTxns"` @@ -45,6 +47,10 @@ type Proposal struct { type ProposalSSZ struct { PrevBlockId types.BlockNumber PrevBlockHash common.Hash + + PatchLevel uint32 + RollbackCounter uint32 + CollatorState types.CollatorState MainChainHash common.Hash ShardHashes []common.Hash `ssz-max:"4096"` @@ -154,11 +160,13 @@ func ConvertProposal(proposal *ProposalSSZ) (*Proposal, error) { } return &Proposal{ - PrevBlockId: proposal.PrevBlockId, - PrevBlockHash: proposal.PrevBlockHash, - CollatorState: proposal.CollatorState, - MainChainHash: proposal.MainChainHash, - ShardHashes: proposal.ShardHashes, + PrevBlockId: proposal.PrevBlockId, + PrevBlockHash: proposal.PrevBlockHash, + PatchLevel: proposal.PatchLevel, + RollbackCounter: proposal.RollbackCounter, + CollatorState: proposal.CollatorState, + MainChainHash: proposal.MainChainHash, + ShardHashes: proposal.ShardHashes, // todo: special txns should be validated InternalTxns: append(proposal.SpecialTxns, internalTxns...), diff --git a/nil/internal/execution/state.go b/nil/internal/execution/state.go index 475466654..ca033782f 100644 --- a/nil/internal/execution/state.go +++ b/nil/internal/execution/state.go @@ -46,6 +46,15 @@ func init() { } } +type RollbackParams struct { + Version uint32 + Counter uint32 + PatchLevel uint32 + MainBlockId uint64 + ReplayDepth uint32 + SearchDepth uint32 +} + type ExecutionState struct { tx db.RwTx ContractTree *ContractTrie @@ -59,6 +68,11 @@ type ExecutionState struct { GasPrice types.Value // Current gas price including priority fee BaseFee types.Value + // Those fields are just copied from the proposal into the block + // and are not used in the state + PatchLevel uint32 + RollbackCounter uint32 + InTransactionHash common.Hash Logs map[common.Hash][]*types.Log DebugLogs map[common.Hash][]*types.DebugLog @@ -109,6 +123,9 @@ type ExecutionState struct { isReadOnly bool FeeCalculator FeeCalculator + + // filled in if a rollback was requested by a transaction + rollback *RollbackParams } type ExecutionResult struct { @@ -215,12 +232,14 @@ func NewEVMBlockContext(es *ExecutionState) (*vm.BlockContext, error) { currentBlockId := uint64(0) var header *types.Block time := uint64(0) + rollbackCounter := uint32(0) if err == nil { header = data.Block() currentBlockId = header.Id.Uint64() + 1 // TODO: we need to use header.Timestamp instead of but it's always zero for now. // Let's return some kind of logical timestamp (monotonic increasing block number). time = header.Id.Uint64() + rollbackCounter = header.RollbackCounter } return &vm.BlockContext{ GetHash: getHashFn(es, header), @@ -229,6 +248,8 @@ func NewEVMBlockContext(es *ExecutionState) (*vm.BlockContext, error) { BaseFee: big.NewInt(10), BlobBaseFee: big.NewInt(10), Time: time, + + RollbackCounter: rollbackCounter, }, nil } @@ -1414,6 +1435,8 @@ func (es *ExecutionState) BuildBlock(blockId types.BlockNumber) (*BlockGeneratio BaseFee: es.BaseFee, GasUsed: es.GasUsed, L1BlockNumber: l1BlockNumber, + PatchLevel: es.PatchLevel, + RollbackCounter: es.RollbackCounter, // TODO(@klonD90): remove this field after changing explorer Timestamp: 0, }, @@ -1701,6 +1724,19 @@ func (es *ExecutionState) GetGasPrice(shardId types.ShardId) (types.Value, error return types.Value{Uint256: &prices.Shards[shardId]}, nil } +func (es *ExecutionState) Rollback(counter, patchLevel uint32, mainBlock uint64) error { + es.rollback = &RollbackParams{ + Counter: counter, + PatchLevel: patchLevel, + MainBlockId: mainBlock, + } + return nil +} + +func (es *ExecutionState) GetRollback() *RollbackParams { + return es.rollback +} + func (es *ExecutionState) SetTokenTransfer(tokens []types.TokenBalance) { es.evm.SetTokenTransfer(tokens) } diff --git a/nil/internal/execution/zerostate.go b/nil/internal/execution/zerostate.go index e2861fc52..0a020da30 100644 --- a/nil/internal/execution/zerostate.go +++ b/nil/internal/execution/zerostate.go @@ -62,6 +62,7 @@ func CreateDefaultZeroStateConfig(mainPublicKey []byte) (*ZeroStateConfig, error {Name: "BtcFaucet", Contract: "FaucetToken", Address: types.BtcFaucetAddress, Value: tokenValue}, {Name: "UsdcFaucet", Contract: "FaucetToken", Address: types.UsdcFaucetAddress, Value: tokenValue}, {Name: "L1BlockInfo", Contract: "system/L1BlockInfo", Address: types.L1BlockInfoAddress, Value: types.Value0}, + {Name: "Governance", Contract: "system/Governance", Address: types.GovernanceAddress, Value: smartAccountValue, CtorArgs: []any{hexutil.Encode(mainPublicKey)}}, }, } return zeroStateConfig, nil diff --git a/nil/internal/types/address.go b/nil/internal/types/address.go index a13d5d5fe..7cc0d3abf 100644 --- a/nil/internal/types/address.go +++ b/nil/internal/types/address.go @@ -31,6 +31,7 @@ var ( BtcFaucetAddress = ShardAndHexToAddress(BaseShardId, "111111111111111111111111111111111114") UsdcFaucetAddress = ShardAndHexToAddress(BaseShardId, "111111111111111111111111111111111115") L1BlockInfoAddress = ShardAndHexToAddress(MainShardId, "222222222222222222222222222222222222") + GovernanceAddress = ShardAndHexToAddress(MainShardId, "777777777777777777777777777777777777") ) func GetTokenName(addr TokenId) string { @@ -98,16 +99,6 @@ func ShardAndHexToAddress(shardId ShardId, s string) Address { return addr } -// IsHexAddress verifies whether a string can represent a valid hex-encoded -// Ethereum address or not. -func IsHexAddress(s string) bool { - if len(s) >= 2 && s[0] == '0' && (s[1] == 'x' || s[1] == 'X') { - s = s[2:] - } - _, err := hex.DecodeString(s) - return err == nil -} - // Bytes gets the string representation of the underlying address. func (a Address) Bytes() []byte { return a[:] } diff --git a/nil/internal/types/block.go b/nil/internal/types/block.go index 4a811a050..93509d17a 100644 --- a/nil/internal/types/block.go +++ b/nil/internal/types/block.go @@ -50,6 +50,12 @@ type BlockData struct { BaseFee Value `json:"gasPrice" ch:"gas_price"` GasUsed Gas `json:"gasUsed" ch:"gas_used"` L1BlockNumber uint64 `json:"l1BlockNumber" ch:"l1_block_number"` + + // Incremented after every rollback, used to prevent rollback replay attacks + RollbackCounter uint32 `json:"rollbackCounter" ch:"rollback_counter"` + // Required validator patchLevel, incremented if validator updates + // are required to mitigate an issue + PatchLevel uint32 `json:"patchLevel" ch:"patch_level"` } type ConsensusParams struct { diff --git a/nil/internal/types/exec_errors.go b/nil/internal/types/exec_errors.go index 5bf027972..954f88e37 100644 --- a/nil/internal/types/exec_errors.go +++ b/nil/internal/types/exec_errors.go @@ -148,8 +148,14 @@ const ( // ErrorPrecompileStateDbReturnedError is an internal error indicating that the Execution returned an error ErrorPrecompileStateDbReturnedError // ErrorOnlyMainShardContractsCanChangeConfig is returned when a contract from a shard other than the main one tries - // to change on-cahin config + // to change on-chain config ErrorOnlyMainShardContractsCanChangeConfig + // ErrorPrecompileWrongCaller is returned when the caller of the precompile is not the expected one + ErrorPrecompileWrongCaller + // ErrorPrecompileWrongVersion is returned when the version of the precompile is not the expected one + ErrorPrecompileWrongVersion + // ErrorPrecompileBadArgument is returned when the precompile receives an invalid argument + ErrorPrecompileBadArgument // ErrorPrecompileConfigSetParamFailed is returned when the precompile fails to set the config parameter ErrorPrecompileConfigSetParamFailed // ErrorPrecompileConfigGetParamFailed is returned when the precompile fails to get the config parameter diff --git a/nil/internal/types/ssz_test.go b/nil/internal/types/ssz_test.go index d9373e4b6..f027e6f76 100644 --- a/nil/internal/types/ssz_test.go +++ b/nil/internal/types/ssz_test.go @@ -36,7 +36,7 @@ func TestSszBlock(t *testing.T) { h, err := common.PoseidonSSZ(&block2) require.NoError(t, err) - h2, err := hex.DecodeString("19590a5f03cbb70db36b7cafa77b91e997d3e31fb344572cbad2afd31b90fce6") + h2, err := hex.DecodeString("305b9ab7546ad01a500dd57f29935bc43648d3f428b8b3a8869ab33c543940db") require.NoError(t, err) require.Equal(t, common.BytesToHash(h2), common.BytesToHash(h[:])) diff --git a/nil/internal/vm/evm.go b/nil/internal/vm/evm.go index a11b81bfe..375b4943b 100644 --- a/nil/internal/vm/evm.go +++ b/nil/internal/vm/evm.go @@ -38,6 +38,8 @@ type BlockContext struct { BaseFee *big.Int // Provides information for BASEFEE (0 if vm runs with NoBaseFee flag and 0 gas price) BlobBaseFee *big.Int // Provides information for BLOBBASEFEE (0 if vm runs with NoBaseFee flag and 0 blob gas price) Random *common.Hash // Provides information for PREVRANDAO + + RollbackCounter uint32 // Provides information for rollback handling } // TxContext provides the EVM with information about a transaction. diff --git a/nil/internal/vm/interface.go b/nil/internal/vm/interface.go index 2491878be..da325586a 100644 --- a/nil/internal/vm/interface.go +++ b/nil/internal/vm/interface.go @@ -105,6 +105,8 @@ type StateDB interface { SaveVmState(state *types.EvmState, continuationGasCredit types.Gas) error GetConfigAccessor() config.ConfigAccessor + + Rollback(counter, patchLevel uint32, mainBlock uint64) error } // CallContext provides a basic interface for the EVM calling conventions. The EVM diff --git a/nil/internal/vm/precompiled.go b/nil/internal/vm/precompiled.go index f2c29b39e..609ccf235 100644 --- a/nil/internal/vm/precompiled.go +++ b/nil/internal/vm/precompiled.go @@ -99,6 +99,7 @@ var ( SendRequestAddress = types.BytesToAddress([]byte{0xd8}) CheckIsResponseAddress = types.BytesToAddress([]byte{0xd9}) LogAddress = types.BytesToAddress([]byte{0xda}) + GovernanceAddress = types.BytesToAddress([]byte{0xdb}) ) // PrecompiledContractsPrague contains the set of pre-compiled Ethereum @@ -140,6 +141,7 @@ var PrecompiledContractsPrague = map[types.Address]PrecompiledContract{ SendRequestAddress: &sendRequest{}, CheckIsResponseAddress: &checkIsResponse{}, LogAddress: &emitLog{}, + GovernanceAddress: &governance{}, } // RunPrecompiledContract runs and evaluates the output of a precompiled contract. @@ -183,7 +185,7 @@ type simple struct { contract SimplePrecompiledContract } -var _ PrecompiledContract = (*simple)(nil) +var _ ReadOnlyPrecompiledContract = (*simple)(nil) func (a *simple) RequiredGas(input []byte, state StateDBReadOnly) (uint64, error) { return a.contract.RequiredGas(input), nil @@ -536,15 +538,9 @@ func (a *awaitCall) Run(evm *EVM, input []byte, value *uint256.Int, caller Contr return nil, types.NewVmError(types.ErrorAwaitCallCalledFromNotTopLevel) } - method := getPrecompiledMethod("precompileAwaitCall") - - // Unpack arguments, skipping the first 4 bytes (function selector) - args, err := method.Inputs.Unpack(input[4:]) + args, err := precompiledArgs("precompileAwaitCall", input, 3) if err != nil { - return nil, types.NewVmVerboseError(types.ErrorAbiUnpackFailed, err.Error()) - } - if len(args) != 3 { - return nil, types.NewVmError(types.ErrorPrecompileWrongNumberOfArguments) + return nil, err } // Get `dst` argument @@ -685,7 +681,7 @@ func (a *verifySignature) Run(input []byte) ([]byte, error) { pubkey, ok1 := values[0].([]byte) hash, ok2 := values[1].(*big.Int) sig, ok3 := values[2].([]byte) - if !(ok1 && ok2 && ok3 && len(sig) == 65) { + if !(ok1 && ok2 && ok3 && len(sig) == common.SignatureSize) { return common.EmptyHash[:], nil } result := crypto.VerifySignature(pubkey, common.BigToHash(hash).Bytes(), sig[:64]) @@ -726,6 +722,66 @@ func (a *checkIsInternal) Run(state StateDBReadOnly, input []byte, value *uint25 return res, nil } +func precompiledArgs(method string, input []byte, argCount int) ([]any, error) { + if len(input) < 4 { + return nil, types.NewVmError(types.ErrorPrecompileTooShortCallData) + } + + // Unpack arguments, skipping the first 4 bytes (function selector) + args, err := getPrecompiledMethod(method).Inputs.Unpack(input[4:]) + if err != nil { + return nil, err + } + if len(args) != argCount { + return nil, types.NewVmError(types.ErrorPrecompileWrongNumberOfArguments) + } + return args, nil +} + +type governance struct{} + +var _ EvmAccessedPrecompiledContract = (*governance)(nil) + +func (g *governance) RequiredGas([]byte, StateDBReadOnly) (uint64, error) { + return 10, nil +} + +func (g *governance) Run(evm *EVM, input []byte, value *uint256.Int, caller ContractRef) ([]byte, error) { + if caller.Address() != types.GovernanceAddress { + return nil, types.NewVmError(types.ErrorPrecompileWrongCaller) + } + + args, err := precompiledArgs("precompileRollback", input, 4) + if err != nil { + return nil, err + } + + version, ok := args[0].(uint32) + if !ok || version != 1 { + return nil, types.NewVmError(types.ErrorPrecompileWrongVersion) + } + + counter, ok1 := args[1].(uint32) + patchLevel, ok2 := args[2].(uint32) + mainBlockId, ok3 := args[3].(uint64) + if !ok1 || !ok2 || !ok3 { + return nil, types.NewVmError(types.ErrorAbiUnpackFailed) + } + + if evm.Context.RollbackCounter != counter { + return nil, types.NewVmError(types.ErrorPrecompileBadArgument) + } + + err = evm.StateDB.Rollback(counter, patchLevel, mainBlockId) + + res := make([]byte, 32) + if err == nil { + res[31] = 1 + } + + return res, err +} + type checkIsResponse struct{} var _ ReadOnlyPrecompiledContract = (*checkIsResponse)(nil) diff --git a/nil/services/cliservice/block_format_test.go b/nil/services/cliservice/block_format_test.go index d34e34da1..16db89631 100644 --- a/nil/services/cliservice/block_format_test.go +++ b/nil/services/cliservice/block_format_test.go @@ -147,7 +147,7 @@ func TestDebugBlockToText(t *testing.T) { text, err := s.debugBlockToText(types.ShardId(13), block, false, false) require.NoError(t, err) - expectedText := `Block #100500 [0x000d9bb830d574ddf2973a367f2fe9d899c7c523afb809a1a7d2480ab9bcd4cf] @ 13 shard + expectedText := `Block #100500 [0x000dad995dc84952f5631bfba6237a9a77520053aa6f282a6471dd1ea1af6a89] @ 13 shard PrevBlock: 0x00000000000000000000000000000000000000000000000000000000deadbeef ChildBlocksRootHash: 0x00000000000000000000000000000000000000000000000000000000deadbabe ChildBlocks: diff --git a/nil/services/rpc/jsonrpc/types.go b/nil/services/rpc/jsonrpc/types.go index cb52b8232..9f26cdb50 100644 --- a/nil/services/rpc/jsonrpc/types.go +++ b/nil/services/rpc/jsonrpc/types.go @@ -73,6 +73,8 @@ type RPCBlock struct { Number types.BlockNumber `json:"number"` Hash common.Hash `json:"hash"` ParentHash common.Hash `json:"parentHash"` + PatchLevel uint32 `json:"patchLevel"` + RollbackCounter uint32 `json:"rollbackCounter"` InTransactionsRoot common.Hash `json:"inTransactionsRoot"` ReceiptsRoot common.Hash `json:"receiptsRoot"` ChildBlocksRootHash common.Hash `json:"childBlocksRootHash"` @@ -379,6 +381,8 @@ func NewRPCBlock(shardId types.ShardId, data *BlockWithEntities, fullTx bool) (* Number: blockId, Hash: blockHash, ParentHash: block.PrevBlock, + PatchLevel: block.PatchLevel, + RollbackCounter: block.RollbackCounter, InTransactionsRoot: block.InTransactionsRoot, ReceiptsRoot: block.ReceiptsRoot, ChildBlocksRootHash: block.ChildBlocksRootHash, diff --git a/nil/services/synccommittee/prover/tracer/state_db.go b/nil/services/synccommittee/prover/tracer/state_db.go index 701fe6cf8..15df1f359 100644 --- a/nil/services/synccommittee/prover/tracer/state_db.go +++ b/nil/services/synccommittee/prover/tracer/state_db.go @@ -667,6 +667,10 @@ func (tsdb *TracerStateDB) Selfdestruct6780(types.Address) error { return errors.New("not implemented") } +func (tsdb *TracerStateDB) Rollback(_, _ uint32, _ uint64) error { + return errors.New("not implemented") +} + // Exist reports whether the given account exists in state. // Notably this should also return true for self-destructed accounts. func (tsdb *TracerStateDB) Exists(address types.Address) (bool, error) { diff --git a/nil/tests/governance/rollback_test.go b/nil/tests/governance/rollback_test.go new file mode 100644 index 000000000..2f3f6e5c6 --- /dev/null +++ b/nil/tests/governance/rollback_test.go @@ -0,0 +1,62 @@ +package governance + +import ( + "testing" + + "github.com/NilFoundation/nil/nil/internal/collate" + "github.com/NilFoundation/nil/nil/internal/execution" + "github.com/NilFoundation/nil/nil/internal/types" + "github.com/NilFoundation/nil/nil/services/nilservice" + "github.com/NilFoundation/nil/nil/services/rpc" + "github.com/NilFoundation/nil/nil/services/rpc/transport" + "github.com/NilFoundation/nil/nil/tests" + "github.com/stretchr/testify/suite" +) + +const numShards = 4 + +type RollbackSuite struct { + tests.RpcSuite +} + +func (s *RollbackSuite) SetupTest() { + s.Start(&nilservice.Config{ + NShards: numShards, + HttpUrl: rpc.GetSockPath(s.T()), + CollatorTickPeriodMs: 300, + RunMode: nilservice.CollatorsOnlyRunMode, + }) +} + +func (s *RollbackSuite) TearDownTest() { + s.Cancel() +} + +func (s *RollbackSuite) TestSendRollbackTx() { + params := &execution.RollbackParams{ + Version: 1, + Counter: 0, + PatchLevel: 1, + MainBlockId: 2, + ReplayDepth: 3, + SearchDepth: 4, + } + + calldata, err := collate.CreateRollbackCalldata(params) + s.Require().NoError(err) + + receipt := s.SendExternalTransaction(calldata, types.GovernanceAddress) + s.Require().NotNil(receipt) + s.True(receipt.Success) + + // Check that the patchLevel has been updated + block, err := s.Client.GetBlock(s.Context, types.MainShardId, transport.BlockNumber(receipt.BlockNumber), false) + s.Require().NoError(err) + s.Equal(uint32(1), block.PatchLevel) +} + +func TestRollback(t *testing.T) { + t.Parallel() + + suite.Run(t, &RollbackSuite{}) +} diff --git a/nil/tests/rpc_suite.go b/nil/tests/rpc_suite.go index 67c928ddf..c1868ec1e 100644 --- a/nil/tests/rpc_suite.go +++ b/nil/tests/rpc_suite.go @@ -227,13 +227,6 @@ func (s *RpcSuite) SendTransactionViaSmartAccountNoCheck(addrSmartAccount types. return receipt } -func (s *RpcSuite) SendRawTransaction(data []byte) *jsonrpc.RPCReceipt { - txHash, err := s.Client.SendRawTransaction(s.Context, data) - s.Require().NoError(err) - receipt := s.WaitIncludedInMain(txHash) - return receipt -} - func (s *RpcSuite) CallGetter(addr types.Address, calldata []byte, blockId any, overrides *jsonrpc.StateOverrides) []byte { s.T().Helper() return CallGetter(s.T(), s.Context, s.Client, addr, calldata, blockId, overrides) diff --git a/smart-contracts/contracts/Nil.sol b/smart-contracts/contracts/Nil.sol index 321efd004..86e4f3d6f 100644 --- a/smart-contracts/contracts/Nil.sol +++ b/smart-contracts/contracts/Nil.sol @@ -29,6 +29,7 @@ library Nil { address private constant SEND_REQUEST = address(0xd8); address public constant IS_RESPONSE_TRANSACTION = address(0xd9); address public constant LOG = address(0xda); + address public constant GOVERNANCE = address(0xdb); // The following constants specify from where and how the gas should be taken during async call. // Forwarding values are calculated in the following order: FORWARD_VALUE, FORWARD_PERCENTAGE, FORWARD_REMAINING. @@ -373,6 +374,22 @@ library Nil { return __Precompile__(GET_POSEIDON_HASH).precompileGetPoseidonHash(data); } + /** + * @dev Initiates a rollback + * + * Unused parameters are temporarily commented out. + */ + function rollback( + uint32 version, + uint32 counter, + uint32 patchLevel, + uint64 mainBlockId /*, + uint32 replayDepth, + uint32 searchDepth */ + ) internal { + __Precompile__(GOVERNANCE).precompileRollback(version, counter, patchLevel, mainBlockId /*, replayDepth, searchDepth */); + } + /** * @dev Sets a configuration parameter. * @param name Name of the parameter. @@ -508,6 +525,7 @@ contract __Precompile__ { function precompileGetPoseidonHash(bytes memory data) public returns(uint256) {} function precompileConfigParam(bool isSet, string calldata name, bytes calldata data) public returns(bytes memory) {} function precompileLog(string memory transaction, int[] memory data) public returns(bool) {} + function precompileRollback(uint32, uint32, uint32, uint64 /*, uint32, uint32*/) public returns(bool) {} } contract NilConfigAbi { diff --git a/smart-contracts/contracts/SmartAccount.sol b/smart-contracts/contracts/SmartAccount.sol index b664f0451..7e9d0c56c 100644 --- a/smart-contracts/contracts/SmartAccount.sol +++ b/smart-contracts/contracts/SmartAccount.sol @@ -6,9 +6,11 @@ import "./NilTokenBase.sol"; /** * @title SmartAccount - * @dev Basic Smart Account contract which provides functional for calling another contracts and sending tokens. - * It also supports multi-token functionality providing methods for minting and sending token. - * NilTokenBase class implements functional for managing own token(where `tokenId = address(this)`). + * @dev Basic Smart Account contract that provides functionality for interacting + * with other contracts and sending tokens. It also supports multi-token + * functionality, including methods for minting and sending tokens. + * The NilTokenBase class implements functionality for managing the contract's own + * token (where `tokenId = address(this)`). */ contract SmartAccount is NilTokenBase { bytes pubkey;