Skip to content

Fix DRep image base64 data URI stripping (#1966)#2138

Open
ArturWieczorek wants to merge 2 commits into
masterfrom
1966-offchain-drep-image-prefix
Open

Fix DRep image base64 data URI stripping (#1966)#2138
ArturWieczorek wants to merge 2 commits into
masterfrom
1966-offchain-drep-image-prefix

Conversation

@ArturWieczorek

Copy link
Copy Markdown
Contributor

When an off-chain DRep metadata image is an inline base64 data URI (data:<mime>;base64,...), db-sync stored only the base64 payload in off_chain_vote_drep_data.image_url, stripping the data:<mime>;base64, prefix (and the MIME type), so the stored value was neither the source contentUrl nor a usable data URI.

CIP-119 defines contentUrl as the full data URI for inline images, so store it verbatim. The Image JSON decoder now keeps any data:-scheme contentUrl as-is; regular URL images (which carry a sha256) are unchanged.

Adds a regression test asserting a base64 data-URI contentUrl is decoded without losing its prefix.

Description

Add your description here, if it fixes a particular issue please provide a link to the issue.

Checklist

  • Commit sequence broadly makes sense
  • Commits have useful messages
  • New tests are added if needed and existing tests are updated
  • Any changes are noted in the changelog
  • Code is formatted with fourmolu on version 0.17.0.0 (which can be run with scripts/fourmolize.sh)
  • Self-reviewed the diff

Migrations

  • The pr causes a breaking change of type a,b or c
  • If there is a breaking change, the pr includes a database migration and/or a fix process for old values, so that upgrade is possible
  • Resyncing and running the migrations provided will result in the same database semantically

If there is a breaking change, especially a big one, please add a justification here. Please elaborate
more what the migration achieves, what it cannot achieve or why a migration is not possible.

When an off-chain DRep metadata image is an inline base64 data URI
(`data:<mime>;base64,...`), db-sync stored only the base64 payload in
`off_chain_vote_drep_data.image_url`, stripping the `data:<mime>;base64,`
prefix (and the MIME type), so the stored value was neither the source
contentUrl nor a usable data URI.

CIP-119 defines `contentUrl` as the full data URI for inline images, so
store it verbatim. The `Image` JSON decoder now keeps any `data:`-scheme
contentUrl as-is; regular URL images (which carry a sha256) are unchanged.

Adds a regression test asserting a base64 data-URI contentUrl is decoded
without losing its prefix.
@ArturWieczorek

Copy link
Copy Markdown
Contributor Author

Looks like this CI failure is not caused by code changes but some Nix infrastructure issue on the macOS CI runner.

@ArturWieczorek

Copy link
Copy Markdown
Contributor Author

Tests Run

💚 All tests passed except one that is supposed to fail for this exact change:
cardano_node_tests/tests/tests_conway/test_drep.py::TestDReps::test_register_and_retire_drep[drep_metadata1-build-submit_cli]@smoke

image
[gw1] linux -- Python 3.13.11 /home/runner/work/cardano-node-tests/cardano-node-tests/run_workdir/.venv/bin/python3

self = <cardano_node_tests.tests.tests_conway.test_drep.TestDReps object at 0x7fffdd9c7d40>
cluster_manager = <cardano_node_tests.cluster_management.manager.ClusterManager object at 0x7fffd28c4cd0>
cluster = <ClusterLib: command_era=latest>
payment_addr = AddressRecord(address='addr_test1vp8c5ejp8dvnd4q5ltm6jre8vvl8q3jlyc5tpczdz5nz8ncjyl8t2', vkey_file=PosixPath('test_register_and_retire_drep[drep_metadata0-build_raw-submit_cli]_ci2_loyjkk_fund_addr_1.vkey'), skey_file=PosixPath('test_register_and_retire_drep[drep_metadata0-build_raw-submit_cli]_ci2_loyjkk_fund_addr_1.skey'))
build_method = <BuildMethods.BUILD: 'build'>, submit_method = 'cli'
drep_metadata = {'drep_metadata_file': PosixPath('/home/runner/work/cardano-node-tests/cardano-node-tests/cardano_node_tests/tests/data/drep_metadata_ipfs.json'), 'drep_metadata_url': 'https://tinyurl.com/drep-ipfs', 'expected_hash': '7b45535061dce55fca685fedfec6e41a195c4fe4ef812767c34505390b413ff3'}

    @allure.link(helpers.get_vcs_link())
    @submit_utils.PARAM_SUBMIT_METHOD
    @common.PARAM_BUILD_METHOD_NO_EST
    @pytest.mark.dbsync
    @pytest.mark.dbsync_config
    @pytest.mark.testnets
    @pytest.mark.smoke
    @pytest.mark.parametrize(
        "drep_metadata",
        (
            {
                "drep_metadata_file": DATA_DIR / "drep_metadata_url.json",
                "drep_metadata_url": "https://tinyurl.com/drep-url",
                "expected_hash": "18b4b10150eab04ba66c8f9cb497ff05c6c31b9c9825388481c1790ce76b6b90",
            },
            {
                "drep_metadata_file": DATA_DIR / "drep_metadata_ipfs.json",
                "drep_metadata_url": "https://tinyurl.com/drep-ipfs",
                "expected_hash": "7b45535061dce55fca685fedfec6e41a195c4fe4ef812767c34505390b413ff3",
            },
        ),
    )
    def test_register_and_retire_drep(
        self,
        cluster_manager: cluster_management.ClusterManager,
        cluster: clusterlib.ClusterLib,
        payment_addr: clusterlib.AddressRecord,
        build_method: str,
        submit_method: str,
        drep_metadata: dict,
    ):
        """Test DRep registration and retirement.
    
        * Register DRep
        * Check that DRep was registered
        * Retire DRep
        * Check that DRep was retired
        * Check that deposit was returned to source address
        """
        temp_template = common.get_test_id(cluster)
        deposit_drep_amt = cluster.g_query.get_drep_deposit()
    
        # Make sure there's enought funds on the payment address on long running
        # testnets where the DRep deposit is higher.
        if deposit_drep_amt >= MAINNET_DREP_DEPOSIT:
            clusterlib_utils.fund_from_faucet(
                payment_addr,
                cluster_obj=cluster,
                all_faucets=cluster_manager.cache.addrs_data,
                amount=deposit_drep_amt + 10_000_000,
            )
    
        errors_final = []
    
        drep_metadata_file = drep_metadata["drep_metadata_file"]
        drep_metadata_url = drep_metadata["drep_metadata_url"]
        expected_hash = drep_metadata["expected_hash"]
    
        # Register DRep
        reqc.cli012.start(url=helpers.get_vcs_link())
        drep_metadata_hash = cluster.g_governance.drep.get_metadata_hash(
            drep_metadata_file=drep_metadata_file
        )
        with open(drep_metadata_file, encoding="utf-8") as anchor_fp:
            drep_metadata_content = json.load(anchor_fp)
    
        _url = helpers.get_vcs_link()
        [r.start(url=_url) for r in (reqc.cli008, reqc.cli009, reqc.cli010, reqc.cip021)]
        reg_drep = governance_utils.get_drep_reg_record(
            cluster_obj=cluster,
            name_template=temp_template,
            drep_metadata_url=drep_metadata_url,
            drep_metadata_hash=drep_metadata_hash,
        )
        [r.success() for r in (reqc.cli008, reqc.cli009, reqc.cli010, reqc.cip021)]
    
        tx_files_reg = clusterlib.TxFiles(
            certificate_files=[reg_drep.registration_cert],
            signing_key_files=[payment_addr.skey_file, reg_drep.key_pair.skey_file],
        )
    
        tx_output_reg = clusterlib_utils.build_and_submit_tx(
            cluster_obj=cluster,
            name_template=f"{temp_template}_reg",
            src_address=payment_addr.address,
            submit_method=submit_method,
            build_method=build_method,
            tx_files=tx_files_reg,
            deposit=reg_drep.deposit,
        )
    
        reg_out_utxos = cluster.g_query.get_utxo(tx_raw_output=tx_output_reg)
        assert (
            clusterlib.filter_utxos(utxos=reg_out_utxos, address=payment_addr.address)[0].amount
            == clusterlib.calculate_utxos_balance(tx_output_reg.txins)
            - tx_output_reg.fee
            - reg_drep.deposit
        ), f"Incorrect balance for source address `{payment_addr.address}`"
    
        reqc.cli033.start(url=helpers.get_vcs_link())
        reg_drep_state = cluster.g_query.get_drep_state(drep_vkey_file=reg_drep.key_pair.vkey_file)
        assert reg_drep_state[0][0]["keyHash"] == reg_drep.drep_id, "DRep was not registered"
        reqc.cli033.success()
    
        metadata_anchor = reg_drep_state[0][1]["anchor"]
        assert metadata_anchor["dataHash"] == drep_metadata_hash == expected_hash, (
            "Unexpected metadata hash"
        )
        assert metadata_anchor["url"] == drep_metadata_url, "Unexpected metadata url"
    
        if configuration.HAS_DBSYNC:
            try:
                _url = helpers.get_vcs_link()
                [r.start(url=_url) for r in (reqc.db001, reqc.db006)]
                drep_data = dbsync_utils.check_drep_registration(
                    drep=reg_drep, drep_state=reg_drep_state
                )
                [r.success() for r in (reqc.db001, reqc.db006)]
    
                # The same metadata is used for all the parametrized tests. Therefore the data will
                # be present in db-sync once the first parametrized test runs. Therefore it doesn't
                # make sense to check the metadata for all combinations of parameters.
                if build_method == clusterlib_utils.BuildMethods.BUILD and submit_method == "cli":
    
                    def _query_func():
                        if not drep_data:
                            return
                        dbsync_utils.check_off_chain_drep_registration(
                            drep_data=drep_data, metadata=drep_metadata_content
                        )
    
                    dbsync_utils.retry_query(query_func=_query_func, timeout=360)
    
            except AssertionError as exc:
                str_exc = str(exc)
                errors_final.append(f"DB-Sync unexpected DRep registration error: {str_exc}")
    
        reqc.cli012.success()
    
        # Retire DRep
    
        _url = helpers.get_vcs_link()
        [r.start(url=_url) for r in (reqc.cli011, reqc.cip023)]
        ret_cert = cluster.g_governance.drep.gen_retirement_cert(
            cert_name=temp_template,
            deposit_amt=reg_drep.deposit,
            drep_vkey_file=reg_drep.key_pair.vkey_file,
        )
        [r.success() for r in (reqc.cli011, reqc.cip023)]
    
        tx_files_ret = clusterlib.TxFiles(
            certificate_files=[ret_cert],
            signing_key_files=[payment_addr.skey_file, reg_drep.key_pair.skey_file],
        )
    
        tx_output_ret = clusterlib_utils.build_and_submit_tx(
            cluster_obj=cluster,
            name_template=f"{temp_template}_ret",
            src_address=payment_addr.address,
            submit_method=submit_method,
            build_method=build_method,
            tx_files=tx_files_ret,
            deposit=-reg_drep.deposit,
        )
    
        reqc.cip024.start(url=helpers.get_vcs_link())
        ret_drep_state = cluster.g_query.get_drep_state(drep_vkey_file=reg_drep.key_pair.vkey_file)
        assert not ret_drep_state, "DRep was not retired"
        reqc.cip024.success()
    
        ret_out_utxos = cluster.g_query.get_utxo(tx_raw_output=tx_output_ret)
        assert (
            clusterlib.filter_utxos(utxos=ret_out_utxos, address=payment_addr.address)[0].amount
            == clusterlib.calculate_utxos_balance(tx_output_ret.txins)
            - tx_output_ret.fee
            + reg_drep.deposit
        ), f"Incorrect balance for source address `{payment_addr.address}`"
    
        try:
            dbsync_utils.check_drep_deregistration(drep=reg_drep)
        except AssertionError as exc:
            str_exc = str(exc)
            errors_final.append(f"DB-Sync unexpected DRep deregistration error: {str_exc}")
    
        if errors_final:
>           raise AssertionError("\n".join(errors_final))
E           AssertionError: DB-Sync unexpected DRep registration error: 'image_url' value is different than expected Base64 'contentUrl';

/home/runner/work/cardano-node-tests/cardano-node-tests/cardano_node_tests/tests/tests_conway/test_drep.py:502: AssertionError

------------------------------ Captured log call -------------------------------
WARNING  cardano_node_tests.utils.dbsync_utils:dbsync_utils.py:510 Sleeping 3s before repeating query for the 1 time.
WARNING  cardano_node_tests.utils.dbsync_utils:dbsync_utils.py:510 Sleeping 6s before repeating query for the 2 time.
WARNING  cardano_node_tests.utils.dbsync_utils:dbsync_utils.py:510 Sleeping 11s before repeating query for the 3 time.
WARNING  cardano_node_tests.utils.dbsync_utils:dbsync_utils.py:510 Sleeping 18s before repeating query for the 4 time.
WARNING  cardano_node_tests.utils.dbsync_utils:dbsync_utils.py:510 Sleeping 27s before repeating query for the 5 time.
WARNING  cardano_node_tests.utils.dbsync_utils:dbsync_utils.py:510 Sleeping 38s before repeating query for the 6 time.
WARNING  cardano_node_tests.utils.dbsync_utils:dbsync_utils.py:510 Sleeping 51s before repeating query for the 7 time.

Tests Report 💚
testrun-report.zip

@ArturWieczorek

ArturWieczorek commented Jun 16, 2026

Copy link
Copy Markdown
Contributor Author

Test Result 💚

CI

Run: artur/fix-drep-image-base64-data-uri-stripping

Local

  • image_url_prefix starts with data:image/png;base64, - the full URI is stored verbatim, prefix intact
  • image_hash is empty - expected for inline data URIs (no separate sha256)
  • data_hash matches the expected_hash from the test (7b45535061dce5...)
image
❯ pytest cardano_node_tests/tests/tests_conway/test_drep.py::TestDReps::test_register_and_retire_drep[drep_metadata1-build-submit_cli]
============================================================================================================== test session starts ==============================================================================================================
platform linux -- Python 3.13.3, pytest-9.0.3, pluggy-1.5.0 -- /home/artur/Projects/cardano-node-tests/.venv/bin/python
cachedir: .pytest_cache
hypothesis profile 'default'
metadata: {'Python': '3.13.3', 'Platform': 'Linux-6.8.0-124-generic-x86_64-with-glibc2.39', 'Packages': {'pytest': '9.0.3', 'pluggy': '1.5.0'}, 'Plugins': {'hypothesis': '6.148.7', 'allure-pytest': '2.15.2', 'metadata': '3.1.1', 'xdist': '3.8.0', 'order': '1.3.0', 'select': '0.1.2', 'html': '4.1.1'}, 'github actions url': '', 'cardano-node': '11.0.1', 'cardano-node rev': '390273284c76cc182cdd22b142e8a4df63edbcd3', 'cardano-node ghc': 'ghc-9.6', 'cardano-cli': '11.0.0.0', 'cardano-cli rev': '390273284c76cc182cdd22b142e8a4df63edbcd3', 'cardano-cli ghc': 'ghc-9.6', 'COMMAND_ERA': '', 'TESTNET_VARIANT': 'conway_fast', 'NUM_POOLS': '3', 'UTXO_BACKEND': '', 'MIXED_UTXO_BACKENDS': '', 'MAX_TESTS_PER_CLUSTER': 8, 'CLUSTERS_COUNT': 1, 'HAS_CC': 'True', 'cardano-node-tests rev': '275801e52aa3dd28077e40e12e1fc1de160d9f0a', 'cardano-node-tests url': 'https://github.com/IntersectMBO/cardano-node-tests/tree/275801e52aa3dd28077e40e12e1fc1de160d9f0a', 'CARDANO_NODE_SOCKET_PATH': '/var/tmp/cardonnay-of-artur/state-cluster0/bft1.socket', 'cardano-cli exe': '/nix/store/maax1flgc32fhn8cq611x5pkpi6ybnjv-cardano-cli-exe-cardano-cli-11.0.0.0/bin/cardano-cli', 'cardano-node exe': '/nix/store/yz8ks1as4z8vsiqa32gdnrfzpsdgsnyl-cardano-node-exe-cardano-node-11.0.1/bin/cardano-node', 'cardano-submit-api exe': '/nix/store/spcpa3p6kh00gj36c1ww59rgyfjnv2l2-cardano-submit-api-exe-cardano-submit-api-11.0.0/bin/cardano-submit-api', 'network magic': 42, 'HAS_DBSYNC': 'True', 'db-sync': '13.7.1.0', 'db-sync rev': '576efc987767a18ec4b3c81eccdb5307577e977b', 'db-sync ghc': 'ghc-9.6', 'db-sync exe': '/home/artur/.local/bin/cardano-db-sync', 'HAS_SMASH': 'False'}
rootdir: /home/artur/Projects/cardano-node-tests
configfile: pyproject.toml
plugins: hypothesis-6.148.7, allure-pytest-2.15.2, metadata-3.1.1, xdist-3.8.0, order-1.3.0, select-0.1.2, html-4.1.1
collected 1 item                                                                                                                                                                                                                                

cardano_node_tests/tests/tests_conway/test_drep.py::TestDReps::test_register_and_retire_drep[drep_metadata1-build-submit_cli]@smoke 
---------------------------------------------------------------------------------------------------------------- live log setup -----------------------------------------------------------------------------------------------------------------
INFO     cardano_node_tests.tests.conftest:conftest.py:225 Changed CWD to '/home/artur/Projects/cardano-node-tests/tmp/pytest-of-artur/pytest-0'.
----------------------------------------------------------------------------------------------------------------- live log call -----------------------------------------------------------------------------------------------------------------
WARNING  cardano_node_tests.utils.dbsync_utils:dbsync_utils.py:510 Sleeping 3s before repeating query for the 1 time.
WARNING  cardano_node_tests.utils.dbsync_utils:dbsync_utils.py:510 Sleeping 6s before repeating query for the 2 time.
WARNING  cardano_node_tests.utils.dbsync_utils:dbsync_utils.py:510 Sleeping 11s before repeating query for the 3 time.
WARNING  cardano_node_tests.utils.dbsync_utils:dbsync_utils.py:510 Sleeping 18s before repeating query for the 4 time.
WARNING  cardano_node_tests.utils.dbsync_utils:dbsync_utils.py:510 Sleeping 27s before repeating query for the 5 time.
WARNING  cardano_node_tests.utils.dbsync_utils:dbsync_utils.py:510 Sleeping 38s before repeating query for the 6 time.
WARNING  cardano_node_tests.utils.dbsync_utils:dbsync_utils.py:510 Sleeping 51s before repeating query for the 7 time.
WARNING  cardano_node_tests.utils.dbsync_utils:dbsync_utils.py:510 Sleeping 66s before repeating query for the 8 time.
PASSED                                                                                                                                                                                                                                    [100%]
--------------------------------------------------------------------------------------------------------------- live log teardown ---------------------------------------------------------------------------------------------------------------
INFO     cardano_node_tests.utils.dbsync_conn:dbsync_conn.py:35 Closing connection to db-sync database dbsync0.


========================================================================================================= 1 passed in 240.95s (0:04:00) =========================================================================================================



SQL Query

image
PGHOST=localhost PGPORT=55432 PGUSER=postgres psql -d dbsync0 -P pager=off -c " SELECT
      left(drep.image_url, 50) AS image_url_prefix,
      drep.image_hash,
      drep.given_name,
      va.url,
      va.data_hash
  FROM off_chain_vote_drep_data AS drep
  INNER JOIN off_chain_vote_data AS vd ON vd.id = drep.off_chain_vote_data_id
  INNER JOIN voting_anchor va ON vd.voting_anchor_id = va.id
  WHERE va.url = 'https://tinyurl.com/drep-ipfs';"
                  image_url_prefix                  | image_hash |   given_name    |              url              |                             data_hash                              
----------------------------------------------------+------------+-----------------+-------------------------------+--------------------------------------------------------------------
 data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgA |            | Artur Wieczorek | https://tinyurl.com/drep-ipfs | \x7b45535061dce55fca685fedfec6e41a195c4fe4ef812767c34505390b413ff3
(1 row)

Full SQL Query

SELECT
    drep.image_url,
    drep.image_hash,
    drep.given_name,
    va.url,
    va.data_hash
FROM off_chain_vote_drep_data AS drep
INNER JOIN off_chain_vote_data AS vd ON vd.id = drep.off_chain_vote_data_id
INNER JOIN voting_anchor va ON vd.voting_anchor_id = va.id
WHERE va.url = 'https://tinyurl.com/drep-ipfs';

@ArturWieczorek ArturWieczorek marked this pull request as ready for review June 16, 2026 09:11
@ArturWieczorek ArturWieczorek requested a review from a team as a code owner June 16, 2026 09:11
@kderme

kderme commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

My concern is how this affect tools that rely on current design or the mismatch between old nad new values.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants