Semantic Web and Web 3.0 · class 11 · demo

The contract, deployed

examples/03-contract/contracts/Log.sol, compiled by solc 0.8.37 to 1412 bytes and deployed on the in-memory chain. Then the certificates of the domain were registered, one transaction per block.

0x8f7a45ebde059392e46a46dcc14ab24681a961ea
contract address
8412990
deployed in block
1412
bytes of code · 4 ABI entries

The transactions, block by block

The issuer is the fixed account 0x1111111111111111111111111111111111111111; on this virtual machine nothing is signed, the caller is declared. The transaction hashes are stand-ins — a real one comes from the receipt of a signed transaction.

BlockTimeCallResultTx
84129902026-10-30 23:59:48 UTCdeploy Logcontract created0xe153…57542f
84129912026-10-31 00:00:00 UTCregister(0x1d38a2…94aef7, certificate:2026-001)event LogCreated0x5ebc…937c1d
84129922026-10-31 00:00:12 UTCregister(0x559cc4…e5555a, certificate:2026-002)event LogCreated0x607d…57203e
84129932026-10-31 00:00:24 UTCregister(0x1d38a2…94aef7, certificate:2026-001)reverted AlreadyRegistered(0x1d38a2…94aef7)
no event, no change to the state — and on a real chain still in a block, and still paid for
0xcd41…60cac6
84129942026-10-31 00:00:36 UTCregister(0x736e39…ab42d6, revocation:2026-002)event LogCreated0xb8f2…1fd509

The state, read back with get(digest)

A view call: no transaction, no block. A digest nobody registered does not fail — it reads as an entry of zeros (highlighted). That is why existence is tested by the author.

DigestDocumentauthorregisteredAturi
0x1d38a2…94aef7certificate:2026-0010x111111…1111111793404800
2026-10-31 00:00:00 UTC
https://data.csoares.me/certificate/2026-001
0x559cc4…e5555acertificate:2026-0020x111111…1111111793404812
2026-10-31 00:00:12 UTC
https://data.csoares.me/certificate/2026-002
0x736e39…ab42d6revocation:2026-0020x111111…1111111793404836
2026-10-31 00:00:36 UTC
https://data.csoares.me/revocation/2026-002
0xc29e5c…8bb569certificate:2026-0030x000…0000""

private is not secret

entries is private, and yet the storage slot of the first certificate, keccak256(digest · 0), reads straight from the state, with no ABI (example 04). The author and the timestamp are packed together in it:

slot  0x79a8298a3d002eae54728b21b3812c3a6be516217830289278cdddeb4bd74104
value 0x6ae52f801111111111111111111111111111111111111111

The ABI: what the outside world sees

KindDeclarationId
errorerror AlreadyRegistered(bytes32 digest)0x77caf672selector: the first 4 bytes of the call data
eventevent LogCreated(bytes32 indexed digest, address indexed author, string uri)0x9b9e191a…5a8db1topic 0: what a node filters on
functionfunction get(bytes32 digest) view returns ((address author, uint64 registeredAt, string uri))0x8eaa6ac0selector: the first 4 bytes of the call data
functionfunction register(bytes32 digest, string uri)0xcf2d31fbselector: the first 4 bytes of the call data

The source

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

/// @title Log -- an anchor of existence and integrity.
/// @notice Keeps on chain only the document digest and the URI where it
/// lives. The document stays off chain: costly, public and immutable are three problems.
contract Log {
    // A struct groups fields, like a record. uint64 is a 64-bit unsigned
    // integer: enough for a Unix timestamp, and cheaper to store than uint256.
    struct Entry {
        address author;
        uint64 registeredAt;
        string uri;
    }

    // A mapping is a key -> value table kept in the contract's storage, on
    // chain. Every key "exists": a missing one reads as a struct of zeros.
    mapping(bytes32 => Entry) private entries;

    // An event is written into the transaction log, not into storage. It is
    // cheap, contracts cannot read it, but off-chain programs (the indexer)
    // can. `indexed` fields can be used as filters.
    event LogCreated(bytes32 indexed digest, address indexed author, string uri);

    // A custom error: cheaper than a revert string, and it carries data.
    error AlreadyRegistered(bytes32 digest);

    // `external`: callable from outside (a transaction or another contract).
    // `calldata`: the string is read straight from the call input, not copied.
    function register(bytes32 digest, string calldata uri) external {
        // Existence is tested by the author: address(0) means "never written".
        if (entries[digest].author != address(0)) revert AlreadyRegistered(digest);
        // msg.sender is whoever called; block.timestamp is the block's time.
        entries[digest] = Entry(msg.sender, uint64(block.timestamp), uri);
        emit LogCreated(digest, msg.sender, uri);
    }

    // `view`: reads the state and changes nothing, so calling it costs no gas
    // when done off chain. `memory`: the returned struct is a temporary copy.
    function get(bytes32 digest) external view returns (Entry memory) {
        return entries[digest];
    }
}

An in-memory chain (the Ethereum virtual machine, @ethereumjs/evm), in this one process: no test network, no wallet, no keys, no money. Nothing a visitor does is kept.