2022-01-31 12:22:35 +00:00
|
|
|
// Copyright 2022 The go-ethereum Authors
|
2021-04-16 19:29:22 +00:00
|
|
|
// This file is part of the go-ethereum library.
|
|
|
|
//
|
|
|
|
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
|
|
// it under the terms of the GNU Lesser General Public License as published by
|
|
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
|
|
// (at your option) any later version.
|
|
|
|
//
|
|
|
|
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
// GNU Lesser General Public License for more details.
|
|
|
|
//
|
|
|
|
// You should have received a copy of the GNU Lesser General Public License
|
|
|
|
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
|
2023-02-06 15:37:58 +00:00
|
|
|
package engine
|
2021-04-16 19:29:22 +00:00
|
|
|
|
|
|
|
import (
|
2022-01-20 11:29:06 +00:00
|
|
|
"fmt"
|
all: core rework for the merge transition (#23761)
* all: work for eth1/2 transtition
* consensus/beacon, eth: change beacon difficulty to 0
* eth: updates
* all: add terminalBlockDifficulty config, fix rebasing issues
* eth: implemented merge interop spec
* internal/ethapi: update to v1.0.0.alpha.2
This commit updates the code to the new spec, moving payloadId into
it's own object. It also fixes an issue with finalizing an empty blockhash.
It also properly sets the basefee
* all: sync polishes, other fixes + refactors
* core, eth: correct semantics for LeavePoW, EnterPoS
* core: fixed rebasing artifacts
* core: light: performance improvements
* core: use keyed field (f)
* core: eth: fix compilation issues + tests
* eth/catalyst: dbetter error codes
* all: move Merger to consensus/, remove reliance on it in bc
* all: renamed EnterPoS and LeavePoW to ReachTDD and FinalizePoS
* core: make mergelogs a function
* core: use InsertChain instead of InsertBlock
* les: drop merger from lightchain object
* consensus: add merger
* core: recoverAncestors in catalyst mode
* core: fix nitpick
* all: removed merger from beacon, use TTD, nitpicks
* consensus: eth: add docstring, removed unnecessary code duplication
* consensus/beacon: better comment
* all: easy to fix nitpicks by karalabe
* consensus/beacon: verify known headers to be sure
* core: comments
* core: eth: don't drop peers who advertise blocks, nitpicks
* core: never add beacon blocks to the future queue
* core: fixed nitpicks
* consensus/beacon: simplify IsTTDReached check
* consensus/beacon: correct IsTTDReached check
Co-authored-by: rjl493456442 <garyrong0905@gmail.com>
Co-authored-by: Péter Szilágyi <peterke@gmail.com>
2021-11-26 11:23:02 +00:00
|
|
|
"math/big"
|
|
|
|
|
2021-04-16 19:29:22 +00:00
|
|
|
"github.com/ethereum/go-ethereum/common"
|
|
|
|
"github.com/ethereum/go-ethereum/common/hexutil"
|
2022-01-31 12:22:35 +00:00
|
|
|
"github.com/ethereum/go-ethereum/core/types"
|
|
|
|
"github.com/ethereum/go-ethereum/trie"
|
2021-04-16 19:29:22 +00:00
|
|
|
)
|
|
|
|
|
2023-01-25 14:32:25 +00:00
|
|
|
//go:generate go run github.com/fjl/gencodec -type PayloadAttributes -field-override payloadAttributesMarshaling -out gen_blockparams.go
|
|
|
|
|
|
|
|
// PayloadAttributes describes the environment context in which a block should
|
|
|
|
// be built.
|
|
|
|
type PayloadAttributes struct {
|
|
|
|
Timestamp uint64 `json:"timestamp" gencodec:"required"`
|
|
|
|
Random common.Hash `json:"prevRandao" gencodec:"required"`
|
|
|
|
SuggestedFeeRecipient common.Address `json:"suggestedFeeRecipient" gencodec:"required"`
|
|
|
|
Withdrawals []*types.Withdrawal `json:"withdrawals"`
|
2021-04-16 19:29:22 +00:00
|
|
|
}
|
|
|
|
|
2023-01-25 14:32:25 +00:00
|
|
|
// JSON type overrides for PayloadAttributes.
|
2021-12-03 15:26:28 +00:00
|
|
|
type payloadAttributesMarshaling struct {
|
2021-04-16 19:29:22 +00:00
|
|
|
Timestamp hexutil.Uint64
|
|
|
|
}
|
|
|
|
|
2023-01-25 14:32:25 +00:00
|
|
|
//go:generate go run github.com/fjl/gencodec -type ExecutableData -field-override executableDataMarshaling -out gen_ed.go
|
|
|
|
|
|
|
|
// ExecutableData is the data necessary to execute an EL payload.
|
|
|
|
type ExecutableData struct {
|
|
|
|
ParentHash common.Hash `json:"parentHash" gencodec:"required"`
|
|
|
|
FeeRecipient common.Address `json:"feeRecipient" gencodec:"required"`
|
|
|
|
StateRoot common.Hash `json:"stateRoot" gencodec:"required"`
|
|
|
|
ReceiptsRoot common.Hash `json:"receiptsRoot" gencodec:"required"`
|
|
|
|
LogsBloom []byte `json:"logsBloom" gencodec:"required"`
|
|
|
|
Random common.Hash `json:"prevRandao" gencodec:"required"`
|
|
|
|
Number uint64 `json:"blockNumber" gencodec:"required"`
|
|
|
|
GasLimit uint64 `json:"gasLimit" gencodec:"required"`
|
|
|
|
GasUsed uint64 `json:"gasUsed" gencodec:"required"`
|
|
|
|
Timestamp uint64 `json:"timestamp" gencodec:"required"`
|
|
|
|
ExtraData []byte `json:"extraData" gencodec:"required"`
|
|
|
|
BaseFeePerGas *big.Int `json:"baseFeePerGas" gencodec:"required"`
|
|
|
|
BlockHash common.Hash `json:"blockHash" gencodec:"required"`
|
|
|
|
Transactions [][]byte `json:"transactions" gencodec:"required"`
|
|
|
|
Withdrawals []*types.Withdrawal `json:"withdrawals"`
|
2021-04-16 19:29:22 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// JSON type overrides for executableData.
|
|
|
|
type executableDataMarshaling struct {
|
all: core rework for the merge transition (#23761)
* all: work for eth1/2 transtition
* consensus/beacon, eth: change beacon difficulty to 0
* eth: updates
* all: add terminalBlockDifficulty config, fix rebasing issues
* eth: implemented merge interop spec
* internal/ethapi: update to v1.0.0.alpha.2
This commit updates the code to the new spec, moving payloadId into
it's own object. It also fixes an issue with finalizing an empty blockhash.
It also properly sets the basefee
* all: sync polishes, other fixes + refactors
* core, eth: correct semantics for LeavePoW, EnterPoS
* core: fixed rebasing artifacts
* core: light: performance improvements
* core: use keyed field (f)
* core: eth: fix compilation issues + tests
* eth/catalyst: dbetter error codes
* all: move Merger to consensus/, remove reliance on it in bc
* all: renamed EnterPoS and LeavePoW to ReachTDD and FinalizePoS
* core: make mergelogs a function
* core: use InsertChain instead of InsertBlock
* les: drop merger from lightchain object
* consensus: add merger
* core: recoverAncestors in catalyst mode
* core: fix nitpick
* all: removed merger from beacon, use TTD, nitpicks
* consensus: eth: add docstring, removed unnecessary code duplication
* consensus/beacon: better comment
* all: easy to fix nitpicks by karalabe
* consensus/beacon: verify known headers to be sure
* core: comments
* core: eth: don't drop peers who advertise blocks, nitpicks
* core: never add beacon blocks to the future queue
* core: fixed nitpicks
* consensus/beacon: simplify IsTTDReached check
* consensus/beacon: correct IsTTDReached check
Co-authored-by: rjl493456442 <garyrong0905@gmail.com>
Co-authored-by: Péter Szilágyi <peterke@gmail.com>
2021-11-26 11:23:02 +00:00
|
|
|
Number hexutil.Uint64
|
|
|
|
GasLimit hexutil.Uint64
|
|
|
|
GasUsed hexutil.Uint64
|
|
|
|
Timestamp hexutil.Uint64
|
|
|
|
BaseFeePerGas *hexutil.Big
|
|
|
|
ExtraData hexutil.Bytes
|
|
|
|
LogsBloom hexutil.Bytes
|
|
|
|
Transactions []hexutil.Bytes
|
|
|
|
}
|
|
|
|
|
2023-01-25 14:32:25 +00:00
|
|
|
//go:generate go run github.com/fjl/gencodec -type ExecutionPayloadEnvelope -field-override executionPayloadEnvelopeMarshaling -out gen_epe.go
|
|
|
|
|
|
|
|
type ExecutionPayloadEnvelope struct {
|
|
|
|
ExecutionPayload *ExecutableData `json:"executionPayload" gencodec:"required"`
|
|
|
|
BlockValue *big.Int `json:"blockValue" gencodec:"required"`
|
|
|
|
}
|
|
|
|
|
|
|
|
// JSON type overrides for ExecutionPayloadEnvelope.
|
|
|
|
type executionPayloadEnvelopeMarshaling struct {
|
|
|
|
BlockValue *hexutil.Big
|
|
|
|
}
|
|
|
|
|
2022-03-17 15:20:03 +00:00
|
|
|
type PayloadStatusV1 struct {
|
|
|
|
Status string `json:"status"`
|
|
|
|
LatestValidHash *common.Hash `json:"latestValidHash"`
|
|
|
|
ValidationError *string `json:"validationError"`
|
2021-12-03 15:26:28 +00:00
|
|
|
}
|
|
|
|
|
2022-03-17 15:20:03 +00:00
|
|
|
type TransitionConfigurationV1 struct {
|
|
|
|
TerminalTotalDifficulty *hexutil.Big `json:"terminalTotalDifficulty"`
|
|
|
|
TerminalBlockHash common.Hash `json:"terminalBlockHash"`
|
|
|
|
TerminalBlockNumber hexutil.Uint64 `json:"terminalBlockNumber"`
|
all: core rework for the merge transition (#23761)
* all: work for eth1/2 transtition
* consensus/beacon, eth: change beacon difficulty to 0
* eth: updates
* all: add terminalBlockDifficulty config, fix rebasing issues
* eth: implemented merge interop spec
* internal/ethapi: update to v1.0.0.alpha.2
This commit updates the code to the new spec, moving payloadId into
it's own object. It also fixes an issue with finalizing an empty blockhash.
It also properly sets the basefee
* all: sync polishes, other fixes + refactors
* core, eth: correct semantics for LeavePoW, EnterPoS
* core: fixed rebasing artifacts
* core: light: performance improvements
* core: use keyed field (f)
* core: eth: fix compilation issues + tests
* eth/catalyst: dbetter error codes
* all: move Merger to consensus/, remove reliance on it in bc
* all: renamed EnterPoS and LeavePoW to ReachTDD and FinalizePoS
* core: make mergelogs a function
* core: use InsertChain instead of InsertBlock
* les: drop merger from lightchain object
* consensus: add merger
* core: recoverAncestors in catalyst mode
* core: fix nitpick
* all: removed merger from beacon, use TTD, nitpicks
* consensus: eth: add docstring, removed unnecessary code duplication
* consensus/beacon: better comment
* all: easy to fix nitpicks by karalabe
* consensus/beacon: verify known headers to be sure
* core: comments
* core: eth: don't drop peers who advertise blocks, nitpicks
* core: never add beacon blocks to the future queue
* core: fixed nitpicks
* consensus/beacon: simplify IsTTDReached check
* consensus/beacon: correct IsTTDReached check
Co-authored-by: rjl493456442 <garyrong0905@gmail.com>
Co-authored-by: Péter Szilágyi <peterke@gmail.com>
2021-11-26 11:23:02 +00:00
|
|
|
}
|
|
|
|
|
2022-01-20 11:29:06 +00:00
|
|
|
// PayloadID is an identifier of the payload build process
|
|
|
|
type PayloadID [8]byte
|
|
|
|
|
|
|
|
func (b PayloadID) String() string {
|
|
|
|
return hexutil.Encode(b[:])
|
|
|
|
}
|
|
|
|
|
|
|
|
func (b PayloadID) MarshalText() ([]byte, error) {
|
|
|
|
return hexutil.Bytes(b[:]).MarshalText()
|
|
|
|
}
|
|
|
|
|
|
|
|
func (b *PayloadID) UnmarshalText(input []byte) error {
|
|
|
|
err := hexutil.UnmarshalFixedText("PayloadID", input, b[:])
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("invalid payload id %q: %w", input, err)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2021-12-03 15:26:28 +00:00
|
|
|
type ForkChoiceResponse struct {
|
2022-03-17 15:20:03 +00:00
|
|
|
PayloadStatus PayloadStatusV1 `json:"payloadStatus"`
|
|
|
|
PayloadID *PayloadID `json:"payloadId"`
|
2021-12-03 15:26:28 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
type ForkchoiceStateV1 struct {
|
all: core rework for the merge transition (#23761)
* all: work for eth1/2 transtition
* consensus/beacon, eth: change beacon difficulty to 0
* eth: updates
* all: add terminalBlockDifficulty config, fix rebasing issues
* eth: implemented merge interop spec
* internal/ethapi: update to v1.0.0.alpha.2
This commit updates the code to the new spec, moving payloadId into
it's own object. It also fixes an issue with finalizing an empty blockhash.
It also properly sets the basefee
* all: sync polishes, other fixes + refactors
* core, eth: correct semantics for LeavePoW, EnterPoS
* core: fixed rebasing artifacts
* core: light: performance improvements
* core: use keyed field (f)
* core: eth: fix compilation issues + tests
* eth/catalyst: dbetter error codes
* all: move Merger to consensus/, remove reliance on it in bc
* all: renamed EnterPoS and LeavePoW to ReachTDD and FinalizePoS
* core: make mergelogs a function
* core: use InsertChain instead of InsertBlock
* les: drop merger from lightchain object
* consensus: add merger
* core: recoverAncestors in catalyst mode
* core: fix nitpick
* all: removed merger from beacon, use TTD, nitpicks
* consensus: eth: add docstring, removed unnecessary code duplication
* consensus/beacon: better comment
* all: easy to fix nitpicks by karalabe
* consensus/beacon: verify known headers to be sure
* core: comments
* core: eth: don't drop peers who advertise blocks, nitpicks
* core: never add beacon blocks to the future queue
* core: fixed nitpicks
* consensus/beacon: simplify IsTTDReached check
* consensus/beacon: correct IsTTDReached check
Co-authored-by: rjl493456442 <garyrong0905@gmail.com>
Co-authored-by: Péter Szilágyi <peterke@gmail.com>
2021-11-26 11:23:02 +00:00
|
|
|
HeadBlockHash common.Hash `json:"headBlockHash"`
|
2021-12-03 15:26:28 +00:00
|
|
|
SafeBlockHash common.Hash `json:"safeBlockHash"`
|
all: core rework for the merge transition (#23761)
* all: work for eth1/2 transtition
* consensus/beacon, eth: change beacon difficulty to 0
* eth: updates
* all: add terminalBlockDifficulty config, fix rebasing issues
* eth: implemented merge interop spec
* internal/ethapi: update to v1.0.0.alpha.2
This commit updates the code to the new spec, moving payloadId into
it's own object. It also fixes an issue with finalizing an empty blockhash.
It also properly sets the basefee
* all: sync polishes, other fixes + refactors
* core, eth: correct semantics for LeavePoW, EnterPoS
* core: fixed rebasing artifacts
* core: light: performance improvements
* core: use keyed field (f)
* core: eth: fix compilation issues + tests
* eth/catalyst: dbetter error codes
* all: move Merger to consensus/, remove reliance on it in bc
* all: renamed EnterPoS and LeavePoW to ReachTDD and FinalizePoS
* core: make mergelogs a function
* core: use InsertChain instead of InsertBlock
* les: drop merger from lightchain object
* consensus: add merger
* core: recoverAncestors in catalyst mode
* core: fix nitpick
* all: removed merger from beacon, use TTD, nitpicks
* consensus: eth: add docstring, removed unnecessary code duplication
* consensus/beacon: better comment
* all: easy to fix nitpicks by karalabe
* consensus/beacon: verify known headers to be sure
* core: comments
* core: eth: don't drop peers who advertise blocks, nitpicks
* core: never add beacon blocks to the future queue
* core: fixed nitpicks
* consensus/beacon: simplify IsTTDReached check
* consensus/beacon: correct IsTTDReached check
Co-authored-by: rjl493456442 <garyrong0905@gmail.com>
Co-authored-by: Péter Szilágyi <peterke@gmail.com>
2021-11-26 11:23:02 +00:00
|
|
|
FinalizedBlockHash common.Hash `json:"finalizedBlockHash"`
|
|
|
|
}
|
2022-01-31 12:22:35 +00:00
|
|
|
|
|
|
|
func encodeTransactions(txs []*types.Transaction) [][]byte {
|
|
|
|
var enc = make([][]byte, len(txs))
|
|
|
|
for i, tx := range txs {
|
|
|
|
enc[i], _ = tx.MarshalBinary()
|
|
|
|
}
|
|
|
|
return enc
|
|
|
|
}
|
|
|
|
|
|
|
|
func decodeTransactions(enc [][]byte) ([]*types.Transaction, error) {
|
|
|
|
var txs = make([]*types.Transaction, len(enc))
|
|
|
|
for i, encTx := range enc {
|
|
|
|
var tx types.Transaction
|
|
|
|
if err := tx.UnmarshalBinary(encTx); err != nil {
|
|
|
|
return nil, fmt.Errorf("invalid transaction %d: %v", i, err)
|
|
|
|
}
|
|
|
|
txs[i] = &tx
|
|
|
|
}
|
|
|
|
return txs, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// ExecutableDataToBlock constructs a block from executable data.
|
|
|
|
// It verifies that the following fields:
|
2022-09-10 11:25:40 +00:00
|
|
|
//
|
|
|
|
// len(extraData) <= 32
|
|
|
|
// uncleHash = emptyUncleHash
|
|
|
|
// difficulty = 0
|
|
|
|
//
|
2023-01-25 14:32:25 +00:00
|
|
|
// and that the blockhash of the constructed block matches the parameters. Nil
|
|
|
|
// Withdrawals value will propagate through the returned block. Empty
|
|
|
|
// Withdrawals value must be passed via non-nil, length 0 value in params.
|
|
|
|
func ExecutableDataToBlock(params ExecutableData) (*types.Block, error) {
|
2022-01-31 12:22:35 +00:00
|
|
|
txs, err := decodeTransactions(params.Transactions)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
if len(params.ExtraData) > 32 {
|
|
|
|
return nil, fmt.Errorf("invalid extradata length: %v", len(params.ExtraData))
|
|
|
|
}
|
2022-05-31 09:11:50 +00:00
|
|
|
if len(params.LogsBloom) != 256 {
|
|
|
|
return nil, fmt.Errorf("invalid logsBloom length: %v", len(params.LogsBloom))
|
|
|
|
}
|
|
|
|
// Check that baseFeePerGas is not negative or too big
|
|
|
|
if params.BaseFeePerGas != nil && (params.BaseFeePerGas.Sign() == -1 || params.BaseFeePerGas.BitLen() > 256) {
|
|
|
|
return nil, fmt.Errorf("invalid baseFeePerGas: %v", params.BaseFeePerGas)
|
|
|
|
}
|
2023-01-25 14:32:25 +00:00
|
|
|
// Only set withdrawalsRoot if it is non-nil. This allows CLs to use
|
|
|
|
// ExecutableData before withdrawals are enabled by marshaling
|
|
|
|
// Withdrawals as the json null value.
|
|
|
|
var withdrawalsRoot *common.Hash
|
|
|
|
if params.Withdrawals != nil {
|
|
|
|
h := types.DeriveSha(types.Withdrawals(params.Withdrawals), trie.NewStackTrie(nil))
|
|
|
|
withdrawalsRoot = &h
|
|
|
|
}
|
2022-01-31 12:22:35 +00:00
|
|
|
header := &types.Header{
|
2023-01-25 14:32:25 +00:00
|
|
|
ParentHash: params.ParentHash,
|
|
|
|
UncleHash: types.EmptyUncleHash,
|
|
|
|
Coinbase: params.FeeRecipient,
|
|
|
|
Root: params.StateRoot,
|
|
|
|
TxHash: types.DeriveSha(types.Transactions(txs), trie.NewStackTrie(nil)),
|
|
|
|
ReceiptHash: params.ReceiptsRoot,
|
|
|
|
Bloom: types.BytesToBloom(params.LogsBloom),
|
|
|
|
Difficulty: common.Big0,
|
|
|
|
Number: new(big.Int).SetUint64(params.Number),
|
|
|
|
GasLimit: params.GasLimit,
|
|
|
|
GasUsed: params.GasUsed,
|
|
|
|
Time: params.Timestamp,
|
|
|
|
BaseFee: params.BaseFeePerGas,
|
|
|
|
Extra: params.ExtraData,
|
|
|
|
MixDigest: params.Random,
|
|
|
|
WithdrawalsHash: withdrawalsRoot,
|
2022-01-31 12:22:35 +00:00
|
|
|
}
|
2023-01-25 14:32:25 +00:00
|
|
|
block := types.NewBlockWithHeader(header).WithBody(txs, nil /* uncles */).WithWithdrawals(params.Withdrawals)
|
2022-01-31 12:22:35 +00:00
|
|
|
if block.Hash() != params.BlockHash {
|
|
|
|
return nil, fmt.Errorf("blockhash mismatch, want %x, got %x", params.BlockHash, block.Hash())
|
|
|
|
}
|
|
|
|
return block, nil
|
|
|
|
}
|
|
|
|
|
2023-01-25 14:32:25 +00:00
|
|
|
// BlockToExecutableData constructs the ExecutableData structure by filling the
|
2022-01-31 12:22:35 +00:00
|
|
|
// fields from the given block. It assumes the given block is post-merge block.
|
2023-01-25 14:32:25 +00:00
|
|
|
func BlockToExecutableData(block *types.Block, fees *big.Int) *ExecutionPayloadEnvelope {
|
|
|
|
data := &ExecutableData{
|
2022-01-31 12:22:35 +00:00
|
|
|
BlockHash: block.Hash(),
|
|
|
|
ParentHash: block.ParentHash(),
|
|
|
|
FeeRecipient: block.Coinbase(),
|
|
|
|
StateRoot: block.Root(),
|
|
|
|
Number: block.NumberU64(),
|
|
|
|
GasLimit: block.GasLimit(),
|
|
|
|
GasUsed: block.GasUsed(),
|
|
|
|
BaseFeePerGas: block.BaseFee(),
|
|
|
|
Timestamp: block.Time(),
|
|
|
|
ReceiptsRoot: block.ReceiptHash(),
|
|
|
|
LogsBloom: block.Bloom().Bytes(),
|
|
|
|
Transactions: encodeTransactions(block.Transactions()),
|
|
|
|
Random: block.MixDigest(),
|
|
|
|
ExtraData: block.Extra(),
|
2023-01-25 14:32:25 +00:00
|
|
|
Withdrawals: block.Withdrawals(),
|
2022-01-31 12:22:35 +00:00
|
|
|
}
|
2023-01-25 14:32:25 +00:00
|
|
|
return &ExecutionPayloadEnvelope{ExecutionPayload: data, BlockValue: fees}
|
2022-01-31 12:22:35 +00:00
|
|
|
}
|
2023-02-06 09:21:40 +00:00
|
|
|
|
|
|
|
// ExecutionPayloadBodyV1 is used in the response to GetPayloadBodiesByHashV1 and GetPayloadBodiesByRangeV1
|
|
|
|
type ExecutionPayloadBodyV1 struct {
|
|
|
|
TransactionData []hexutil.Bytes `json:"transactions"`
|
2023-03-07 15:30:04 +00:00
|
|
|
Withdrawals []*types.Withdrawal `json:"withdrawals"`
|
2023-02-06 09:21:40 +00:00
|
|
|
}
|