Commit Graph

2423 Commits

Author SHA1 Message Date
Jamie Lokier 0e2bc8408d
Transaction: Run all computations via EVMC `execute`
1. Send all EVM executions through the EVMC `execute` function.

   It leads to the same place in the end as calling `Computation` before, but
   `execute` is the API function used by all EVMC implementations, and it is
   very explicit what data is passed back and forth.

2. As a consequence this starts using the new `host_services` code from EVM, so
   this is a significant change to the paths used for account state processing.

3. Because we will have to remove the `newComputation` call on the host side,
   anticipating that the contract code is now saved in `host` instead of being
   copied around.  As it's saved in `host`, there is no need to pass it
   separately to `evmcExecComputation`.

Signed-off-by: Jamie Lokier <jamie@shareable.org>
2021-06-08 18:29:39 +01:00
Jamie Lokier df71c8bec9
EVMC: Disable byte-endian conversion of 256-bit values on EVM side
We'll re-enable endian conversions based on a negotiated run-time option later,
but for now let's remove one complication to testing the new EVMC paths, and
also gain a little performance.

Signed-off-by: Jamie Lokier <jamie@shareable.org>
2021-06-08 18:29:39 +01:00
Jamie Lokier e8a79e7246
EVMC: Reference EVMC entry point so it's linked into the program
Even though `evmc_create_nimbus_evm` is called, it fails at link time because
the definition of that function isn't included unless it is pulled in
explicitly.

Signed-off-by: Jamie Lokier <jamie@shareable.org>
2021-06-08 18:29:39 +01:00
Jamie Lokier 10807bce39
EVMC: Binary compatibility on the VM side for calling `execute`
This provides the functions a loadable VM must provide for a host to use it.
The main access to a loaded EVM is `evmc_create_nimbus_evm`, and this meant to
be the only exported function the caller starts with.

That provides access to other functions, also defined in this patch, to
configure the EVM and then the key interesting function is `execute`.

`execute` runs a full computation, here using Nimbus EVM `Computation`.

(Note, even though everything is EVMC binary-compatible, there is a small
dependency on `TransactionHost` in `execute` here, which prevents this being
used by a host that is not Nimbus at the moment.  It is necessary for some
tests, and will eventually go away.)

Although this provides the VM-side functionality needed by the host, it does
not contain the glue functions for `Computation` to call the host, which are
already part of the Nimbus EVM in `nimbus/vm/evmc_api.nim`.

Signed-off-by: Jamie Lokier <jamie@shareable.org>
2021-06-08 18:29:39 +01:00
Jamie Lokier 0b19f42158
EVMC: Binary compatibility glue on the host side
1. This provides the necessary type adjustments for host services to be
   (optionally) callable via the EVMC binary-compatible interface.  This layer
   is stashed away in a glue module so the host services continue to use
   appropriate Nim types, and are still callable directly.

   Inlining is used to ensure there should be no real overhead, including stack
   frame size for the `call` function.  Note, `import` must be used for
   `{.inline.}` to work effectively.

2. This also provides a key call in the other direction, the version of host to
   EVM `execute` that is called on the host side.

Signed-off-by: Jamie Lokier <jamie@shareable.org>
2021-06-08 18:29:39 +01:00
Jamie Lokier d49fa0bb86
Transaction: Add "host services", accessors to host state from EVM
This provides "host services", functions provided by the application to an EVM.
They are a key part of EVMC compatibility, but we will switch to using these
with "native" EVM as well.

These are functions like `getStorage`, `setStorage` and `emitLog` for accessing
the account state, because the EVM is not allowed direct access to the database.

This code is adapted from `nimbus/vm/evmc_host.nim` and other places, but there
is more emphasis on being host-side only, no dependency on the EVM or
`Computation` type.  It uses `TransactionHost` and types in `host_types`.

These host services have two goals: To be compatible with EVMC, and to be a
good way for the Nimbus EVM to access the data it needs.  In our new Nimbus
internal architecture, the EVM will only access the databases and other
application state via these host service functions.

The reason for containing the EVM like this, even "native" EVM, is that having
one good interface to the data makes it a lot easier to change how the database
works, which is on the roadmap.

These functions almost have EVMC signatures, but they are not binary compatible
with EVMC.  (Binary compatibility is provided by another module).  It would be
fine for Nimbus EVM to call these functions directly when linked directly.

