Rendering and transformation caching

Caching is an opt-in core capability. The core artifact owns the cache keys, bounded in-process storage, single-flight loading, and the rendering and transformation facades. The web module only resolves HTTP parameters, binds configuration, creates the core facades, and serves their results.

This distinction matters for consuming applications:

Integration Cache behavior
Call representable-contract-state-web over HTTP Requests use that web-service instance's cache.
Embed net.finmath:representable-contract-state Construct and share the core caching facades in the consuming process.

An embedded application should not add representable-contract-state-web as a library dependency merely to obtain caching. The web artifact is an executable Spring Boot application.

Core dependency and setup

Depend on the core artifact:

<dependency>
    <groupId>net.finmath</groupId>
    <artifactId>representable-contract-state</artifactId>
    <version>${representable-contract-state.version}</version>
</dependency>

Create one shared cached facade for each renderer configuration:

import net.finmath.smartcontracts.representablestate.cache.CachedRenderingService;
import net.finmath.smartcontracts.representablestate.cache.CachedXsltTransformationService;
import net.finmath.smartcontracts.representablestate.cache.EvmSnapshot;
import net.finmath.smartcontracts.representablestate.cache.EvmSnapshotVerifier;
import net.finmath.smartcontracts.representablestate.cache.RendererCacheOptions;
import net.finmath.smartcontracts.representablestate.postprocessing.ExternalXsltResourceLoader;
import net.finmath.smartcontracts.representablestate.xml.EvmXmlRenderer;
import net.finmath.smartcontracts.representablestate.xml.XsltPostProcessor;
import net.finmath.smartcontracts.representablestate.xml.XsltPostProcessor.TransformationResult;

import java.time.Duration;

RendererCacheOptions options = RendererCacheOptions.defaults();
EvmXmlRenderer rawRenderer = new EvmXmlRenderer(rpcUrl);
CachedRenderingService renderings = new CachedRenderingService(
        rawRenderer,
        options
);

EvmSnapshot snapshot = new EvmSnapshot(chainId, blockNumber, blockHash);
String stateXml = renderings.render(snapshot, contractAddress);
String partXml = renderings.renderPart(snapshot, contractAddress, partId);
String template = renderings.renderTemplate(
        snapshot,
        contractAddress,
        true
);

The raw EvmXmlRenderer remains available when caching is not wanted. Creating a new cached facade for each request defeats reuse; expose a singleton or equivalent application-scoped instance instead.

To cache a complete render-plus-XSLT operation, use a second facade:

XsltPostProcessor postProcessor = new XsltPostProcessor();
ExternalXsltResourceLoader resourceLoader = new ExternalXsltResourceLoader(
        web3j,
        ExternalXsltResourceLoader.Options.defaults()
);

CachedXsltTransformationService transformations =
        new CachedXsltTransformationService(
                renderings,
                postProcessor,
                resourceLoader,
                options
        );

TransformationResult document = transformations.transform(
        snapshot,
        contractAddress,
        xsltSpecification
);

The rendering and transformation caches are separate. The configured maximum weight applies to each cache, so two caches configured for 64 MiB may retain approximately 128 MiB plus implementation overhead.

Snapshot and key semantics

Resolve latest exactly once before calling a cached facade. EvmSnapshot requires:

  • the RPC provider's chain ID;
  • a non-negative block number; and
  • that block's 32-byte hash.

The block hash prevents a document rendered before a chain reorganization from being reused for a different block at the same height. Address matching is case-insensitive. Rendering keys also distinguish complete state, partId, and template XInclude resolution mode. Transformation keys add the normalized XSLT specification.

EvmXmlRenderer currently executes eth_call by block number. Applications that require strong canonicality guarantees should therefore verify the block hash after a newly computed rendering, as a reorganization can occur between snapshot resolution and the RPC calls. Supply that check directly to the core facades:

EvmSnapshotVerifier snapshotVerifier = snapshot -> {
    // Re-read snapshot.blockNumber() and reject a different block hash.
};

CachedRenderingService verifiedRenderings = new CachedRenderingService(
        rawRenderer,
        options,
        snapshotVerifier
);

The verifier runs after a successful computation and before its result is returned or cached. CachedXsltTransformationService has an equivalent five-argument constructor and verifies after the complete transformation, including external stylesheet retrieval. The web application supplies its Web3j-backed verifier automatically and returns 409 Conflict when the block changes during generation.

A cache hit is identified by the full snapshot hash and does not run the post-computation verifier again. Web requests still resolve the current block hash before every lookup. Embedded callers using the two-argument constructor must retain their own canonicality checks. Post-verification narrows the reorg window but does not replace the caller's finality policy or hash-pinned EIP-1898 RPC support.

Expiry and external stylesheets

When caching is enabled, successful values are cached. Exceptions and null results are not retained, and concurrent calls for the same key share one computation. Entries are local to the process and disappear on restart or upgrade. clear() explicitly removes the entries owned by a facade. Disabled facades execute and verify every call directly without retaining or coalescing work.

The default options are:

enabled:                  true
maximum weight per cache: 64 MiB
expire after write:       10 minutes

Use explicit options when the host application needs different bounds:

RendererCacheOptions options = new RendererCacheOptions(
        16L * 1024L * 1024L,
        Duration.ofMinutes(5)
);

Inline and block-pinned web3:// stylesheets are tied to the source snapshot. An HTTP(S) URL can return different content without changing the transformation key. Its freshness is therefore bounded only by cache expiry. Use a suitably short expiry, immutable content-addressed URLs, or disable that integration when this is not acceptable.

Web-service configuration

representable-contract-state-web creates the core facades automatically. Its defaults can be overridden with Spring configuration:

renderer.cache.enabled=true
renderer.cache.maximum-weight=64MB
renderer.cache.expire-after-write=10m

Set renderer.cache.enabled=false to bypass both caches without changing the HTTP endpoints or their snapshot verification. Every request is then rendered, transformed, and verified directly. This provides an operational rollback for mutable resources or cache-related incidents.

This is server-side computation caching. The transform and text-resource endpoints continue to send Cache-Control: no-store; browser and proxy caching is a separate policy.

Smart Bond handover

Smart Bond currently embeds the core renderer rather than calling the web service. Its integration should therefore:

  1. Create one shared CachedRenderingService for each RPC/renderer configuration instead of constructing EvmXmlRenderer in each service.

  2. Map its existing snapshot directly to core identity:

    EvmSnapshot cacheSnapshot = new EvmSnapshot(
            snapshot.chainId(),
            snapshot.blockNumber(),
            snapshot.blockHash()
    );
    
  3. Adapt Smart Bond's existing post-render canonicality check to EvmSnapshotVerifier and pass it to the three-argument constructor. This keeps a reorganization failure from admitting the rejected XML to the cache; retain any additional pre-render and finality checks Smart Bond already performs.

  4. Put Smart Bond-specific validation, product-term extraction, and lifecycle rules above the generic cached renderer.

  5. Use distinct facades for differently configured data-retrieval and event-listening RPC connections.

  6. Test cache misses across chain ID, block number, block hash, contract, kind, partId, and transformation input.

Caching Smart Bond's richer final RenderedState can be added at its application boundary if profiling shows value. That domain cache is separate from the generic XML/XSLT cache supplied here.

The optional on-chain stateVersion() extension is a different, cross-block optimization. It is not required to reuse an exact fixed-snapshot rendering.