Concordium / concordium-transaction-logger

A service that logs all transactions and indexes them in a postgres database
Mozilla Public License 2.0
2 stars 2 forks source link

Transaction logger

Log affected accounts and smart contracts into a postgres database.

Supported configuration options

Failure handling

The service handles nodes disappearing or getting behind, as well as the database connection being lost. The key design features of the service are

Database format

The transaction logging can be used to construct additional indices that are not needed for consensus and thus not provided by the node directly. The transaction logger at the moment creates an index of transactions by affected account and smart contract. This can be used for monitoring incoming transactions on an account. Only finalized transactions are logged.

The database must exist before the logger starts.

If correct tables exist in the database then they will be used, otherwise the schema.sql script will be executed to create the tables and indices that are required.

There are two sets of tables. The first three tables, ati, cti, and summaries, contain an index of transactions by account and contract address. The ati and cti stand for account, respectively contract, transaction index. They contain an index so that a transaction affecting a given contract or account can be quickly looked up. The outcome of each transaction is in the summaries table.

The summary that is stored in the summary column of the summaries table is stored as a JSON value. The contents is either of the form

{
    "Left": ...
}

where ... is a transaction outcome in the same format as it appears in block summaries, or

{
    "Right": ...
}

where ... is a special transaction outcome in the same format as it appears in block summaries.

The second set is the table of CIS2 tokens containing a list of all tokens that were discovered, together with their total supply.

Account transaction index

The meaning of the (id, account, summary_id) row in the ati table is that account account was affected by transaction pointed to by summary_id. Affected here means that either the account sent the transaction, or it was the target of it, for example another account sent a transfer to it. Note that accounts are stored in binary format, so as 32-byte arrays, and not in their Base58check encoding.

The data is written to the table upon each finalization from oldest to newest block finalized by that round. For each block transactions are written in the order they appear in the block, that is, from start to end of the block. The ids in all tables are automatically generated. Note that they should not be relied upon to be strictly sequential. Postgres does not guarantee this. It only guarantees that they will be strictly increasing, but there might be gaps.

The logger will never update any rows in the database, it only ever appends data to the tables.

Contract transaction index

The meaning is analogous to the account transaction index, except here the logger logs transactions that affect smart contracts.

Examples

Most recent transactions

The database can be polled for transactions affecting a given account, for example to get the most recent transactions affecting an account a query like the following could be used

SELECT summaries.block, summaries.timestamp, summaries.summary
FROM ati JOIN summaries ON ati.summary = summaries.id
WHERE ati.account = $1
ORDER BY ati.id DESC LIMIT $2

where $1 would be the given account address, and $2 the number of desired transactions.

Notifications

Postgres supports Notifications and Listening on channels. This can be used to replace polling for updates, in some cases, with subscriptions.

One way to achieve this is to register triggers on, ati and cti tables (or just one of them). For example, a trigger that would send notifications to listen_channel for each row inserted into the ati table would look as follows.

CREATE FUNCTION pg_temp.notify_insert ()
RETURNS trigger
LANGUAGE plpgsql
as $$
BEGIN
  PERFORM (
    WITH summary AS
    (
      SELECT summaries.summary FROM summaries WHERE summaries.id = NEW.summary
    )
    SELECT pg_notify(TG_ARGV[0], summary::text) from summary
  );
  RETURN NULL;
END;
$$;

CREATE TRIGGER notify_on_insert_into_ati
AFTER INSERT
ON ati
FOR EACH ROW
EXECUTE PROCEDURE pg_temp.notify_insert('listen_channel');

LISTEN listen_channel;

Note that the use of the pg_temp schema means that these triggers will only apply to the current Postgres session, i.e., they will be dropped upon disconnect. If this is not desired then use a different schema (or no schema at all).

For each row that is inserted this will notify the listen_channel with the summary. Note that it is not guaranteed that there will be one notification for each insertion. If summaries are equal then Postgres is allowed to coalesce multiple notifications into one. To get one notification per insertion adjust the summary to include the row id, for example.

Caveats

Contributing

Contributor Covenant

This repository's CI automatically checks formatting and common problems in rust. Changes to any of the packages must be such that

Everything in this repository should build with stable rust at the moment (at least version 1.60 and up), however the fmt tool must be from a nightly release since some of the configuration options are not stable. One way to run the fmt tool is

 cargo +nightly-2023-04-01 fmt

(the exact version used by the CI can be found in .github/workflows/ci.yaml file). You will need to have a recent enough nightly version installed, which can be done via

rustup toolchain install nightly-2023-04-01

or similar, using the rustup tool. See the documentation of the tool for more details.

In order to contribute you should make a pull request and ask a person familiar with the codebase for a review.

Building

The project is a pure Rust project, and can be build by running

cargo build --release

This produces a single binary target/release/transaction-logger.

Make sure that you have checked and initialized submodules before the build e.g., using

git submodule update --init --recursive

Docker build

A Dockerfile is available that produces a self-contained image with the service installed and set as the entrypoint.

This docker image can be built using

docker build --build-arg build_image=rust:1.61-buster --build-arg base_image=debian:buster .

which produces a debian-buster based image.