A Solana Program Change Can Be Compatible with One Consumer but Not Another

Testing Provider Changes Against Consumer-Declared Requirements


Two compiled implementations accepted the same CPI. Both transactions and both CPIs succeeded. One left post-state 3; the other left 6.

That result is not yet a compatibility verdict. It becomes one only after the post-state is compared with a consumer’s declared requirement. In these local LiteSVM fixtures, Caller A requires an exact delta and Caller C requires only an increase. The candidate violates A’s requirement and satisfies C’s.

So the useful question is not simply whether a replacement runs. It is: compatible for which consumer?

Here, “compatible” means that observed behavior conforms to a requirement declared for that consumer and invocation.

For consumer C_i, provider implementation B_t, invocation context x, and declared requirement R_i, this prototype evaluates:

[ \operatorname{conforms}(C_i, B_t, x, R_i) = R_i(\operatorname{observe}(C_i \to B_t, x)) ]

The verdict therefore belongs to the consumer-provider dependency under that context, not to the provider binary alone.

This follows Testing Compatibility Across Solana Program Changes, which showed that successful execution can miss one caller-level postcondition. This article asks what changes when consumers rely on different requirements.

Two separate local runs use the same designated Program ID B. Baseline B0 leaves state 3 and candidate B1 leaves state 6; Caller A's exact-delta contract fails on B1 while Caller C's increase contract passes.
Fig. 1. One provider change, different consumer outcomes. B0 computes before + delta; B1 computes before + 2 × delta at the same designated Program ID in separate fresh local runs. Both CPIs succeed. Exact delta fails on B1; a monotonic increase passes.

One address, two requirements

The counter uses two compiled callers and two compiled callees. B0 writes after = before + delta; B1 writes after = before + 2 × delta. For each comparison, the harness holds the caller binary, caller Program ID, callee Program ID, state address, initial state bytes, instruction bytes, and CPI privileges fixed. The changing control is the callee implementation installed at the designated address.

With before = 0 and delta = 3, B0 leaves 3 and B1 leaves 6. Caller A declares after == before + delta. Caller C declares after > before. Four top-level transactions succeed; the harness also classifies four successful CPIs from LiteSVM logs. Across the two successful candidate runs, the provider leaves the same relevant post-state, while A and C apply different declared requirements. A and C are separate caller fixtures. Provider-side invocation controls are held fixed within each comparison.

That separates three facts that are easy to collapse:

  • Reachable: the designated provider is present at the expected Program ID and the CPI can reach it.
  • Executable: the transaction succeeds and the CPI completes.
  • Compatible for a consumer: the observed behavior satisfies that consumer’s declared requirement for this invocation.

In this harness, reachable and executable are prerequisites for evaluating the third. That prevents a successful transaction from being treated as its own compatibility result.

Animated technical diagram on one persistent canvas: Caller A and Caller C reach Program ID B in separate baseline and candidate runs. Successful CPI leaves post-state 3 then 6; Caller A's exact-plus-three requirement flips to failure while Caller C's increase requirement remains satisfied. The lower contract lens also shows reservation and bounded-cost examples.
Fig. 2. The execution canvas stays fixed while the implementation, observed post-state, and consumer judgment change. A and C share the comparison canvas; each caller-provider pair is executed separately. Values change only at the observed states; the runs are separate controlled installs, not a loader upgrade. Static fallback: SVG.

Two application-shaped witnesses

The counter is deliberately small. The same evaluation pattern is easier to use when the requirement has an application-shaped meaning.

Reservation: exact amount versus sufficient capacity

For a request of 100, the baseline leaves provider reservation 100; the candidate leaves 200. Caller A declares that the result must match the requested amount exactly. Caller C declares only that it must be at least the requested amount. The candidate therefore fails A’s accounting requirement and passes C’s capacity requirement.

This fixture uses before = 0 and delta = 100. AtLeastRequested is before-relative: after >= before + delta, so here it means an increase of at least 100. The 100 is a declared expectation, not caller-owned persisted state. This fixture compares that expectation with provider state; it does not model caller-owned accounting.