Signed-off-by: Jamie Lokier <jamie@shareable.org>
2021-06-08 18:29:39 +01:00
Jamie Lokier a4f92a6543
Transaction: Use same log level for all block rejection causes
Block validation failure isn't an error, it's correct rejection of a bad block
from the network.  All conditions that lead to block rejection return a simple
boolean.

When a block is rejected, most reasons log at `debug` level.  Only `stateRoot`
mismatch shouts a loud, highlighted, multi-line error message with big red
`error` alert.

Historically this was to assist EVM development, because it was more likely to
be a Nimbus EVM bug than a real bad block.  But now the EVM is in good shape,
has a large and thorough testsuite, and `stateRoot` mismatch is more likely to
be a real bad block that should be rejected with less fuss.

If there's a genuine EVM bug, we'll still get an alert: Consensus failure will
quickly become obvious, and the block where it happens is easily fetched.

So a big, loud error is no longer useful, and it became a problem during tests.
Recently a few hundred tests were added that trigger it, and now successful
test output is filled with attention-grabbing errors which aren't really errors
or particularly useful.

Since it's not really an error, the original motivation is now backwards, and
other reasons warn at `debug` level, make this like the others.

Signed-off-by: Jamie Lokier <jamie@shareable.org>
2021-06-08 18:07:50 +01:00
Jamie Lokier 534be53873
vm2: Remove vm2 `forks_list` everywhere, use common forks list
Remove file vm2 file `forks_list, and divert all imports to the common forks
list outside the EVM.

Signed-off-by: Jamie Lokier <jamie@shareable.org>
2021-06-08 15:36:31 +01:00
Jamie Lokier 7c90d8de70
EVM: Remove `vm_forks` everywhere, use common forks list instead
The common forks list was already used, redirected via `vm_forks` for
historical compatibility.  Remove the old `vm_forks` now and divert all imports
to the common forks list outside the EVM.

Signed-off-by: Jamie Lokier <jamie@shareable.org>
2021-06-08 15:36:31 +01:00
Jamie Lokier aee0fe39d2
EVM: Remove `vm_types2` everywhere, use common forks list instead
File `vm_types2` is obsolete.  Remove this file and divert all imports to the
common forks list outside the EVM, or in some cases they don't need it anyway.

Signed-off-by: Jamie Lokier <jamie@shareable.org>
2021-06-08 15:36:31 +01:00
Jamie Lokier 84269ddccf
Forks: Use capitalized names again for presentation (logging etc.)
Fork names were originally capitalized, and were made lower case by @narimiran
in commit 36a7519 to satisfy `parseEnum` in some tests.  Restore the
capitalization and make the tests work with it.

Signed-off-by: Jamie Lokier <jamie@shareable.org>
2021-06-08 15:36:31 +01:00
Jamie Lokier 05bc174bef
Forks: Use a common fork list outside the EVMs
Many places outside the EVM use `Fork` and the fork list, and in general we
want progressively fewer dependencies on EVM internal types and files.

This may prove to be a temporary location, especially when we implement
issue #640.  But it's a fine temporary location if so.

Signed-off-by: Jamie Lokier <jamie@shareable.org>
2021-06-08 15:36:31 +01:00
Jamie Lokier b3a788c7ce
Transaction: Move contract address generation outside the EVM
The current EVM generates its own new contract addresses, and this is why there
are separate `msg.contractAddress` and `msg.codeAddress` fields in the
computation start message.

In EVMC, account updates are only allowed on the host side, including contract
generation, and the start message has one destination field, `msg.destination`.
The EVM cannot select addresses, only use them.  It's a sensible design.

The difference makes the current EVM incompatible with EVMC and its message
format, so this patch corrects the difference.  It moves contract address
generation to the host side.  This simplifies the EVM and its API a little.

(As an API change, this is incompatible with vm2, so it's guarded under
`evmc_enabled` to allow vm2 to continue to build and run at this time.  This is
also why there are fewer deletions than would otherwise be expected.)

Signed-off-by: Jamie Lokier <jamie@shareable.org>
2021-06-08 15:36:30 +01:00
Jamie Lokier 775231eef1
EVM: Apply EIP-6 in the code (affects both vm and vm2)
The rationale in EIP-6[1] for changing names to `selfDestruct` applies to code
as much as it does to specs.  Also, Ethereum uses the new names consistently,
so it's useful for our code to match the terms used in later EIP specs and
testsuite entries.

This change is straightforward, and is a prerequisite for patches to come that
do things with the `selfDestruct` fields.

[1] https://eips.ethereum.org/EIPS/eip-6
Hudson Jameson, "EIP-6: Renaming SUICIDE opcode," Ethereum Improvement
Proposals, no. 6, November 2015.

Signed-off-by: Jamie Lokier <jamie@shareable.org>
2021-06-08 15:36:30 +01:00
jangko f42c76467b
bump nim-graphql from 0.2.14 to 0.2.16
- fixes non repeatable directive error message
- refactor nim caching in github action ci
- refactor github action ci matrix
- extract miniz to it's own repo
2021-06-06 11:27:12 +07:00
jangko 051957c464
add nim-miniz submodule to vendor
nim-miniz now sit in it's own repo after
extracted from nim-graphql

it also a dependency of incoming nim-ws
2021-06-06 11:23:30 +07:00
Jamie Lokier ef7773daa6
Whisper: Remove Whisper-specific hexstring/JSON/key storage support
Signed-off-by: Jamie Lokier <jamie@shareable.org>
2021-06-01 18:12:48 +01:00
Jamie Lokier 613f06e61c
Whisper: Remove all the main Whisper code (config, startup, RPC etc)
This is the main patch which removes Whisper code from `nimbus-eth1` code.
It removes all configuration, help, startup, JSON-RPC calls and most types.

Note, there is still Whisper functionality in `nim-eth`.  Also, the "wrapper"
under `wrappers/` isn't dealt with by this change, but it's not built by
default (and might not currently work).

Signed-off-by: Jamie Lokier <jamie@shareable.org>
2021-06-01 18:12:48 +01:00
Jamie Lokier f5c69f372a
Whisper: Remove all code which starts Whisper running
This is the patch which removes Whisper functionality from `nimbus-eth1`,
even though the code has yet to be removed after.

After this change, enabling Whisper has no effect.  Its configuration is
ignored and it won't be started.

Signed-off-by: Jamie Lokier <jamie@shareable.org>
2021-06-01 18:12:48 +01:00
Jamie Lokier ecb0654da7
Whisper: Remove C and Go wrappers and Nimbus as a library
Remove the C and Go example wrappers that call Nimbus as a library, by removing
the entire `wrappers/` directory.  They are removed because they only wrap
Whisper protocol support, which has been removed as it is obsolete.

The only thing wrapped were Whisper functions, even though there were separate
`go_wrapper_example` and `go_wrapper_whisper_example` programs.  The wrappers
don't build without Whisper in Nimbus, and without it, there isn't enough left
for them to be useful examples.

Also remove support for building the whole of Nimbus as a library, because
there is nothing left using it.  These targets are gone from the Makefile:

- `wrappers`
- `wrappers-static`
- `libnimbus.so`
- `libnimbus.a`

The code isn't really gone, because it remains available in Git history.  It
may be useful someday, so a comment has been left in the Makefile for future
generations:

> This note is kept so that anyone wanting to build Nimbus as a library or call
> from C or Go will know it has been done before.  The previous working version
> can be found in Git history.  Look for the `nimbus-eth1` commit that adds
> this comment and removes `wrappers/*`.

Also worth a note, the library support was not tested on Windows, and the
shared library support was only tested on Linux.

Signed-off-by: Jamie Lokier <jamie@shareable.org>
2021-06-01 18:12:48 +01:00
Jamie Lokier cf36bdb801
Whisper (CI): Remove GitHub actions for building `wrappers*`
With Whisper support gone, the wrappers cannot be built.  More detail in the
patch that removes the wrappers.

Signed-off-by: Jamie Lokier <jamie@shareable.org>
2021-06-01 18:12:47 +01:00
Jamie Lokier 3ba22809f0
Whisper: Remove all Whisper tests and test support
Signed-off-by: Jamie Lokier <jamie@shareable.org>
2021-06-01 18:12:47 +01:00
Jamie Lokier bfa7bd0488
Whisper: Stop testing Whisper
Signed-off-by: Jamie Lokier <jamie@shareable.org>
2021-06-01 18:12:47 +01:00
Jamie Lokier 774f697c73
Whisper: Disable Whisper (Shh) protocol by default
This commit turns Whisper off by default, without changing anything else.

So this can be cherry-picked if you just want to disable Whisper without
removing it.

Signed-off-by: Jamie Lokier <jamie@shareable.org>
2021-06-01 18:12:47 +01:00
Jamie Lokier e2689792b0
vm2: Remove `toSymbolName` unnecessary symbol-text-symbol conversion
There's no need for macro `toSymbolName` to convert fork enum values to their
presentation texts (logging etc) then re-parse them back to a fork enum value.
`asFork` is already used in the same function and works without these steps,
so use it consistently.

Same applies to `op.toSymbolName` and `asOp`.

This makes the code simpler, and removes a text pattern-matching requirement.
The patch has been checked to confirm it doesn't change the compiled code.

Motivation: The forks list will be removed from VM because it is used outside
the VM as well.  Doing so highlighted vm2's `toSymbolName`.  It's not needed,
and it's best if the VM doesn't constrain text strings used outside the VM

Signed-off-by: Jamie Lokier <jamie@shareable.org>
2021-06-01 16:54:38 +01:00
Jamie Lokier 8f9c593dac
EVM: Remove `vm_types2` unused `op_codes`/`opcode_values` re-exports
Everything builds with this section removed, all build options.

Signed-off-by: Jamie Lokier <jamie@shareable.org>
2021-06-01 16:54:38 +01:00
Jamie Lokier fdebf6c1f7
EVM: Remove unused file `vm_message`
Signed-off-by: Jamie Lokier <jamie@shareable.org>
2021-06-01 16:54:38 +01:00
Jamie Lokier 5e718bcbe2
EVM: Remove most unused imports of `vm_*` files
Signed-off-by: Jamie Lokier <jamie@shareable.org>
2021-06-01 16:54:38 +01:00
Jamie Lokier beb750b8df
Transaction: Remove no longer used `setupComputation`
The last caller of `setupComputation` is gone, now that it's been replaced by
the single entry point for all EVM calls, `runComputation`.

With this removal, EVM's `Computation` type should no longer be used anywhere
outside the call module (except in some tests and the EVM itself).

Signed-off-by: Jamie Lokier <jamie@shareable.org>
2021-06-01 16:54:34 +01:00
Jamie Lokier 8b4f5a1103 Transaction: Change transactions to go through unified EVM runner
Simplify transaction validations to use `runComputation`; drop other code.

Getting everything right up to this point to pass all the tests was trickier
than it looks.

Signed-off-by: Jamie Lokier <jamie@shareable.org>
2021-06-01 18:40:41 +03:00
Jamie Lokier bf6569bdeb Assembler: Change assembler tests to go through unified EVM runner
Simplify how assembler tests are run to use `runComputation`; drop other code.

Signed-off-by: Jamie Lokier <jamie@shareable.org>
2021-06-01 18:40:41 +03:00
Jamie Lokier 9211a15c0a Fixtures: Change JSON fixture tests to go through unified EVM runner
Simplify how JSON fixtures tests are run to use `runComputation`.
Drop other code.

These use the `noTransfer` option, which is similar enough to calling
`c.executeOpcodes()` instead of `c.execComputation()`.

Signed-off-by: Jamie Lokier <jamie@shareable.org>
2021-06-01 18:40:41 +03:00
Jamie Lokier 5fb3c51e5e Transaction: Skip balance & nonce updates option in `runComputation`
Add another flag to disable a processing step when a call doesn't come from
a real transaction:

- `noTransfer`: Don't update balances, nonces, code.

This is to support VM fixtures tests which require account balances and nonces
to be unchanged when running the account's code.

These tests call `c.executeOpcodes()`, an internal function of the EVM, instead
of the usual `c.execComputation()`.  It goes direct to the bytecode dispatcher,
skipping parts of `Computation` that are normally called.

But we can't keep calling `c.executeOpcodes()` and have a single entry point to
the VM, let alone an EVMC entry point.

`noTransfer` provides similar enough behaviour to calling `c.executeOpcodes()`
that these tests can use the new single entry point like everything else.

Signed-off-by: Jamie Lokier <jamie@shareable.org>
2021-06-01 18:40:41 +03:00
Jamie Lokier deffa20b07
RPC and GraphQL: Change `estimateGas` to go through unified EVM runner
Simplify `estimateGas` to use `runComputation`; drop other code.

The RPC/GraphQL `estimateGas` operation is quite different from the `call`
operation.  It is much more like ordinary transaction execution than `call`,
though there are still enough differences that tx validation cannot be used.

Signed-off-by: Jamie Lokier <jamie@shareable.org>
2021-05-29 08:23:20 +01:00
Jamie Lokier b16aa2f1f7
RPC and GraphQL: Change `call` op to go through unified EVM runner
Simplify `call` to use `runComputation`; drop other code.

The RPC/GraphQL `call` operation differs in many ways from regular transaction
calls.  The following flags are set, to disable various steps in processing.
All four are true (disabling the corresponding step) for `call`:

- `noIntrinsic`:  Don't charge intrinsic gas.
- `noAccessList`: Don't initialise EIP2929 access list.
- `noGasCharge`:  Don't charge sender account for gas.
- `noRefund`:     Don't apply gas refund/burn rule.

Interestingly, the RPC/GraphQL `estimateGas` operation doesn't behave so
differently from regular transactions.  It might be that not all these steps
should be disabled for `call` either.  But until we investigate what
RPC/GraphQL clients are expecting, keep the same behaviour as before.

Signed-off-by: Jamie Lokier <jamie@shareable.org>
2021-05-29 08:23:20 +01:00
Jamie Lokier 306c8e92c2
Transaction: `runComputation` options for non-standard EVM behaviour
The following four flags are added, to change various steps in EVM processing
when a call doesn't come from a real transaction:

- `noIntrinsic`:  Don't charge intrinsic gas.
- `noAccessList`: Don't initialise EIP2929 access list.
- `noGasCharge`:  Don't charge sender account for gas.
- `noRefund`:     Don't apply gas refund/burn rule.

This is to support RPC and GraphQL `call` operations, which behave differently
in some ways from regular transaction calls, and to support some test suites.

In EVMC terms, all these alterations can be performed on the host side.

Signed-off-by: Jamie Lokier <jamie@shareable.org>
2021-05-29 08:23:19 +01:00
Jamie Lokier 12bf0fd346
Transaction: EIP-2930 (Berlin): Extra intrinsic gas for access list
Calculate extra intrinsic gas for an EIP-2930 transaction with access list.

While we're here, do the rest of the intrinsic gas calculation.  Make it clear,
explicit and in one place.  (Previous code delegated parts of the calculation
to `transaction.nim` but had to do the rest locally due to mismatched types.)

Signed-off-by: Jamie Lokier <jamie@shareable.org>
2021-05-29 06:54:19 +01:00
Jamie Lokier f6cc6e3ed1
Transaction: EIP-2930 (Berlin): Per-transaction extra access list
Add `accessList` to the `runComputation` API for EIP-2930 transactions.

Signed-off-by: Jamie Lokier <jamie@shareable.org>
2021-05-29 06:53:11 +01:00
Jamie Lokier 76031ccbe4
Transaction: EIP-2929 (Berlin): Initial access list
Signed-off-by: Jamie Lokier <jamie@shareable.org>
2021-05-29 06:53:11 +01:00
Jamie Lokier c6d50a0ef7
Transaction: Unified runner `runComputation` for all EVM call types
New entry point `runComputation`, for all EVM calls.
(Later the intent is `runComputationAsync`.)

As noted in commit 297d789, there are six entry points calling EVM computation,
with different parameters and expecting different behaviours.  Parameters were
dealt with in `setupComputation`.  Behaviours are unified in `runComputation`,
with options passed via `CallParams`.

This code performs the steps used when validating a transaction.  Options for
non-standard behaviour for RPC, GraphQL and tests to be added as required.

This replaces `setupComputation`, `execComputation` and `executeOpcodes`
(other than its own calls).  As a result `Computation` and other EVM types are
no longer referenced in the main program, and many imports can be dropped.

Signed-off-by: Jamie Lokier <jamie@shareable.org>
2021-05-29 06:53:11 +01:00
Jamie Lokier 94f95efd9e
Transaction: Move `setupComputation` to its own file
Signed-off-by: Jamie Lokier <jamie@shareable.org>
2021-05-29 06:53:11 +01:00
Jamie Lokier 4e51a5bb35
Fixtures: Change fixture tests to use shared `setupComputation`
Change fixtures tests to use shared `setupComputation` instead of
their own slightly different variant.

Signed-off-by: Jamie Lokier <jamie@shareable.org>
2021-05-28 16:48:09 +01:00
Jamie Lokier a5385e5344 RPC/GraphQL: Change RPC/GraphQL to use shared `setupComputation`
Change RPC/GraphQL calls to the EVM to use shared `setupComputation`
instead of their own special variant.

Signed-off-by: Jamie Lokier <jamie@shareable.org>
2021-05-28 16:10:19 +01:00
Jamie Lokier 8b33cbe568
Assembler: Change assembler tests to use shared `setupComputation`
Signed-off-by: Jamie Lokier <jamie@shareable.org>
2021-05-28 15:50:02 +01:00
Jamie Lokier c655d59b5f
Assembler: Rearrange logic where assembler tests call EVM
Signed-off-by: Jamie Lokier <jamie@shareable.org>
2021-05-28 15:49:58 +01:00
Jamie Lokier 2fe2f23e70
CI: Restore "push" events but disable "pull_request"
Reverts part of commit 2539bd9 while keeping the intention of it:
To avoid duplicate CI runs when making and updating a PR.

Disabling `push` means we cannot push to a branch and see the CI results
directly without making a PR, which some of us use.  There are many situations
where this is useful, and "[WIP]" PRs are not appropriate for all.

Disabling `pull_request` has a similar effect, removing duplicate CI.

It is known that `pull_request` _is_ needed when a third party sends a
PR (because it's not committed to the repo yet).  But this is rare at the
moment, and there's a workaround: A committer can push the third party change
to a branch, triggering CI.

So re-enable `push`, disable `pull_request`, and we'll see if the latter
missing causes problems in practice.  Won't know until we try it.

Note: This might be interim until `workflow_dispatch` is working better.
Perhaps that needs more configuration.  Currently, `workflow_dispatch` is kind
of useless for CI tests, because it doesn't result in any CI indicator
associated with a commit or branch.  Even the actions page doesn't show the
name of the branch, just a less-than-useful generic "CI" for these actions.

Signed-off-by: Jamie Lokier <jamie@shareable.org>
2021-05-28 15:33:35 +01:00
Jamie Lokier 0d3117344a
Transaction: Change tx validation to use shared `setupComputation`
Signed-off-by: Jamie Lokier <jamie@shareable.org>
2021-05-28 15:22:06 +01:00
Ștefan Talpalaru 45da426bb8
CI: enable manual running (#695)
Since we think it needs to be on master to have any effect, sending it to master to test.
2021-05-28 15:14:01 +01:00
Jamie Lokier 297d789d21
Transaction: Merge entry point for `*...SetupComputation` functions
There are currently six entry points to running an EVM computation, all with
slightly different parameters, and expecting slightly different EVM behaviours.

First step in merging them is a common `setupComputation` that replaces all
the different `*...SetupComputation` functions.

This uses the `TransactionHost` type because it's a step towards using that
type for all EVM calls using only EVMC.  For now an EVMC message is created
then translated to EVM-internal `Message`.  It is done this way to build up
the new interface in stages where all tests pass at each stage.

Signed-off-by: Jamie Lokier <jamie@shareable.org>
2021-05-28 12:12:10 +01:00
Jamie Lokier e60ad129a2
Transaction: New object `TransactionHost` for "EVMC host services"
`TransactionHost` represents the interface to the EVM from the application once
we've fully transitioned to EVMC.  It represents a managed EVM call, and the
"EVMC host" side of the host<->EVM boundary.

This holds transaction state which sits outside the EVM, but a pointer to this
is passed around by the EVM as _opaque_ type `evmc_host_context`.

To the EVM, this offers "host services", which manage account state that the
EVM cannot do, such as balance transfers, call costs, storage, logs, gas
refunds, nested calls and new contract addresses.  The EVM has no direct access
to the account state database or network; it's all via "host services".

To the application (host side), this object represents a managed EVM call, with
inputs, a method to run, outputs, hidden transaction state, and later async
scheduling.  It is to replace `Computation` on the application side, while
`Computation` will remain but just be for the EVM side.

Signed-off-by: Jamie Lokier <jamie@shareable.org>
2021-05-28 12:12:02 +01:00