forked from cerc-io/plugeth
cmd/evm: add difficulty calculation to t8n tool (#23353)
This PR adds functionality to the evm t8n to calculate ethash difficulty. If the caller does not provide a currentDifficulty, but instead provides the parentTimestamp (well, semi-optional, will default to 0 if not given), and parentDifficulty, we can calculate it for him. The caller can also provide a parentUncleHash. In most, but not all cases, the parent uncle hash also affects the formula. If no such hash is provided (or, if the empty all-zero hash is provided), it's assumed that there were no uncles.
This commit is contained in:
parent
efee85378e
commit
84c51bc5ec
@ -23,6 +23,7 @@ import (
|
|||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
"github.com/ethereum/go-ethereum/common"
|
||||||
"github.com/ethereum/go-ethereum/common/math"
|
"github.com/ethereum/go-ethereum/common/math"
|
||||||
|
"github.com/ethereum/go-ethereum/consensus/ethash"
|
||||||
"github.com/ethereum/go-ethereum/consensus/misc"
|
"github.com/ethereum/go-ethereum/consensus/misc"
|
||||||
"github.com/ethereum/go-ethereum/core"
|
"github.com/ethereum/go-ethereum/core"
|
||||||
"github.com/ethereum/go-ethereum/core/rawdb"
|
"github.com/ethereum/go-ethereum/core/rawdb"
|
||||||
@ -53,6 +54,7 @@ type ExecutionResult struct {
|
|||||||
Bloom types.Bloom `json:"logsBloom" gencodec:"required"`
|
Bloom types.Bloom `json:"logsBloom" gencodec:"required"`
|
||||||
Receipts types.Receipts `json:"receipts"`
|
Receipts types.Receipts `json:"receipts"`
|
||||||
Rejected []*rejectedTx `json:"rejected,omitempty"`
|
Rejected []*rejectedTx `json:"rejected,omitempty"`
|
||||||
|
Difficulty *math.HexOrDecimal256 `json:"currentDifficulty" gencodec:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ommer struct {
|
type ommer struct {
|
||||||
@ -63,21 +65,26 @@ type ommer struct {
|
|||||||
//go:generate gencodec -type stEnv -field-override stEnvMarshaling -out gen_stenv.go
|
//go:generate gencodec -type stEnv -field-override stEnvMarshaling -out gen_stenv.go
|
||||||
type stEnv struct {
|
type stEnv struct {
|
||||||
Coinbase common.Address `json:"currentCoinbase" gencodec:"required"`
|
Coinbase common.Address `json:"currentCoinbase" gencodec:"required"`
|
||||||
Difficulty *big.Int `json:"currentDifficulty" gencodec:"required"`
|
Difficulty *big.Int `json:"currentDifficulty"`
|
||||||
|
ParentDifficulty *big.Int `json:"parentDifficulty"`
|
||||||
GasLimit uint64 `json:"currentGasLimit" gencodec:"required"`
|
GasLimit uint64 `json:"currentGasLimit" gencodec:"required"`
|
||||||
Number uint64 `json:"currentNumber" gencodec:"required"`
|
Number uint64 `json:"currentNumber" gencodec:"required"`
|
||||||
Timestamp uint64 `json:"currentTimestamp" gencodec:"required"`
|
Timestamp uint64 `json:"currentTimestamp" gencodec:"required"`
|
||||||
|
ParentTimestamp uint64 `json:"parentTimestamp,omitempty"`
|
||||||
BlockHashes map[math.HexOrDecimal64]common.Hash `json:"blockHashes,omitempty"`
|
BlockHashes map[math.HexOrDecimal64]common.Hash `json:"blockHashes,omitempty"`
|
||||||
Ommers []ommer `json:"ommers,omitempty"`
|
Ommers []ommer `json:"ommers,omitempty"`
|
||||||
BaseFee *big.Int `json:"currentBaseFee,omitempty"`
|
BaseFee *big.Int `json:"currentBaseFee,omitempty"`
|
||||||
|
ParentUncleHash common.Hash `json:"parentUncleHash"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type stEnvMarshaling struct {
|
type stEnvMarshaling struct {
|
||||||
Coinbase common.UnprefixedAddress
|
Coinbase common.UnprefixedAddress
|
||||||
Difficulty *math.HexOrDecimal256
|
Difficulty *math.HexOrDecimal256
|
||||||
|
ParentDifficulty *math.HexOrDecimal256
|
||||||
GasLimit math.HexOrDecimal64
|
GasLimit math.HexOrDecimal64
|
||||||
Number math.HexOrDecimal64
|
Number math.HexOrDecimal64
|
||||||
Timestamp math.HexOrDecimal64
|
Timestamp math.HexOrDecimal64
|
||||||
|
ParentTimestamp math.HexOrDecimal64
|
||||||
BaseFee *math.HexOrDecimal256
|
BaseFee *math.HexOrDecimal256
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -247,6 +254,7 @@ func (pre *Prestate) Apply(vmConfig vm.Config, chainConfig *params.ChainConfig,
|
|||||||
LogsHash: rlpHash(statedb.Logs()),
|
LogsHash: rlpHash(statedb.Logs()),
|
||||||
Receipts: receipts,
|
Receipts: receipts,
|
||||||
Rejected: rejectedTxs,
|
Rejected: rejectedTxs,
|
||||||
|
Difficulty: (*math.HexOrDecimal256)(vmContext.Difficulty),
|
||||||
}
|
}
|
||||||
return statedb, execRs, nil
|
return statedb, execRs, nil
|
||||||
}
|
}
|
||||||
@ -274,3 +282,23 @@ func rlpHash(x interface{}) (h common.Hash) {
|
|||||||
hw.Sum(h[:0])
|
hw.Sum(h[:0])
|
||||||
return h
|
return h
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// calcDifficulty is based on ethash.CalcDifficulty. This method is used in case
|
||||||
|
// the caller does not provide an explicit difficulty, but instead provides only
|
||||||
|
// parent timestamp + difficulty.
|
||||||
|
// Note: this method only works for ethash engine.
|
||||||
|
func calcDifficulty(config *params.ChainConfig, number, currentTime, parentTime uint64,
|
||||||
|
parentDifficulty *big.Int, parentUncleHash common.Hash) *big.Int {
|
||||||
|
uncleHash := parentUncleHash
|
||||||
|
if uncleHash == (common.Hash{}) {
|
||||||
|
uncleHash = types.EmptyUncleHash
|
||||||
|
}
|
||||||
|
parent := &types.Header{
|
||||||
|
ParentHash: common.Hash{},
|
||||||
|
UncleHash: uncleHash,
|
||||||
|
Difficulty: parentDifficulty,
|
||||||
|
Number: new(big.Int).SetUint64(number - 1),
|
||||||
|
Time: parentTime,
|
||||||
|
}
|
||||||
|
return ethash.CalcDifficulty(config, currentTime, parent)
|
||||||
|
}
|
||||||
|
@ -17,23 +17,29 @@ var _ = (*stEnvMarshaling)(nil)
|
|||||||
func (s stEnv) MarshalJSON() ([]byte, error) {
|
func (s stEnv) MarshalJSON() ([]byte, error) {
|
||||||
type stEnv struct {
|
type stEnv struct {
|
||||||
Coinbase common.UnprefixedAddress `json:"currentCoinbase" gencodec:"required"`
|
Coinbase common.UnprefixedAddress `json:"currentCoinbase" gencodec:"required"`
|
||||||
Difficulty *math.HexOrDecimal256 `json:"currentDifficulty" gencodec:"required"`
|
Difficulty *math.HexOrDecimal256 `json:"currentDifficulty"`
|
||||||
|
ParentDifficulty *math.HexOrDecimal256 `json:"parentDifficulty"`
|
||||||
GasLimit math.HexOrDecimal64 `json:"currentGasLimit" gencodec:"required"`
|
GasLimit math.HexOrDecimal64 `json:"currentGasLimit" gencodec:"required"`
|
||||||
Number math.HexOrDecimal64 `json:"currentNumber" gencodec:"required"`
|
Number math.HexOrDecimal64 `json:"currentNumber" gencodec:"required"`
|
||||||
Timestamp math.HexOrDecimal64 `json:"currentTimestamp" gencodec:"required"`
|
Timestamp math.HexOrDecimal64 `json:"currentTimestamp" gencodec:"required"`
|
||||||
|
ParentTimestamp math.HexOrDecimal64 `json:"parentTimestamp,omitempty"`
|
||||||
BlockHashes map[math.HexOrDecimal64]common.Hash `json:"blockHashes,omitempty"`
|
BlockHashes map[math.HexOrDecimal64]common.Hash `json:"blockHashes,omitempty"`
|
||||||
Ommers []ommer `json:"ommers,omitempty"`
|
Ommers []ommer `json:"ommers,omitempty"`
|
||||||
BaseFee *math.HexOrDecimal256 `json:"currentBaseFee,omitempty"`
|
BaseFee *math.HexOrDecimal256 `json:"currentBaseFee,omitempty"`
|
||||||
|
ParentUncleHash common.Hash `json:"parentUncleHash"`
|
||||||
}
|
}
|
||||||
var enc stEnv
|
var enc stEnv
|
||||||
enc.Coinbase = common.UnprefixedAddress(s.Coinbase)
|
enc.Coinbase = common.UnprefixedAddress(s.Coinbase)
|
||||||
enc.Difficulty = (*math.HexOrDecimal256)(s.Difficulty)
|
enc.Difficulty = (*math.HexOrDecimal256)(s.Difficulty)
|
||||||
|
enc.ParentDifficulty = (*math.HexOrDecimal256)(s.ParentDifficulty)
|
||||||
enc.GasLimit = math.HexOrDecimal64(s.GasLimit)
|
enc.GasLimit = math.HexOrDecimal64(s.GasLimit)
|
||||||
enc.Number = math.HexOrDecimal64(s.Number)
|
enc.Number = math.HexOrDecimal64(s.Number)
|
||||||
enc.Timestamp = math.HexOrDecimal64(s.Timestamp)
|
enc.Timestamp = math.HexOrDecimal64(s.Timestamp)
|
||||||
|
enc.ParentTimestamp = math.HexOrDecimal64(s.ParentTimestamp)
|
||||||
enc.BlockHashes = s.BlockHashes
|
enc.BlockHashes = s.BlockHashes
|
||||||
enc.Ommers = s.Ommers
|
enc.Ommers = s.Ommers
|
||||||
enc.BaseFee = (*math.HexOrDecimal256)(s.BaseFee)
|
enc.BaseFee = (*math.HexOrDecimal256)(s.BaseFee)
|
||||||
|
enc.ParentUncleHash = s.ParentUncleHash
|
||||||
return json.Marshal(&enc)
|
return json.Marshal(&enc)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -41,13 +47,16 @@ func (s stEnv) MarshalJSON() ([]byte, error) {
|
|||||||
func (s *stEnv) UnmarshalJSON(input []byte) error {
|
func (s *stEnv) UnmarshalJSON(input []byte) error {
|
||||||
type stEnv struct {
|
type stEnv struct {
|
||||||
Coinbase *common.UnprefixedAddress `json:"currentCoinbase" gencodec:"required"`
|
Coinbase *common.UnprefixedAddress `json:"currentCoinbase" gencodec:"required"`
|
||||||
Difficulty *math.HexOrDecimal256 `json:"currentDifficulty" gencodec:"required"`
|
Difficulty *math.HexOrDecimal256 `json:"currentDifficulty"`
|
||||||
|
ParentDifficulty *math.HexOrDecimal256 `json:"parentDifficulty"`
|
||||||
GasLimit *math.HexOrDecimal64 `json:"currentGasLimit" gencodec:"required"`
|
GasLimit *math.HexOrDecimal64 `json:"currentGasLimit" gencodec:"required"`
|
||||||
Number *math.HexOrDecimal64 `json:"currentNumber" gencodec:"required"`
|
Number *math.HexOrDecimal64 `json:"currentNumber" gencodec:"required"`
|
||||||
Timestamp *math.HexOrDecimal64 `json:"currentTimestamp" gencodec:"required"`
|
Timestamp *math.HexOrDecimal64 `json:"currentTimestamp" gencodec:"required"`
|
||||||
|
ParentTimestamp *math.HexOrDecimal64 `json:"parentTimestamp,omitempty"`
|
||||||
BlockHashes map[math.HexOrDecimal64]common.Hash `json:"blockHashes,omitempty"`
|
BlockHashes map[math.HexOrDecimal64]common.Hash `json:"blockHashes,omitempty"`
|
||||||
Ommers []ommer `json:"ommers,omitempty"`
|
Ommers []ommer `json:"ommers,omitempty"`
|
||||||
BaseFee *math.HexOrDecimal256 `json:"currentBaseFee,omitempty"`
|
BaseFee *math.HexOrDecimal256 `json:"currentBaseFee,omitempty"`
|
||||||
|
ParentUncleHash *common.Hash `json:"parentUncleHash"`
|
||||||
}
|
}
|
||||||
var dec stEnv
|
var dec stEnv
|
||||||
if err := json.Unmarshal(input, &dec); err != nil {
|
if err := json.Unmarshal(input, &dec); err != nil {
|
||||||
@ -57,10 +66,12 @@ func (s *stEnv) UnmarshalJSON(input []byte) error {
|
|||||||
return errors.New("missing required field 'currentCoinbase' for stEnv")
|
return errors.New("missing required field 'currentCoinbase' for stEnv")
|
||||||
}
|
}
|
||||||
s.Coinbase = common.Address(*dec.Coinbase)
|
s.Coinbase = common.Address(*dec.Coinbase)
|
||||||
if dec.Difficulty == nil {
|
if dec.Difficulty != nil {
|
||||||
return errors.New("missing required field 'currentDifficulty' for stEnv")
|
|
||||||
}
|
|
||||||
s.Difficulty = (*big.Int)(dec.Difficulty)
|
s.Difficulty = (*big.Int)(dec.Difficulty)
|
||||||
|
}
|
||||||
|
if dec.ParentDifficulty != nil {
|
||||||
|
s.ParentDifficulty = (*big.Int)(dec.ParentDifficulty)
|
||||||
|
}
|
||||||
if dec.GasLimit == nil {
|
if dec.GasLimit == nil {
|
||||||
return errors.New("missing required field 'currentGasLimit' for stEnv")
|
return errors.New("missing required field 'currentGasLimit' for stEnv")
|
||||||
}
|
}
|
||||||
@ -73,6 +84,9 @@ func (s *stEnv) UnmarshalJSON(input []byte) error {
|
|||||||
return errors.New("missing required field 'currentTimestamp' for stEnv")
|
return errors.New("missing required field 'currentTimestamp' for stEnv")
|
||||||
}
|
}
|
||||||
s.Timestamp = uint64(*dec.Timestamp)
|
s.Timestamp = uint64(*dec.Timestamp)
|
||||||
|
if dec.ParentTimestamp != nil {
|
||||||
|
s.ParentTimestamp = uint64(*dec.ParentTimestamp)
|
||||||
|
}
|
||||||
if dec.BlockHashes != nil {
|
if dec.BlockHashes != nil {
|
||||||
s.BlockHashes = dec.BlockHashes
|
s.BlockHashes = dec.BlockHashes
|
||||||
}
|
}
|
||||||
@ -82,5 +96,8 @@ func (s *stEnv) UnmarshalJSON(input []byte) error {
|
|||||||
if dec.BaseFee != nil {
|
if dec.BaseFee != nil {
|
||||||
s.BaseFee = (*big.Int)(dec.BaseFee)
|
s.BaseFee = (*big.Int)(dec.BaseFee)
|
||||||
}
|
}
|
||||||
|
if dec.ParentUncleHash != nil {
|
||||||
|
s.ParentUncleHash = *dec.ParentUncleHash
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -252,6 +252,20 @@ func Main(ctx *cli.Context) error {
|
|||||||
return NewError(ErrorVMConfig, errors.New("EIP-1559 config but missing 'currentBaseFee' in env section"))
|
return NewError(ErrorVMConfig, errors.New("EIP-1559 config but missing 'currentBaseFee' in env section"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if env := prestate.Env; env.Difficulty == nil {
|
||||||
|
// If difficulty was not provided by caller, we need to calculate it.
|
||||||
|
switch {
|
||||||
|
case env.ParentDifficulty == nil:
|
||||||
|
return NewError(ErrorVMConfig, errors.New("currentDifficulty was not provided, and cannot be calculated due to missing parentDifficulty"))
|
||||||
|
case env.Number == 0:
|
||||||
|
return NewError(ErrorVMConfig, errors.New("currentDifficulty needs to be provided for block number 0"))
|
||||||
|
case env.Timestamp <= env.ParentTimestamp:
|
||||||
|
return NewError(ErrorVMConfig, fmt.Errorf("currentDifficulty cannot be calculated -- currentTime (%d) needs to be after parent time (%d)",
|
||||||
|
env.Timestamp, env.ParentTimestamp))
|
||||||
|
}
|
||||||
|
prestate.Env.Difficulty = calcDifficulty(chainConfig, env.Number, env.Timestamp,
|
||||||
|
env.ParentTimestamp, env.ParentDifficulty, env.ParentUncleHash)
|
||||||
|
}
|
||||||
// Run the test and aggregate the result
|
// Run the test and aggregate the result
|
||||||
s, result, err := prestate.Apply(vmConfig, chainConfig, txs, ctx.Int64(RewardFlag.Name), getTracer)
|
s, result, err := prestate.Apply(vmConfig, chainConfig, txs, ctx.Int64(RewardFlag.Name), getTracer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
12
cmd/evm/testdata/14/alloc.json
vendored
Normal file
12
cmd/evm/testdata/14/alloc.json
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"a94f5374fce5edbc8e2a8697c15331677e6ebf0b": {
|
||||||
|
"balance": "0x5ffd4878be161d74",
|
||||||
|
"code": "0x",
|
||||||
|
"nonce": "0xac",
|
||||||
|
"storage": {}
|
||||||
|
},
|
||||||
|
"0x8a8eafb1cf62bfbeb1741769dae1a9dd47996192":{
|
||||||
|
"balance": "0xfeedbead",
|
||||||
|
"nonce" : "0x00"
|
||||||
|
}
|
||||||
|
}
|
9
cmd/evm/testdata/14/env.json
vendored
Normal file
9
cmd/evm/testdata/14/env.json
vendored
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"currentCoinbase": "0xc94f5374fce5edbc8e2a8697c15331677e6ebf0b",
|
||||||
|
"currentGasLimit": "0x750a163df65e8a",
|
||||||
|
"currentBaseFee": "0x500",
|
||||||
|
"currentNumber": "12800000",
|
||||||
|
"currentTimestamp": "10015",
|
||||||
|
"parentTimestamp" : "99999",
|
||||||
|
"parentDifficulty" : "0x2000000000000"
|
||||||
|
}
|
10
cmd/evm/testdata/14/env.uncles.json
vendored
Normal file
10
cmd/evm/testdata/14/env.uncles.json
vendored
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"currentCoinbase": "0xc94f5374fce5edbc8e2a8697c15331677e6ebf0b",
|
||||||
|
"currentGasLimit": "0x750a163df65e8a",
|
||||||
|
"currentBaseFee": "0x500",
|
||||||
|
"currentNumber": "12800000",
|
||||||
|
"currentTimestamp": "10035",
|
||||||
|
"parentTimestamp" : "99999",
|
||||||
|
"parentDifficulty" : "0x2000000000000",
|
||||||
|
"parentUncleHash" : "0x000000000000000000000000000000000000000000000000000000000000beef"
|
||||||
|
}
|
41
cmd/evm/testdata/14/readme.md
vendored
Normal file
41
cmd/evm/testdata/14/readme.md
vendored
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
## Difficulty calculation
|
||||||
|
|
||||||
|
This test shows how the `evm t8n` can be used to calculate the (ethash) difficulty, if none is provided by the caller.
|
||||||
|
|
||||||
|
Calculating it (with an empty set of txs) using `London` rules (and no provided unclehash for the parent block):
|
||||||
|
```
|
||||||
|
[user@work evm]$ ./evm t8n --input.alloc=./testdata/14/alloc.json --input.txs=./testdata/14/txs.json --input.env=./testdata/14/env.json --output.result=stdout --state.fork=London
|
||||||
|
INFO [08-08|17:35:46.876] Trie dumping started root=6f0588..7f4bdc
|
||||||
|
INFO [08-08|17:35:46.876] Trie dumping complete accounts=2 elapsed="89.313µs"
|
||||||
|
INFO [08-08|17:35:46.877] Wrote file file=alloc.json
|
||||||
|
{
|
||||||
|
"result": {
|
||||||
|
"stateRoot": "0x6f058887ca01549716789c380ede95aecc510e6d1fdc4dbf67d053c7c07f4bdc",
|
||||||
|
"txRoot": "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
|
||||||
|
"receiptRoot": "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
|
||||||
|
"logsHash": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
|
||||||
|
"logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
|
||||||
|
"receipts": [],
|
||||||
|
"currentDifficulty": 3311729559732224
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Same thing, but this time providing a non-empty (and non-`emptyKeccak`) unclehash, which leads to a slightly different result:
|
||||||
|
```
|
||||||
|
[user@work evm]$ ./evm t8n --input.alloc=./testdata/14/alloc.json --input.txs=./testdata/14/txs.json --input.env=./testdata/14/env.uncles.json --output.result=stdout --state.fork=London
|
||||||
|
INFO [08-08|17:35:49.232] Trie dumping started root=6f0588..7f4bdc
|
||||||
|
INFO [08-08|17:35:49.232] Trie dumping complete accounts=2 elapsed="83.069µs"
|
||||||
|
INFO [08-08|17:35:49.233] Wrote file file=alloc.json
|
||||||
|
{
|
||||||
|
"result": {
|
||||||
|
"stateRoot": "0x6f058887ca01549716789c380ede95aecc510e6d1fdc4dbf67d053c7c07f4bdc",
|
||||||
|
"txRoot": "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
|
||||||
|
"receiptRoot": "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
|
||||||
|
"logsHash": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
|
||||||
|
"logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
|
||||||
|
"receipts": [],
|
||||||
|
"currentDifficulty": 3311179803918336
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
1
cmd/evm/testdata/14/txs.json
vendored
Normal file
1
cmd/evm/testdata/14/txs.json
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
[]
|
Loading…
Reference in New Issue
Block a user