Baseline reservation of 100 satisfies both an exact-accounting and a minimum-capacity contract. Candidate reservation of 200 fails the exact contract and satisfies the minimum contract.
Fig. 3. Expected reservation versus provider reservation. The baseline leaves 100 and the candidate leaves 200. The 100 is an external contract expectation, not a value held in a caller-owned account.

Bounded cost: one realized value, three decisions

The cost witness uses a different compiled candidate from the counter and reservation B1. The baseline realizes 100 for a quote of 100; the surcharge candidate realizes 103. Instruction shape, state width, Program ID, and CPI path stay fixed for the witness. Only the candidate arithmetic changes.

Caller A accepts exactly 100. Caller C accepts values no greater than 105. Caller D accepts any positive value. For this quote, 103 is outside A’s declared region and inside C’s and D’s. Transaction and CPI succeed in all six runs.

A realized cost of 103 sits outside Caller A's exact-100 acceptance region and inside Caller C's at-most-105 and Caller D's positive-value regions.
Fig. 4. One realized value, multiple declared acceptance regions. The drawing is schematic, not a measured scale.

For a fixed call, each declared requirement defines a region of acceptable observed behavior. At 103, the surcharge candidate is outside A’s region and inside C’s and D’s. Change the starting state or quote and those regions may change. The relevant context includes the requested amount, quote, and starting state. These regions are external test requirements, not constraints enforced by Solana. This is declared-contract conformance: observed behavior meets the external requirement supplied for that consumer and invocation.


From an observation to a contract outcome

The prototype keeps the semantic judgment small. Once a compiled CPI produces an observation, the evaluator checks that the contract names the same consumer-provider dependency and that execution succeeded. It then applies the declared postcondition.

pub fn evaluate_contract(
    contract: &DependencyContract,
    observation: &DependencyObservation,
) -> ContractOutcome {
    if contract.consumer != observation.consumer || contract.dependency != observation.dependency {
        return ContractOutcome::Mismatch;
    }
    if !observation.tx_success || !observation.cpi_success {
        return ContractOutcome::NotEvaluated;
    }
    let holds = match contract.postcondition {
        Postcondition::ExactDelta => observation.after == observation.before + observation.delta,
        Postcondition::MonotonicIncrease => observation.after > observation.before,
        Postcondition::AtLeastRequested => {
            observation.after >= observation.before + observation.delta
        }
        Postcondition::MaximumValue { max } => observation.after <= max,
        Postcondition::Positive => observation.after > 0,
    };
    if holds {
        ContractOutcome::Pass
    } else {
        ContractOutcome::Fail
    }
}

MaximumValue { max } is an absolute cap on observed after; in the bounded-cost fixture, max = 105.

For the counter candidate, before = 0, delta = 3, and after = 6. ExactDelta rejects the observation, while MonotonicIncrease accepts it. The observed value is the same across the two candidate runs; the declared predicate differs.

That is declared-contract conformance, not proof that the declaration captures every behavior the consumer actually relies on.

A consumer-associated contract.toml names the consumer, dependency, and postcondition. It is an external test specification, not a Solana account or a runtime-enforced contract.

A separate mutation harness also exercises controlled structural, wire, state, and semantic failures; that evidence is supporting context rather than the main experiment here.

These fresh local LiteSVM fixtures with pinned SBF artifacts establish a controlled counterexample and an executable testing pattern, not prevalence, production error rates, or behavior under a deployed loader upgrade.

The current prototype evaluates scalar postconditions over one recorded observation. It does not yet express multi-account, relational, or temporal dependency requirements.


Closing

A candidate substitution behind the same Program ID can preserve successful CPI execution while violating a consumer’s declared requirement. The useful pre-change question is therefore not only whether the candidate runs, but which tested consumer-provider requirements still hold under the candidate.

This is demonstrated here in controlled local fixtures.