<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Dmytro Nasyrov</title>
    <description>The latest articles on DEV Community by Dmytro Nasyrov (@dmytronasyrov).</description>
    <link>https://dev.to/dmytronasyrov</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F140475%2F9927d920-3e1f-416a-bede-35b832d27c5c.png</url>
      <title>DEV Community: Dmytro Nasyrov</title>
      <link>https://dev.to/dmytronasyrov</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dmytronasyrov"/>
    <language>en</language>
    <item>
      <title>ERC-3643 vs ERC-1400: How to Choose a Standard for Tokenized Instruments</title>
      <dc:creator>Dmytro Nasyrov</dc:creator>
      <pubDate>Wed, 07 Oct 2026 06:16:16 +0000</pubDate>
      <link>https://dev.to/pharos_production/erc-3643-vs-erc-1400-how-to-choose-a-standard-for-tokenized-instruments-67e</link>
      <guid>https://dev.to/pharos_production/erc-3643-vs-erc-1400-how-to-choose-a-standard-for-tokenized-instruments-67e</guid>
      <description>&lt;p&gt;Choose ERC-3643 when the central requirement is that a token can move only to an eligible investor represented through an on-chain identity system. Evaluate an ERC-1400 implementation when the instrument needs explicit subdivisions of a holder's balance, partition-specific operations or the interfaces in that standards family. If the product needs both, neither label finishes the architecture: select an implementation and prove how its identity and partition policies interact.&lt;/p&gt;

&lt;p&gt;The difficult question is what a particular transaction must preserve. A wallet can hold enough tokens and still be unable to transfer the requested amount. A registered investor can lose eligibility after a transfer preview. A transfer agent can have authority to correct ownership while ordinary transfers remain paused. These are different conditions, with different owners and failure paths. In the example below, 150 units cannot fund a 120-unit transfer when only 100 are transferable.&lt;/p&gt;

&lt;p&gt;For &lt;a href="https://pharosproduction.com" rel="noopener noreferrer"&gt;blockchain engineering at Pharos Production&lt;/a&gt;, the useful starting point is a behavior specification connecting an instrument requirement to an observable transaction outcome. Ask for that artifact before comparing feature counts. An issuer choosing a token standard needs to know which behavior is supplied, which depends on configuration and which needs new engineering.&lt;/p&gt;

&lt;p&gt;This comparison uses the published ERC-3643 specification, the original ERC-1400 family documents and two pinned public implementations. Sources were checked on October 7, 2026. It makes architecture recommendations from those interfaces and source paths; it does not report a production deployment or a comparative security audit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with the instrument, not the token number
&lt;/h2&gt;

&lt;p&gt;Write the instrument's rights in language a fund administrator and an engineer can both inspect. Does every unit carry the same economic rights? Can two units in the same wallet have different transfer restrictions? Who decides that a subscription is complete, that a holding period has expired or that ownership needs correction?&lt;/p&gt;

&lt;p&gt;Consider a fund share with one class of economic rights. Investors must be approved before receiving it. Transfers have a concentration limit. Lost-key recovery is an operational requirement. Here, investor identity and transfer controls are the primary engineering problem. ERC-3643 is a natural candidate because its interfaces directly describe the identity registries and administrative operations that this workflow needs.&lt;/p&gt;

&lt;p&gt;Now consider an instrument whose units are divided into issuance lots with different release conditions. A holder can own both transferable and restricted units. An intermediary must specify which lot it is moving and retain that information in its records. Partition-aware operations deserve a place in the evaluation. Whether those partitions represent lots, lockups or something else is an issuer-approved design decision.&lt;/p&gt;

&lt;p&gt;Different economic rights need further scrutiny. Separate share classes might require separate contracts, distinct accounting or a carefully specified partition model. The ability to assign a partition key does not establish the legal meaning of that key. Nor does a single ERC-20 balance demonstrate that all the units it aggregates are interchangeable for settlement.&lt;/p&gt;

&lt;p&gt;The selection worksheet should therefore contain four fields: the unit's rights, the state that restricts movement, the actor authorized to change that state and the evidence retained after a change. A standard is useful when its interfaces fit these requirements without hiding a material distinction in an application database.&lt;/p&gt;

&lt;h2&gt;
  
  
  Compare a final specification with a standards family
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://eips.ethereum.org/EIPS/eip-3643" rel="noopener noreferrer"&gt;ERC-3643&lt;/a&gt; is marked Final in the Ethereum Improvement Proposals repository. It defines token, identity registry, identity storage, compliance, trusted issuer registry and claim topic registry interfaces. It requires ERC-20 compatibility and an on-chain identity system. It also describes recovery, freezes, pause, minting, burning and agent authority.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/SecurityTokenStandard/EIP-Spec/blob/8b082caba8adb4f8e351aa6acdd3e9e394e09dea/eip/eip-1400.md" rel="noopener noreferrer"&gt;original ERC-1400 document&lt;/a&gt; describes an umbrella of related interfaces. Its retained front matter says Draft. One of its authors is Fabian Vogelsteller, also associated with the development of Ethereum token interfaces. The authors describe its scope in one short sentence:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Represents a library of standards for security tokens on Ethereum.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That sentence matters in procurement. ERC-1400 on a slide does not identify the exact interface set, restriction system or administrative behavior delivered by a vendor. Ask which members of the family are implemented and which extensions are proprietary.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Family document&lt;/th&gt;
&lt;th&gt;Engineering job&lt;/th&gt;
&lt;th&gt;What still needs a design decision&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ERC-1410&lt;/td&gt;
&lt;td&gt;Partition balances and partition operations&lt;/td&gt;
&lt;td&gt;Meaning of a partition and permitted transitions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ERC-1594&lt;/td&gt;
&lt;td&gt;Transfer validity queries, attached data, issuance and redemption&lt;/td&gt;
&lt;td&gt;Policy source, authorization data and failure handling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ERC-1643&lt;/td&gt;
&lt;td&gt;Document references and updates&lt;/td&gt;
&lt;td&gt;Document governance, storage and investor access&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ERC-1644&lt;/td&gt;
&lt;td&gt;Declared controller operations&lt;/td&gt;
&lt;td&gt;Controller authorization and correction procedure&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A Final status is a specification maturity signal. It is not evidence that a deployed configuration has been audited, that an upgrade is safe or that a venue supports the token. Conversely, a draft-family implementation may have useful production-oriented features. Evaluate the repository, deployment and operating model separately from the status of the document.&lt;/p&gt;

&lt;h2&gt;
  
  
  ERC-3643 makes investor eligibility an explicit dependency
&lt;/h2&gt;

&lt;p&gt;ERC-3643 connects a receiving wallet to an identity record and checks the required claims against trusted issuers. The claim topics define what must be established; the trusted issuer registry defines whose attestations are accepted. The token's compliance component handles offering-level rules, such as concentration or holder-count restrictions, separately from identity verification.&lt;/p&gt;

&lt;p&gt;This separation helps diagnose a rejected transfer. The recipient might be unregistered, lack a required claim or fail a token-level rule despite having a valid identity. A frozen wallet or insufficient unfrozen balance is another reason to reject movement. A front end that collapses every case into an eligibility error sends the operator to the wrong system.&lt;/p&gt;

&lt;p&gt;The specification's ordinary transfer conditions include recipient verification, wallet freeze checks, free balance, pause state and the compliance result. Do not expand that into a promise that every entry point checks every condition. Administrative operations have different semantics, and the selected implementation must be inspected individually.&lt;/p&gt;

&lt;p&gt;The eligibility owner needs a response procedure as well as contract permissions. If an onboarding provider stops serving an attestation, identify who investigates, who may replace the evidence and how pending instructions are handled. A registry administrator should not make an undocumented exception simply to clear a settlement queue. Keep the rejected instruction linked to its policy evidence so that a subsequent approved retry can be explained.&lt;/p&gt;

&lt;p&gt;Identity freshness also needs an operating policy. Removing an accepted issuer, revoking an attestation or changing a claim requirement can affect future receipts. It does not necessarily remove an existing balance. Decide whether an investor who becomes ineligible may continue holding, redeem, transfer out through a restricted route or require an administrative action. Then encode and test the approved behavior.&lt;/p&gt;

&lt;p&gt;A wallet-to-identity relationship also needs a custody model. For an omnibus custodian, the contract may identify the custodian wallet while the beneficial-owner allocation remains in its ledger. That is a different visibility boundary from individually registered investor wallets. Neither model becomes correct merely because a registry call returns true.&lt;/p&gt;

&lt;p&gt;Avoid putting sensitive onboarding documents into public transaction data to simplify integration. The token system needs verifiable eligibility evidence and controlled links to the off-chain record. The location of personal information, access permissions and correction process should be explicit parts of the identity design.&lt;/p&gt;

&lt;h2&gt;
  
  
  ERC-1400 partitions expose which balance is moving
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/SecurityTokenStandard/EIP-Spec/blob/8b082caba8adb4f8e351aa6acdd3e9e394e09dea/eip/eip-1410.md" rel="noopener noreferrer"&gt;ERC-1410&lt;/a&gt; describes balances grouped under partition keys while retaining aggregate owner balances and supply information. Operations can address a particular partition. This gives an integration a way to distinguish parts of a holding that would disappear into a single aggregate balance.&lt;/p&gt;

&lt;p&gt;A partition is a label and associated balance in the interface. Its restrictions come from the contract's policy. Naming a partition LOCKED does not itself create a time lock. A partition named AVAILABLE does not prove that the recipient is eligible. The implementation must enforce the corresponding conditions through its transfer path and extensions.&lt;/p&gt;

&lt;p&gt;The pinned &lt;a href="https://github.com/Consensys/UniversalToken/tree/54320c6f7a8ee1fd7fcb19073e9c122e1e8f96f9" rel="noopener noreferrer"&gt;Consensys UniversalToken implementation&lt;/a&gt; exposes partition transfers and operator transfers. Its transfer path invokes extensions and maintains partition balances. Its README discusses both signed transfer certificates and on-chain lists of validated investors. These are concrete policy mechanisms in that implementation, not proof that every ERC-1400 token uses the same identity architecture.&lt;/p&gt;

&lt;p&gt;A certificate-based design can attach authorization to an intended transaction. It introduces an issuer or policy service that must remain available and issue the correct authorization. A list-based design moves an eligibility lookup on-chain but needs transactions to keep that state current. An engineering choice between them must account for revocation latency, replay protection and the integration's ability to supply the required data.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl63emu5ro1vc5c8nezkz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl63emu5ro1vc5c8nezkz.png" alt="ERC-3643 organizes eligibility through identity and compliance dependencies; an ERC-1400 implementation can organize balances by partition and enforce policy through transfer extensions. Both require an explicit authority model." width="800" height="560"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Partition behavior also reaches the user interface. A wallet displaying 150 units should distinguish transferable units from restricted units when that difference matters. Custody reports need the partition balances, and a settlement instruction needs an unambiguous source partition. Otherwise the application promises liquidity using an aggregate number that the transfer path cannot spend.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use disqualifiers before assigning scores
&lt;/h2&gt;

&lt;p&gt;A weighted feature score can hide a missing requirement. A candidate with a polished dashboard might score well while lacking the operation an approved instrument needs. Set the rejection conditions first, then compare engineering and operational costs among the remaining candidates.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Requirement&lt;/th&gt;
&lt;th&gt;ERC-3643 candidate&lt;/th&gt;
&lt;th&gt;ERC-1400 candidate&lt;/th&gt;
&lt;th&gt;Reject the candidate when&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Reusable investor eligibility&lt;/td&gt;
&lt;td&gt;Explicit identity and claim registries&lt;/td&gt;
&lt;td&gt;Inspect the chosen list, certificate or identity extension&lt;/td&gt;
&lt;td&gt;Eligibility updates cannot be explained or tested&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Different restrictions inside one holding&lt;/td&gt;
&lt;td&gt;Additional lot or state model may be needed&lt;/td&gt;
&lt;td&gt;Evaluate partition-aware operations&lt;/td&gt;
&lt;td&gt;Required distinctions exist only in a display label&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Explain a refused transfer&lt;/td&gt;
&lt;td&gt;Combine identity, compliance and token-state diagnostics&lt;/td&gt;
&lt;td&gt;Inspect validity queries and implementation reason codes&lt;/td&gt;
&lt;td&gt;Preview contradicts execution without a diagnosable change&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Administrative correction&lt;/td&gt;
&lt;td&gt;Agent and recovery operations are specified&lt;/td&gt;
&lt;td&gt;Inspect controller and operator capabilities&lt;/td&gt;
&lt;td&gt;Authority or bypass behavior is undocumented&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Documents tied to an instrument&lt;/td&gt;
&lt;td&gt;Define the document integration separately&lt;/td&gt;
&lt;td&gt;Evaluate the ERC-1643 interface and its implementation&lt;/td&gt;
&lt;td&gt;Updated terms cannot be retrieved and reconciled&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Existing custody or venue integration&lt;/td&gt;
&lt;td&gt;Prove restricted ERC-20 operations work&lt;/td&gt;
&lt;td&gt;Prove aggregate and partition operations work&lt;/td&gt;
&lt;td&gt;Required calls or events are unsupported&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Upgrades and policy changes&lt;/td&gt;
&lt;td&gt;Inspect registries, modules and deployment authority&lt;/td&gt;
&lt;td&gt;Inspect extensions, partition semantics and deployment authority&lt;/td&gt;
&lt;td&gt;A change can silently reinterpret outstanding holdings&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Pharos Production's &lt;a href="https://pharosproduction.com/services/tokenization-and-rwa-development/" rel="noopener noreferrer"&gt;tokenization and RWA development process&lt;/a&gt; describes working alongside legal counsel to map asset ownership rights to token mechanics. That mapping addresses the central selection problem: engineering needs approved rights and restrictions before choosing a contract model. Ask for a requirement-to-operation matrix as the handoff, with legal assumptions distinguished from demonstrated software behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  Walk one holding through both models
&lt;/h2&gt;

&lt;p&gt;Use an illustrative holding of 150 units: 100 currently transferable and 50 restricted. The holder requests a transfer of 120 units to an eligible recipient. Assume the token is not paused and both wallets satisfy the applicable conditions. These numbers are a design example, not measurements from a deployed system.&lt;/p&gt;

&lt;p&gt;An ERC-3643 design could represent the restriction as 50 partially frozen tokens. The ordinary free balance would be 100, so the request should fail even though the aggregate balance is 150. Whether partial freezing is the right representation depends on the reason for the restriction and who may release it. Freezing is an administrative mechanism; it does not automatically implement the instrument's contractual release schedule.&lt;/p&gt;

&lt;p&gt;An ERC-1400 design could represent 100 units in an available partition and 50 in a restricted partition. A request for 120 from the available partition should fail because that partition contains only 100. If the aggregate ERC-20 transfer route can consume multiple partitions, its default selection order and restriction checks must be part of the design. Do not assume the aggregate route behaves like the partition-specific route.&lt;/p&gt;

&lt;p&gt;Reduce the request to 40. After a successful transfer that preserves the partition, the sender would have 60 available and 50 restricted; the recipient would receive 40 available. In the partial-freeze model, the sender would have 110 total and 50 frozen, leaving 60 free. Those arithmetic outcomes look similar, but the retained information differs: partition identifiers can carry lot distinctions that one frozen subtotal cannot express by itself.&lt;/p&gt;

&lt;p&gt;Now revoke the recipient's eligibility after preview but before execution. Execution must use the applicable current policy; a successful earlier query is not a reservation of eligibility. If the transaction is rejected, balances and associated accounting must remain consistent. If the approved model permits the movement, the record needs to explain which policy and authorization allowed it.&lt;/p&gt;

&lt;p&gt;Finally, let the restriction expire. In the freeze model, determine how the frozen quantity is released. In the partition model, determine whether units remain in the same partition with a changed rule or move to another partition. A wall-clock date in a database cannot substitute for an on-chain transition when the contract still blocks the transfer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preflight is a snapshot, not a settlement promise
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/SecurityTokenStandard/EIP-Spec/blob/8b082caba8adb4f8e351aa6acdd3e9e394e09dea/eip/eip-1594.md" rel="noopener noreferrer"&gt;ERC-1594&lt;/a&gt; describes transfer validity queries with error signalling and attached data. Use the exact return values of the implemented interface to build an actionable response. A recipient rejection should lead to an eligibility investigation; a balance rejection should lead to a position check. A failed authorization certificate requires a different operator than either of those.&lt;/p&gt;

&lt;p&gt;For ERC-3643, inspecting the compliance result alone leaves out other token conditions. The application needs the relevant identity, freeze, pause and balance checks, with a clear warning when its diagnostic view is incomplete. A callback or external dependency can also fail during execution even if an earlier policy query succeeded.&lt;/p&gt;

&lt;p&gt;Store the block identifier used for preview and the instruction parameters. If execution fails later, compare the state and policy changes between the preview and execution blocks. Recomputing against the latest state without preserving the original observation can conceal why the user saw an apparently valid instruction. The user-facing result should name the rejected operation and the next permitted action, without promising that retrying will succeed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Administrative paths need their own contract
&lt;/h2&gt;

&lt;p&gt;Forced transfers, wallet recovery and redemption are part of the operating model. They must have an authority policy that explains who can invoke them and what ordinary restrictions they bypass. A multisig threshold alone cannot explain whether its signers are allowed to perform a particular correction.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/TokenySolutions/T-REX/blob/f1390127d4270e7f31764c068caf0a1526b852d4/contracts/token/Token.sol" rel="noopener noreferrer"&gt;pinned T-REX token source&lt;/a&gt; illustrates why path-specific review matters. Its forced transfer requires agent authority and verifies the recipient. It can release enough frozen tokens to complete the movement and does not call the ordinary compliance pre-check. It does call the post-transfer compliance callback. Those details matter to a module that maintains holder counts or position limits.&lt;/p&gt;

&lt;p&gt;The same source's mint path checks recipient verification and the compliance pre-check. This differs from the published specification's explanatory description of mint bypassing compliance rules. Treat the pinned code as the behavior being evaluated and record the discrepancy. A standards summary is not a substitute for examining the deployed entry point.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/SecurityTokenStandard/EIP-Spec/blob/8b082caba8adb4f8e351aa6acdd3e9e394e09dea/eip/eip-1644.md" rel="noopener noreferrer"&gt;ERC-1644&lt;/a&gt; describes controller operations and transparency about whether unilateral transfers are possible. A candidate implementation must still demonstrate its exact controller configuration, events and interaction with extensions. Never infer that a controller has the same permissions as an ERC-3643 agent.&lt;/p&gt;

&lt;p&gt;For each exceptional path, retain the business authorization, transaction receipt and before-and-after ownership state. A correction transfers ownership in the current ledger; it does not erase the historical transaction. Recovery should preserve the instrument's supply and relevant restrictions while making the old wallet's future role explicit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Integration includes settlement and document state
&lt;/h2&gt;

&lt;p&gt;ERC-20 compatibility provides familiar calls such as balance and allowance queries. It does not mean that an arbitrary exchange, bridge or escrow contract is an eligible holder. A settlement contract can satisfy the ABI and still fail a recipient eligibility check. Partition-aware integrations additionally need to preserve the intended partition through the complete settlement route.&lt;/p&gt;

&lt;p&gt;Test with the actual custody and venue workflow. Does the intermediary support attached transfer data? Can it authorize an operator? Does it display a failure reason to the correct actor? Can its indexer distinguish an ordinary transfer from an administrative movement and reconstruct the relevant balances after a restart?&lt;/p&gt;

&lt;p&gt;Delivery versus payment adds a second asset and a settlement boundary. Token eligibility is one condition; cash availability and atomic execution are separate conditions. If policy changes between instruction creation and settlement, the system needs a defined rejection or cancellation result. A selected security-token standard does not make those conditions atomic by itself.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/SecurityTokenStandard/EIP-Spec/blob/8b082caba8adb4f8e351aa6acdd3e9e394e09dea/eip/eip-1643.md" rel="noopener noreferrer"&gt;ERC-1643&lt;/a&gt; supplies document references and update events. A URI and content hash can help connect a contract to a particular document. They do not establish that every investor received the document or accepted amended terms. Retain the availability and acknowledgement process separately, with versioned mappings to instrument state.&lt;/p&gt;

&lt;p&gt;Redemption also crosses systems. Burning tokens may reduce on-chain supply before an off-chain payment completes. Define whether the workflow escrows units, burns after payment or uses another approved state machine. Reconcile outstanding claims and instrument supply against the administrator's records rather than treating a burn event as proof that cash was delivered.&lt;/p&gt;

&lt;h2&gt;
  
  
  Require a reproducible selection record
&lt;/h2&gt;

&lt;p&gt;Request the same acceptance cases from each candidate, scoped to the approved instrument. Successful transfers alone are weak evidence. Rejected and exceptional operations reveal whether restrictions remain intact when actors or state change.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Acceptance case&lt;/th&gt;
&lt;th&gt;Evidence to retain&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Recipient eligibility changes after preview&lt;/td&gt;
&lt;td&gt;Policy change, execution result and unchanged balances on rejection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Aggregate balance exceeds spendable balance&lt;/td&gt;
&lt;td&gt;Partition or frozen-state snapshot and rejected request&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Operator authorization is removed&lt;/td&gt;
&lt;td&gt;Revocation transaction and subsequent unauthorized-call rejection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Administrative correction while ordinary transfers are paused&lt;/td&gt;
&lt;td&gt;Approved bypass policy, exact path and resulting events&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Required attestation issuer is removed&lt;/td&gt;
&lt;td&gt;Registry state and expected receipt behavior&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Redemption fails in the payment system&lt;/td&gt;
&lt;td&gt;Token state, cash state and reconciliation outcome&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Policy module or extension changes&lt;/td&gt;
&lt;td&gt;Old and new configuration plus affected-case results&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These are proposed acceptance cases, not a claim that this article executed a conformance suite. Each implementation should provide its own reproducible fixtures, transaction inputs and expected outcomes. Tests using identity stubs need to say which signature, expiry or revocation behavior they do not cover.&lt;/p&gt;

&lt;p&gt;Before release, bind the results to the source commit, compiler configuration, dependency versions and deployed addresses. Include the active identity registries or extensions, privileged-role holders and implementation or proxy relationships. Pinning source makes the comparison reproducible; it does not identify an audited release without matching audit evidence.&lt;/p&gt;

&lt;p&gt;The same record needs an operational owner. Name who maintains the investor registry, reconciles settlement exceptions and reviews privileged changes. For certificates, include signer rotation and incident handling. For partitions, include the dictionary of keys and the procedure for introducing a new one. If these jobs belong to different organizations, make their handoffs visible in the acceptance evidence rather than assigning them implicitly to the token developer.&lt;/p&gt;

&lt;p&gt;An upgrade needs a before-and-after ownership check that includes restrictions. Equal total supply can hide a lost freeze, a reinterpreted partition or an eligibility registry pointing at the wrong implementation. Rehearse ordinary transfers and exceptional paths against representative outstanding holdings, then reconcile the resulting ownership records.&lt;/p&gt;

&lt;p&gt;Record the choice in one sentence with a falsifiable reason. For example: select an ERC-3643 implementation because the instrument has one fungible share class and requires claim-based recipient eligibility plus wallet recovery. Or select the evaluated ERC-1400 implementation because issuance lots must remain explicit through partition-aware custody and settlement. Attach the acceptance evidence that makes the reason true.&lt;/p&gt;

&lt;p&gt;When both identities and partitions are essential, evaluate their composition as a separate design obligation. Specify where eligibility is enforced, how restrictions survive partition transitions and which authority may change either rule. The next engineering deliverable is the behavior matrix and reproducible transaction evidence for that exact composition.&lt;/p&gt;

&lt;h2&gt;
  
  
  More insights to read
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.bulbapp.io/p/e2580ef7-324b-442d-b818-92b8c8442b3d/five-smart-contract-upgrade-patterns-compared" rel="noopener noreferrer"&gt;Five Smart Contract Upgrade Patterns Compared&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://medium.com/pharos-production/smart-contracts-their-potential-and-real-limitations-part-2-40942c055d79" rel="noopener noreferrer"&gt;Smart Contracts. Their Potential and Real Limitations. Part 2&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://medium.com/pharos-production/smart-contracts-their-potential-and-real-limitations-part-1-222fe44ee14c" rel="noopener noreferrer"&gt;Smart Contracts. Their Potential and Real Limitations. Part 1&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dmytronasyrov.medium.com/upgradeable-solidity-smart-contracts-part-1-versioning-7e6e97cafc28" rel="noopener noreferrer"&gt;Upgradeable Solidity Smart Contracts. Part 1 — Versioning&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://medium.com/pharos-production/web3-smart-contracts-oracles-part-1-3905b127c01d" rel="noopener noreferrer"&gt;Web3. Smart Contracts. Oracles. Part 1&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  About the author
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foclejrac8sjqi79uy7hm.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foclejrac8sjqi79uy7hm.jpg" width="112" height="112" alt="Portrait of Dmytro Nasyrov wearing a dark suit and light blue shirt against a dark background."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;small&gt;Dmytro Nasyrov. Photo supplied by the author.&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Written by &lt;a href="https://pharosproduction.com/dmytro-nasyrov/" rel="noopener noreferrer"&gt;Dmytro Nasyrov&lt;/a&gt; PhD, software architect with 24 years of production experience. Dmytro is the founder and CTO of Pharos Production. He works on production software architecture for FinTech, AI, Web3 and blockchain systems.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>solidity</category>
      <category>blockchain</category>
      <category>web3</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Omnibus vs Segregated Wallets: The Ledger Decides</title>
      <dc:creator>Dmytro Nasyrov</dc:creator>
      <pubDate>Thu, 01 Oct 2026 05:59:05 +0000</pubDate>
      <link>https://dev.to/dmytronasyrov/omnibus-vs-segregated-wallets-the-ledger-decides-3fnb</link>
      <guid>https://dev.to/dmytronasyrov/omnibus-vs-segregated-wallets-the-ledger-decides-3fnb</guid>
      <description>&lt;p&gt;An omnibus wallet and a set of segregated wallets can hold exactly the same assets while supporting different reconciliation claims. The difference appears when a customer balance changes without a corresponding movement between customer wallets. A pool can still match total liabilities. Dedicated wallets can match that total while backing the wrong customers.&lt;/p&gt;

&lt;p&gt;Choose the model by the invariant your ledger must prove. For pooled assets, prove aggregate backing and separately validate customer allocations. For strict segregation, also prove that each customer's assigned wallets match that customer's entitlement. A wallet label cannot establish either result.&lt;/p&gt;

&lt;p&gt;The required evidence differs.&lt;/p&gt;

&lt;p&gt;The example below uses Alice, Bob and 100 synthetic asset units. A complete Python snapshot checker and twelve tests expose an aggregate check that incorrectly approves a segregated allocation. The fixture makes no calls to a blockchain or custody provider.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separate the wallet, the ledger and the authority
&lt;/h2&gt;

&lt;p&gt;A wallet describes an operational arrangement for holding and moving assets. A customer ledger describes entitlements, reservations and postings. An authority model describes who can approve or sign a movement. These are separate design decisions, even when a provider presents them together in one interface. In a pooled arrangement, several customers' entitlements are backed by assets held in shared wallets. The application needs a private ledger to assign those entitlements. In a segregated arrangement, identified wallets or vault accounts are assigned to individual customers. The ledger still needs to describe spendable balances, pending operations and corrections. The address does not explain those states by itself.&lt;/p&gt;

&lt;p&gt;Fireblocks documents these operational models in &lt;a href="https://fireblocks.readme.io/docs/create-direct-custody-wallets" rel="noopener noreferrer"&gt;Create Direct Custody Wallets&lt;/a&gt;. Its description distinguishes pooled vault arrangements from individual customer vaults and treats hot or cold storage as another dimension. That distinction matters: segregation does not automatically mean offline signing, and a hot wallet does not automatically mean pooled customer accounting.&lt;/p&gt;

&lt;p&gt;The same documentation explains the application-side mapping for a deposit address:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This deposit address is assigned to Alice and mapped accordingly within the customer’s private ledger.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Fireblocks, &lt;em&gt;&lt;a href="https://fireblocks.readme.io/docs/create-direct-custody-wallets" rel="noopener noreferrer"&gt;Create Direct Custody Wallets&lt;/a&gt;&lt;/em&gt;, official developer documentation, accessed October 1, 2026. The page does not state an exact publication date.&lt;/p&gt;

&lt;p&gt;The quoted sentence identifies the missing join: an observed address must be connected to a customer in the ledger. It does not claim that a deposit address proves legal ownership or that every asset uses a separate address for each customer.&lt;/p&gt;

&lt;p&gt;This separation also guides the &lt;a href="https://pharosproduction.com" rel="noopener noreferrer"&gt;blockchain engineering work at Pharos Production&lt;/a&gt;: selecting wallet infrastructure is only one part of specifying how the surrounding application records financial state. The engineering decision needs an entitlement model and a recovery boundary before an address diagram can serve as an acceptance artifact.&lt;/p&gt;

&lt;p&gt;Product terminology deserves a second check. Coinbase describes &lt;a href="https://help.coinbase.com/en/prime/prime-custody/introduction" rel="noopener noreferrer"&gt;Prime Custody&lt;/a&gt; as an omnibus arrangement using its LSOC terminology, while Prime Vault uses dedicated addresses. Those are statements about named products, not evidence that any architecture called segregated has the same contractual treatment. Obtain the applicable custody agreement separately. Neither this article nor its test fixture determines legal title, insolvency protection or the adequacy of a provider's controls.&lt;/p&gt;

&lt;h2&gt;
  
  
  Compare the evidence each model needs
&lt;/h2&gt;

&lt;p&gt;The useful comparison is between operating requirements. A diagram with one box per customer may be easier to explain, but its proof obligations can be more demanding when the product permits transfers between customers.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Decision dimension&lt;/th&gt;
&lt;th&gt;Omnibus arrangement&lt;/th&gt;
&lt;th&gt;Segregated arrangement&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Customer entitlement&lt;/td&gt;
&lt;td&gt;Requires a ledger allocation inside the shared asset pool&lt;/td&gt;
&lt;td&gt;Requires a ledger allocation and an explicit customer-to-wallet assignment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backing check&lt;/td&gt;
&lt;td&gt;Compare included pool assets with included customer liabilities&lt;/td&gt;
&lt;td&gt;Compare the aggregate and every customer's assigned assets with their liabilities&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Internal customer transfer&lt;/td&gt;
&lt;td&gt;Can change ledger allocations while the pool remains unchanged&lt;/td&gt;
&lt;td&gt;Needs an explicit policy for pending allocation or corresponding wallet movement&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deposit handling&lt;/td&gt;
&lt;td&gt;Attribution must survive any sweep into a shared wallet&lt;/td&gt;
&lt;td&gt;Attribution must survive wallet creation, reassignment and recovery&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Operating constraint&lt;/td&gt;
&lt;td&gt;Shared movements obscure customer allocation on-chain&lt;/td&gt;
&lt;td&gt;More customer wallet movements can increase transaction and monitoring work&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Disqualifier&lt;/td&gt;
&lt;td&gt;Cannot satisfy a requirement for exclusive customer wallet backing&lt;/td&gt;
&lt;td&gt;Cannot claim strict backing when allocations move ahead of assigned assets&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These rows are design consequences of the definitions used here, not a provider performance benchmark. There is no universal winner. Pooling may fit a product whose customer transfers are internal ledger operations. Dedicated wallets may fit a product that requires independently inspectable wallet assignments. Both remain dependent on complete asset inventory and correct postings.&lt;/p&gt;

&lt;p&gt;Define the acceptance language before comparing suppliers. If the requirement says customer assets remain identifiable, ask whether that means ledger identification, dedicated wallet assignment, a contractual arrangement, or all three. Each interpretation needs different evidence. A vendor showing balances by customer in a dashboard has demonstrated a view. It has not demonstrated the provenance of those balances.&lt;/p&gt;

&lt;p&gt;Also specify the accounting perimeter. A customer asset pool, the operator's own funds, a fee wallet and assets in transit must not be silently added together. A mixed dashboard total can hide an excluded liability or compensate for a shortfall with funds outside the agreed perimeter. Keep the comparison per asset, per network and per cutoff before considering any converted reporting total.&lt;/p&gt;

&lt;h2&gt;
  
  
  A balanced total can hide the wrong allocation
&lt;/h2&gt;

&lt;p&gt;Start with one synthetic asset. Alice has an entitlement of 60 units; Bob has 40. Total customer liabilities are 100. In the omnibus case, the pool contains 100. In the segregated case, Alice's assigned wallet contains 60 and Bob's contains 40.&lt;/p&gt;

&lt;p&gt;Now record a transfer of 15 units from Alice to Bob in the customer ledger. The new entitlements are 45 and 55. Total liabilities are still 100. The following table isolates the wallet movement from the ledger posting:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Snapshot&lt;/th&gt;
&lt;th&gt;Alice's ledger&lt;/th&gt;
&lt;th&gt;Bob's ledger&lt;/th&gt;
&lt;th&gt;Omnibus pool&lt;/th&gt;
&lt;th&gt;Segregated Alice/Bob wallets&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Opening&lt;/td&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;td&gt;40&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;60 / 40&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Internal entitlement transfer recorded&lt;/td&gt;
&lt;td&gt;45&lt;/td&gt;
&lt;td&gt;55&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;60 / 40&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Corresponding segregated movement completed&lt;/td&gt;
&lt;td&gt;45&lt;/td&gt;
&lt;td&gt;55&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;45 / 55&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;An aggregate checker reports zero difference in every row. For the middle row, the segregated checker must report Alice's assigned assets minus entitlement as +15, and Bob's as −15. The aggregate result is correct. The conclusion that segregation is reconciled is incorrect.&lt;/p&gt;

&lt;p&gt;There are two coherent ways to handle that middle state. A strict settled-balance policy can keep the customer transfer pending until the required wallet movement completes. A more complex policy can account explicitly for an allocation bridge or receivable. The second option needs its own permitted states, aging rules and reconciliation equations. It cannot borrow the strict model's passing label while omitting the bridge from the report.&lt;/p&gt;

&lt;p&gt;The simple checker below deliberately implements the strict snapshot interpretation. It has no bridge account and no transaction state machine. Its rejection means the dedicated wallet amounts do not match the supplied settled entitlements. It does not mean that every temporary mismatch is fraud, loss or an invalid business operation.&lt;/p&gt;

&lt;p&gt;For the omnibus row, aggregate equality establishes only that included pool assets equal included customer liabilities. Alice could still be credited incorrectly, or Bob could receive a duplicated posting, while another posting offsets the error. Customer allocation correctness therefore needs independent journal and authorization checks. A pooled report should state that owner backing was not checked, rather than display an empty exceptions list that resembles a successful customer-level check.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run the snapshot witness and the regression
&lt;/h2&gt;

&lt;p&gt;The aggregate equation is included wallet assets minus recognized customer liabilities. The strict segregated equation repeats that subtraction for each assigned customer. Both must equal zero for this fixture. An excess is a discrepancy too: it may represent an unattributed deposit, an omitted customer liability or funds outside the declared perimeter. The checker does not classify the cause from the sign alone.&lt;/p&gt;

&lt;p&gt;Notice that matching every owner implies the aggregate matches only when the inventory and owner mapping cover the same population. Retaining both outputs makes the declared population inspectable and exposes adapter mistakes. The function rejects an assigned owner absent from the position map instead of quietly excluding that wallet from the owner calculation.&lt;/p&gt;

&lt;p&gt;Save the next block as &lt;code&gt;reconciliation.py&lt;/code&gt;. Amounts are integer base units. &lt;code&gt;Position.total&lt;/code&gt; includes reserved units. Reservations reduce availability, not the liability already represented by the total. A wallet row is one included balance observation, identified by a unique name within this materialized list.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dataclasses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dataclass&lt;/span&gt;

&lt;span class="nd"&gt;@dataclass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frozen&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;reserved&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;

&lt;span class="nd"&gt;@dataclass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frozen&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Wallet&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;asset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;reconcile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;positions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;wallets&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;asset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Pure snapshot witness. Inputs are trusted observations, not fetched here.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;omnibus&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;segregated&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unknown model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;positions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="nf"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;owner&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reserved&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
                &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reserved&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;invalid position&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;seen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;owner_assets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;owner&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;positions&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;wallets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;duplicate wallet&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;asset&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;asset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;asset mismatch&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cutoff mismatch&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;invalid wallet&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;segregated&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;owner&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;positions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;owner mismatch&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;owner_assets&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;
        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;owner&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;owner mismatch&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;assets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;wallets&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;liabilities&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;positions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="n"&gt;owner_deltas&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="n"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;owner_assets&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;
                     &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;positions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;
                    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;segregated&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt;
    &lt;span class="n"&gt;delta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;assets&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;liabilities&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;asset_total&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;assets&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;liability_total&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;liabilities&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;available_total&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reserved&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;positions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;aggregate_delta&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;owner_deltas&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;owner_deltas&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;owner_checked&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;segregated&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reconciled&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;delta&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;owner_deltas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;())}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save the following as &lt;code&gt;test_reconciliation.py&lt;/code&gt; in the same directory. The asset identifier and block label are synthetic constants. The tests exercise distinct outcomes and refusal classes rather than every possible combination of inputs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;unittest&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;reconciliation&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Position&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Wallet&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reconcile&lt;/span&gt;

&lt;span class="n"&gt;ASSET&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chain-a:token-x&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;  &lt;span class="c1"&gt;# Synthetic asset identity, not a live token.
&lt;/span&gt;&lt;span class="n"&gt;BLOCK&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;block-hash-100&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;   &lt;span class="c1"&gt;# Synthetic named cutoff, not a live block.
&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ReconciliationTests&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;unittest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TestCase&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;positions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;alice&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Position&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bob&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Position&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;wallet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;owner&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;asset&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ASSET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;BLOCK&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Wallet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;asset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;positions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;wallets&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;reconcile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;positions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;wallets&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;asset&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ASSET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;BLOCK&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_omnibus_total_matches&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;report&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;omnibus&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;positions&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wallet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pool&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)])&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertTrue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reconciled&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertFalse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;owner_checked&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;owner_deltas&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_segregated_allocation_matches&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;report&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;segregated&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;positions&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wallet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;a&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;alice&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wallet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bob&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)])&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertTrue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reconciled&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;owner_deltas&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;alice&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bob&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_balanced_total_wrong_owner_fails&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;positions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;alice&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Position&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;45&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bob&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Position&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;55&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
        &lt;span class="n"&gt;report&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;segregated&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;positions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wallet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;a&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;alice&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wallet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bob&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)])&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;aggregate_delta&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertFalse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reconciled&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;owner_deltas&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;alice&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bob&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_reserved_is_already_in_total&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;positions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;alice&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Position&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;45&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bob&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Position&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;55&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
        &lt;span class="n"&gt;report&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;omnibus&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;positions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wallet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pool&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)])&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;liability_total&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;available_total&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertTrue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reconciled&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_asset_shortfall_is_visible&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;report&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;omnibus&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;positions&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wallet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pool&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="p"&gt;)])&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;aggregate_delta&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertFalse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reconciled&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_duplicate_wallet_rejected&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertRaisesRegex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;duplicate wallet&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;omnibus&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;positions&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wallet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pool&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_mixed_asset_rejected&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertRaisesRegex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;asset mismatch&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;omnibus&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;positions&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
                &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wallet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pool&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;asset&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chain-b:token-x&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)])&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_mixed_cutoff_rejected&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertRaisesRegex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cutoff mismatch&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;omnibus&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;positions&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
                &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wallet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pool&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;block-hash-101&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)])&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_invalid_reservation_rejected&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertRaisesRegex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;invalid position&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;omnibus&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;alice&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Position&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;)},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wallet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pool&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)])&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_unknown_owner_rejected&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertRaisesRegex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;owner mismatch&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;segregated&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;positions&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wallet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pool&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;carol&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)])&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_unknown_model_rejected&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertRaisesRegex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unknown model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unreviewed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;positions&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wallet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pool&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)])&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_invalid_wallet_units_rejected&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertRaisesRegex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;invalid wallet&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;omnibus&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;positions&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wallet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pool&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)])&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;unittest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run both files with &lt;a href="https://docs.python.org/3.10/whatsnew/3.10.html" rel="noopener noreferrer"&gt;Python 3.10 or later&lt;/a&gt;, which supports the union annotation used above. The recorded local run used Python 3.14.7. The &lt;a href="https://docs.python.org/3/library/unittest.html" rel="noopener noreferrer"&gt;Python unittest documentation&lt;/a&gt; describes the command and assertions used here.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 &lt;span class="nt"&gt;-m&lt;/span&gt; unittest &lt;span class="nt"&gt;-v&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The regression was first executed against an incomplete aggregate-only version. That version returned &lt;code&gt;reconciled=True&lt;/code&gt; for the middle table row, causing &lt;code&gt;test_balanced_total_wrong_owner_fails&lt;/code&gt; to fail with &lt;code&gt;AssertionError: True is not false&lt;/code&gt;. The correction accumulated assets by assigned owner and required every owner difference to be zero. The same regression then passed, followed by all twelve tests.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbysvzr2ry85ggsszbeyw.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbysvzr2ry85ggsszbeyw.gif" alt="Recorded test-output replay: the wrong-owner regression fails against an aggregate-only checker, the correction adds per-owner reconciliation, and the complete twelve-test suite passes." width="799" height="371"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;small&gt;Replay of recorded local test output and the recorded correction. This is a synthetic fixture, not a terminal screen recording or a live custody-system test.&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;The report preserves the distinction between a numerical discrepancy and invalid input. A shortfall produces a report with &lt;code&gt;reconciled=False&lt;/code&gt;. Mixed assets, mixed cutoffs, duplicate wallet rows and unknown assigned owners raise errors. A production adapter needs similarly explicit handling, but this function does not establish that its input inventory is complete or authentic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reservations and withdrawals need a posting policy
&lt;/h2&gt;

&lt;p&gt;Reserve 20 units for Bob after the transfer, when Alice has 45 and Bob has 55. Bob's available balance becomes 35. Total customer liabilities remain 100, and aggregate available balances become 80. Adding the reservation to the liability total again would report 120 against assets of 100, manufacturing a shortfall.&lt;/p&gt;

&lt;p&gt;The reservation test checks this classification only. It does not execute a withdrawal. In a zero-fee illustrative withdrawal that completes for 20 units, assets would fall to 80 and customer liabilities would fall to 80: Alice 45, Bob 35. While the withdrawal is merely reserved, the supplied snapshot still contains the original 100 of liabilities. The accounting transition needs a defined settlement event.&lt;/p&gt;

&lt;p&gt;Pharos Production addresses the upstream design choice in its &lt;a href="https://pharosproduction.com/services/crypto-wallet-development/" rel="noopener noreferrer"&gt;crypto wallet development process&lt;/a&gt;, which includes custody-model decisions, threat modeling and recovery design. Those decisions should establish which component may reserve funds, approve a withdrawal and recognize completion. The service description is evidence of that stated process. It is not evidence that this illustrative checker is deployed in a client system.&lt;/p&gt;

&lt;p&gt;Specify failure behavior for each boundary. A failed signing request can release a reservation if no asset movement occurred. A submitted transaction with an uncertain outcome needs investigation or a pending state. Releasing the same reservation immediately could make those units available for another withdrawal while the first remains executable. A timeout is an observation about response time, not proof of cancellation.&lt;/p&gt;

&lt;p&gt;Fees need an equally explicit rule. If a withdrawal consumes a fee in the same asset, identify whose liability bears it and when that debit becomes final. If the fee uses a different network asset, reconcile that asset separately. Do not conceal it by rounding a converted portfolio value. The example uses no fees so its per-owner mismatch is easy to inspect. A fee policy would extend the fixture and its tests.&lt;/p&gt;

&lt;p&gt;Keep availability checks separate from backing checks. The first answers whether a customer may initiate another operation. The second compares recognized obligations with included assets. A customer can have a correctly backed total and no available balance because every unit is reserved. Conversely, an available balance can be overstated even when the aggregate backing report is numerically correct.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preserve identities through deposits and sweeps
&lt;/h2&gt;

&lt;p&gt;A balance observation needs more identity than an amount and a token symbol. Record the network, the asset identity, the wallet or vault assignment, the source observation and the cutoff. Two assets with the same display symbol must not enter one reconciliation equation merely because the interface renders the same label.&lt;/p&gt;

&lt;p&gt;For an ERC-20 token, &lt;a href="https://eips.ethereum.org/EIPS/eip-20" rel="noopener noreferrer"&gt;the standard's balanceOf method&lt;/a&gt; returns the balance of an address. It does not return the application's customer identifier. The application must provide that relationship, including the effective period of an assignment. A reused identifier or an incorrect assignment can make accurate chain data answer the wrong customer question.&lt;/p&gt;

&lt;p&gt;Fireblocks' &lt;a href="https://fireblocks.readme.io/docs/manage-deposits-at-scale" rel="noopener noreferrer"&gt;deposit-at-scale guidance&lt;/a&gt; describes validating deposit observations and maintaining a private ledger after assets are swept. A sweep changes where assets are held. It must not create another customer deposit credit simply because a second wallet received the funds. The credit and the internal asset movement have different accounting meanings.&lt;/p&gt;

&lt;p&gt;Build source identity around those meanings. For a token deposit, a transaction can contain multiple relevant events, so a transaction hash alone may be insufficient to distinguish credits. Include the event identity and the adapter's declared network scope. For a provider callback, preserve the provider's operation identity and status history rather than treating every delivery as a new financial operation.&lt;/p&gt;

&lt;p&gt;Idempotency then becomes a posting contract: receiving the same recognized deposit twice produces one customer credit. A later status update changes the operation's state under a permitted transition. It does not append another credit by default. A replay check should compare the resulting ledger state and asset inventory, not just count callbacks successfully acknowledged by the server.&lt;/p&gt;

&lt;p&gt;The snapshot function rejects duplicated wallet names, but that is a much smaller guarantee. It does not detect duplicated journal entries, verify deposit events, fetch a block, handle a reorganization or prove settlement finality. An adapter must supply observations at the declared cutoff and explain its finality policy. A matching string in every row establishes consistency of the supplied labels, not truth of the underlying chain history.&lt;/p&gt;

&lt;h2&gt;
  
  
  Signing authority does not replace reconciliation
&lt;/h2&gt;

&lt;p&gt;Separate the ability to move assets from the ability to alter reported obligations. A signing policy can stop an unauthorized withdrawal while a faulty ledger posting still changes customer balances. A perfect journal can coexist with a compromised signer. Review both boundaries because their evidence and failure modes differ.&lt;/p&gt;

&lt;p&gt;For each model, identify who may create a customer-to-wallet assignment, who may change it and who may approve exceptions. The reconciliation service should consume a versioned mapping. If an operator silently reassigns Bob's wallet to Alice, a report can change without any asset movement. Retain the previous mapping and the reason for the change so a reviewer can reconstruct the earlier result.&lt;/p&gt;

&lt;p&gt;The same principle applies to model selection. Switching a mismatching snapshot from segregated to omnibus removes owner checks and can turn failure into success. That is a change in the promise being evaluated. Require an approved model configuration and bind its version to every report. Do not let a reconciliation operator select the interpretation that produces the fewest exceptions.&lt;/p&gt;

&lt;p&gt;Emergency controls need a defined effect on accounting. A pause may prevent new withdrawals while deposits and status observations continue. State whether incoming events are still recorded, whether customer credits remain pending and how reservations are handled. A pause button without these rules can produce a technically stopped service whose ledger keeps drifting.&lt;/p&gt;

&lt;p&gt;Recovery should preserve investigation evidence. Before accepting a manual correction, retain the original report, the supporting observations and the proposed posting. Record the correction's author and approval separately from the program that recalculates balances. Then rerun the same invariant under the same perimeter. A balancing entry is not explanatory evidence merely because it reduces the difference to zero.&lt;/p&gt;

&lt;p&gt;For segregation, also rehearse a wallet reassignment or recovery operation. Verify that old and new assignments cannot both count the same asset observation, and that the customer's history remains accessible. For pooling, rehearse restoration of the allocation journal from a known point. In either case, a usable backup must support the particular accounting promise, not just restore a wallet service login.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make the acceptance receipt reconstructable
&lt;/h2&gt;

&lt;p&gt;A supplier demo should hand over a report that another engineer can reproduce. Bind the model version, asset identity, customer perimeter, cutoff, wallet assignments and journal position to the result. Include totals and exceptions together. An empty exception file without the evaluated population cannot establish what passed.&lt;/p&gt;

&lt;p&gt;For this fixture, acceptance means recreating the opening balances, applying the internal entitlement change and observing the strict segregated failure. It also means seeing the omnibus report declare &lt;code&gt;owner_checked=False&lt;/code&gt;. That boolean is evidence of limited scope. It is not a warning that the pooled model is inherently defective.&lt;/p&gt;

&lt;p&gt;Ask for the negative cases before signing off. The wrong-owner case is useful because it preserves the aggregate and challenges the claim that a zero total difference is enough. The shortfall case proves that a real numerical discrepancy remains visible. Invalid-input cases prove that incompatible observations cannot be silently combined into a plausible-looking report.&lt;/p&gt;

&lt;p&gt;Keep the reported coverage accurate. The twelve local tests cover snapshot results, reservations and selected validation failures. They do not cover concurrent withdrawals, durable journal posting, signer compromise, provider outages, stale RPC data or inventory completeness. Those requirements belong to the surrounding system and need separate evidence. Passing this suite cannot certify custody safety or accounting compliance.&lt;/p&gt;

&lt;p&gt;To extend the example, add one real requirement at a time. If the product allows pending internal transfers between dedicated wallets, specify the bridge account and the permitted duration before implementing it. Write a failure case showing how an expired or unmatched bridge affects acceptance. If the product mixes customer assets with operating funds, first separate the accounting perimeter rather than widening the sum until it balances.&lt;/p&gt;

&lt;p&gt;Operational ownership is part of acceptance. Name who investigates a mismatch, which operations may continue and which report releases the restriction. Preserve unresolved cases across restarts. A status page can say a service is healthy while customer backing exceptions remain open. Health and reconciliation are different measurements.&lt;/p&gt;

&lt;p&gt;An exception receipt should identify the affected asset and customer, the observed difference, the original cutoff and the next authorized action. If new observations resolve it, retain both results rather than replacing the failed snapshot. If a mapping correction resolves it, preserve the mapping change alongside the recalculation. These records distinguish a late observation from a corrected interpretation. Both may produce a zero difference while requiring different operational responses.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choose the promise you can keep
&lt;/h2&gt;

&lt;p&gt;Use omnibus custody when pooled backing and an independently controlled customer allocation ledger satisfy the product's actual requirements. Use segregation when the product requires dedicated assignments and can reconcile each customer's assigned assets against recognized entitlements. Evaluate the contractual and signing arrangements alongside that choice, with their own evidence.&lt;/p&gt;

&lt;p&gt;The deciding question is what must remain true after a transfer, a reservation, a sweep and a recovery. If the answer requires per-customer backing, require per-customer deltas. If the answer permits shared backing, preserve the allocation journal and state the limits of the aggregate result. A single balance cannot carry both promises without additional records.&lt;/p&gt;

&lt;p&gt;Before approving a wallet design, request one trace that leaves the aggregate unchanged while changing customer allocations. Require the team to explain exactly when that trace becomes settled and why the report accepts or rejects it. The 60/40 to 45/55 example supplies a reproducible starting point for that conversation.&lt;/p&gt;

&lt;p&gt;For engineers operating dedicated customer wallets: does your system keep an internal customer transfer pending until the assigned assets move, or account for an explicit bridge? What evidence allows that pending state to close?&lt;/p&gt;

&lt;h2&gt;
  
  
  More insights to read
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://pharosengineeringnotes.wordpress.com/2026/09/30/stablecoin-reconciliation-acceptance-evidence/" rel="noopener noreferrer"&gt;How to Specify Stablecoin Reconciliation Acceptance Evidence&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pharosengineeringnotes.wordpress.com/2026/09/11/how-to-specify-a-blockchain-ledger-integration-contract/" rel="noopener noreferrer"&gt;How to Specify a Blockchain Ledger Integration Contract&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pharosengineeringnotes.wordpress.com/2026/09/26/specify-audit-evidence-exports-fintech-delivery-contract/" rel="noopener noreferrer"&gt;How to Specify Audit Evidence Exports in a FinTech Delivery Contract&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://hackmd.io/@dmytro-nasyrov/ledger-reconciliation-snapshot-cutoff-late-events" rel="noopener noreferrer"&gt;What Did the Ledger Know at Cutoff? Build a Reconciliation Snapshot&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pharosengineeringnotes.wordpress.com/2026/09/11/how-to-choose-a-blockchain-integrator-for-an-existing-fintech-ledger/" rel="noopener noreferrer"&gt;How to Choose a Blockchain Integrator for an Existing FinTech Ledger&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  About the author
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkqea3ixmuakc8lc41xtu.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkqea3ixmuakc8lc41xtu.jpg" width="112" height="112" alt="Portrait of Dmytro Nasyrov wearing a dark suit and light blue shirt against a dark background."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;small&gt;Dmytro Nasyrov. Photo supplied by the author.&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Written by &lt;a href="https://pharosproduction.com/dmytro-nasyrov/" rel="noopener noreferrer"&gt;Dmytro Nasyrov&lt;/a&gt; PhD, software architect with 24 years of production experience. Dmytro is the founder and CTO of Pharos Production. He works on production software architecture for FinTech, AI, Web3 and blockchain systems.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>blockchain</category>
      <category>fintech</category>
      <category>architecture</category>
      <category>testing</category>
    </item>
    <item>
      <title>How to Implement ERC-3643 Transfer Controls for Permissioned Tokens</title>
      <dc:creator>Dmytro Nasyrov</dc:creator>
      <pubDate>Wed, 30 Sep 2026 07:46:18 +0000</pubDate>
      <link>https://dev.to/pharos_production/how-to-implement-erc-3643-transfer-controls-for-permissioned-tokens-2e40</link>
      <guid>https://dev.to/pharos_production/how-to-implement-erc-3643-transfer-controls-for-permissioned-tokens-2e40</guid>
      <description>&lt;p&gt;A permissioned token needs a testable answer to a simple question: why did this movement of tokens succeed? A successful wallet transfer proves little if an agent operation follows different rules, a registry change removes an eligibility requirement or a frontend mistakes a compliance check for complete transaction validation.&lt;/p&gt;

&lt;p&gt;This walkthrough implements a recipient-country rule against the actual ERC-3643 contract suite and exercises it with 19 passing tests. It also records a deliberately broken version of the module, the failing regression and the correction. The test fixture uses real token, registry and compliance contracts.&lt;/p&gt;

&lt;p&gt;Identity signatures are replaced with explicit test doubles, so the result establishes transfer-control behavior within that boundary.&lt;/p&gt;

&lt;p&gt;The distinction between documentation and executable behavior matters here. The ERC-3643 specification describes minting as bypassing compliance rules. The pinned implementation used below calls &lt;code&gt;canTransfer&lt;/code&gt; during minting. A release review must resolve that difference for its selected contracts before anyone signs an issuance transaction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with a policy-to-operation map
&lt;/h2&gt;

&lt;p&gt;Write the intended rule before choosing a module. In this example, an ordinary transfer requires an eligible recipient whose registered country appears in an allowlist. Wallet freezes, available balance and the token's pause state remain separate token-level checks. Country values represent registry records maintained by authorized operators; they do not reveal a wallet's physical location.&lt;/p&gt;

&lt;p&gt;The implementation question in &lt;a href="https://pharosproduction.com" rel="noopener noreferrer"&gt;blockchain engineering work at Pharos Production&lt;/a&gt; is how a requirement becomes an observable contract behavior. For this walkthrough, the useful deliverable is a table connecting each rule to a transaction and an assertion. A statement that the token supports compliance cannot tell an integration engineer which call should revert.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Requirement&lt;/th&gt;
&lt;th&gt;Enforcement point&lt;/th&gt;
&lt;th&gt;Observable test&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Recipient has the required trusted claim&lt;/td&gt;
&lt;td&gt;Identity registry&lt;/td&gt;
&lt;td&gt;Missing claim or removed issuer rejects transfer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Recipient country is permitted&lt;/td&gt;
&lt;td&gt;Custom compliance module&lt;/td&gt;
&lt;td&gt;Registered recipient in a denied country cannot receive an ordinary transfer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Token is available for ordinary movement&lt;/td&gt;
&lt;td&gt;Token pause modifier&lt;/td&gt;
&lt;td&gt;Paused transfer fails even when &lt;code&gt;canTransfer&lt;/code&gt; returns true&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wallet and balance restrictions apply&lt;/td&gt;
&lt;td&gt;Token freeze checks&lt;/td&gt;
&lt;td&gt;Frozen wallet fails; partially frozen balance retains its boundary&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Delegated spending stays bounded&lt;/td&gt;
&lt;td&gt;Token allowance accounting&lt;/td&gt;
&lt;td&gt;Successful spending reduces allowance; rejected transfer preserves it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Privileged movement has explicit authority&lt;/td&gt;
&lt;td&gt;Token agent role&lt;/td&gt;
&lt;td&gt;Unauthorized forced transfer fails; authorized exceptional path is tested separately&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Decide separately what should happen when an investor's country changes while they already own tokens. This module restricts recipients. It does not automatically confiscate an existing holding, prevent every outbound movement or calculate ownership concentration. Those are different policy decisions and require their own enforcement points and tests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pin the contracts before interpreting the standard
&lt;/h2&gt;

&lt;p&gt;The code below targets &lt;a href="https://github.com/ERC-3643/ERC-3643/tree/2f0704dee9658ad9cd5b6ed82e7427da74c28345" rel="noopener noreferrer"&gt;ERC-3643/ERC-3643 at commit &lt;code&gt;2f0704dee9658ad9cd5b6ed82e7427da74c28345&lt;/code&gt;&lt;/a&gt;. Its package identifies version 4.1.3. The compiler is Solidity 0.8.17, with OpenZeppelin contracts and upgradeable contracts pinned to 4.8.3 and the ONCHAINID Solidity package pinned to 2.0.0. This is a reproducible source selection, not a recommendation to deploy an unreviewed repository head.&lt;/p&gt;

&lt;p&gt;Read the selected entry points directly. In this version, &lt;code&gt;transfer&lt;/code&gt; and &lt;code&gt;transferFrom&lt;/code&gt; combine token checks with recipient verification and modular compliance. Do not describe that as universal verification of both participants. If your policy requires renewed sender eligibility on every ordinary movement, add and test that requirement explicitly rather than assuming the recipient check covers it.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://eips.ethereum.org/EIPS/eip-3643#transfer" rel="noopener noreferrer"&gt;standard's transfer description&lt;/a&gt; groups minting with forced transfers as paths that bypass compliance. However, the pinned &lt;a href="https://github.com/ERC-3643/ERC-3643/blob/2f0704dee9658ad9cd5b6ed82e7427da74c28345/contracts/token/Token.sol" rel="noopener noreferrer"&gt;Token implementation&lt;/a&gt; checks &lt;code&gt;canTransfer(address(0), recipient, amount)&lt;/code&gt; inside &lt;code&gt;mint&lt;/code&gt;. Our mint-denial test captures this behavior. Preserve that test when upgrading the dependency; a changed result needs an explicit issuance-policy decision.&lt;/p&gt;

&lt;p&gt;Review inherited behavior and callbacks too. An entry point can omit an ordinary permission check while still reverting in a downstream module action. An interface name alone cannot establish that an operation will succeed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make identity requirements explicit
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://docs.erc3643.org/erc-3643/smart-contracts-library/onchain-identities/identity-registry" rel="noopener noreferrer"&gt;identity registry documentation&lt;/a&gt; separates a wallet's registration from its verification. Registration associates the wallet with an identity and a country. Verification evaluates the required claim topics against the trusted issuer configuration. Keep those responsibilities distinct in onboarding screens and operational runbooks.&lt;/p&gt;

&lt;p&gt;A configuration edge case deserves its own regression. In the pinned &lt;a href="https://github.com/ERC-3643/ERC-3643/blob/2f0704dee9658ad9cd5b6ed82e7427da74c28345/contracts/registry/implementation/IdentityRegistry.sol" rel="noopener noreferrer"&gt;IdentityRegistry implementation&lt;/a&gt;, a registered identity passes verification when the required-topic list is empty. Our fixture first removes a claim and observes rejection. It then removes the sole required topic and observes acceptance. That is configuration behavior, not evidence of a newly discovered vulnerability.&lt;/p&gt;

&lt;p&gt;Consequently, checking that every wallet has an identity contract is insufficient for a deployment that requires a particular credential. The release receipt should record the required topics, trusted issuers and each issuer's permitted topics. A reviewer should be able to explain why an empty list is acceptable or show that the deployment rejects that configuration before activation.&lt;/p&gt;

&lt;p&gt;Topic &lt;code&gt;7&lt;/code&gt; is a synthetic identifier in this fixture, not a universal KYC topic. Its issuer stub always accepts the supplied claim, and its identity stub exposes unrestricted setters for test setup. Neither stub belongs in a production deployment. Real signature validation, claim expiry and issuer-specific revocation behavior require a separate integration suite using the chosen identity implementation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implement one recipient-country module
&lt;/h2&gt;

&lt;p&gt;Keep this first rule narrow enough to inspect. A mapping answers whether a particular compliance contract permits a particular country. The outer address key matters because a module can serve more than one compliance context. Without that separation, one deployment's policy update could unintentionally alter another deployment's behavior.&lt;/p&gt;

&lt;p&gt;The module reads the token bound to the supplied compliance address, then reads the recipient's country through that token's identity registry. The allowlist defaults to rejection. Country zero cannot be enabled through the setter, making the example's requirement for an explicitly configured country visible in code. The fixture uses &lt;code&gt;250&lt;/code&gt; and &lt;code&gt;276&lt;/code&gt; as two distinct country values; neither value represents a policy recommendation.&lt;/p&gt;

&lt;p&gt;Save the following as &lt;code&gt;src/RecipientCountryModule.sol&lt;/code&gt;. It extends the pinned &lt;a href="https://github.com/ERC-3643/ERC-3643/blob/2f0704dee9658ad9cd5b6ed82e7427da74c28345/contracts/compliance/modular/modules/AbstractModule.sol" rel="noopener noreferrer"&gt;AbstractModule&lt;/a&gt;. Configuration requires a call from a bound compliance contract. The check also rejects an unbound compliance context. The transfer, mint and burn action hooks remain access-controlled even though this stateless rule has nothing to update.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.17;

import "erc3643/compliance/modular/modules/AbstractModule.sol";
import "erc3643/compliance/modular/IModularCompliance.sol";
import "erc3643/token/IToken.sol";

contract RecipientCountryModule is AbstractModule {
    mapping(address =&amp;gt; mapping(uint16 =&amp;gt; bool)) public allowed;
    event CountryPermissionSet(address indexed compliance, uint16 country, bool enabled);

    function setCountry(uint16 country, bool enabled) external onlyComplianceCall {
        require(country != 0, "country must be explicit");
        allowed[msg.sender][country] = enabled;
        emit CountryPermissionSet(msg.sender, country, enabled);
    }

    function moduleCheck(address, address to, uint256, address compliance)
        external view override onlyBoundCompliance(compliance) returns (bool)
    {
        IToken token = IToken(IModularCompliance(compliance).getTokenBound());
        uint16 country = token.identityRegistry().investorCountry(to);
        return allowed[compliance][country];
    }

    function moduleTransferAction(address, address, uint256)
        external override onlyComplianceCall {}
    function moduleMintAction(address, uint256)
        external override onlyComplianceCall {}
    function moduleBurnAction(address, uint256)
        external override onlyComplianceCall {}
    function canComplianceBind(address) external pure override returns (bool) { return true; }
    function isPlugAndPlay() external pure override returns (bool) { return true; }
    function name() external pure override returns (string memory) { return "RecipientCountryModule"; }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A rule based only on recipient country does not need a transfer counter. If you extend it to maximum holders or rolling volume limits, the action hooks become part of correctness. Define how minting, burning and exceptional transfers update that state. Test a revert in a later callback and prove that earlier changes roll back with the transaction. This article's empty hooks cannot establish that property for a future stateful module.&lt;/p&gt;

&lt;h2&gt;
  
  
  Route configuration through the compliance owner
&lt;/h2&gt;

&lt;p&gt;An operator should not call &lt;code&gt;setCountry&lt;/code&gt; directly on the module. The configured compliance owner calls &lt;code&gt;callModuleFunction&lt;/code&gt;, which invokes the module so that its &lt;code&gt;msg.sender&lt;/code&gt; is the bound compliance contract. This arrangement gives the module the correct policy namespace while preserving the compliance contract's ownership check.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/ERC-3643/ERC-3643/blob/2f0704dee9658ad9cd5b6ed82e7427da74c28345/contracts/compliance/modular/ModularCompliance.sol" rel="noopener noreferrer"&gt;ModularCompliance implementation&lt;/a&gt; evaluates its bound modules in &lt;code&gt;canTransfer&lt;/code&gt;. With no modules attached, there is no module-level restriction to reject the transfer. Therefore, a deployment check should assert the intended module set and configured values. A green transaction against an accidentally empty set proves the wrong policy.&lt;/p&gt;

&lt;p&gt;Permissioned issuance requires agreement between investor restrictions and executable rules. Pharos Production describes that mapping, including work with the client's legal team, in its &lt;a href="https://pharosproduction.com/services/tokenization-and-rwa-development/" rel="noopener noreferrer"&gt;tokenization and RWA development process&lt;/a&gt;. The engineering handoff should carry the approved policy into explicit module settings and transaction tests. A country allowlist by itself cannot establish that an instrument or offering meets applicable legal requirements.&lt;/p&gt;

&lt;p&gt;Prepare the configuration receipt before unpausing. Record the token address, registry address, compliance address and module address alongside the owner and agent assignments. Include the transaction that bound the module and the transactions that set permitted countries. Compare the resulting onchain values with the intended configuration, rather than accepting a deployment script's successful exit status as sufficient evidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build a reproducible local fixture
&lt;/h2&gt;

&lt;p&gt;Use a fresh directory with Git, Node.js, npm and Foundry already installed. The following commands fetch the selected source revision and the pinned dependency versions. Preserve the resulting package lockfile with your test evidence. Review the repository's GPL-3.0 licensing and the licenses of imported dependencies before redistributing or combining the code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; erc3643-controls/src erc3643-controls/test erc3643-controls/vendor
&lt;span class="nb"&gt;cd &lt;/span&gt;erc3643-controls
git clone https://github.com/ERC-3643/ERC-3643.git vendor/erc3643
git &lt;span class="nt"&gt;-C&lt;/span&gt; vendor/erc3643 checkout 2f0704dee9658ad9cd5b6ed82e7427da74c28345
npm init &lt;span class="nt"&gt;-y&lt;/span&gt;
npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--save-exact&lt;/span&gt; &lt;span class="nt"&gt;--ignore-scripts&lt;/span&gt; &lt;span class="nt"&gt;--no-audit&lt;/span&gt; &lt;span class="nt"&gt;--no-fund&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  @openzeppelin/contracts@4.8.3 &lt;span class="se"&gt;\&lt;/span&gt;
  @openzeppelin/contracts-upgradeable@4.8.3 &lt;span class="se"&gt;\&lt;/span&gt;
  @onchain-id/solidity@2.0.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save this configuration as &lt;code&gt;foundry.toml&lt;/code&gt;. The remappings are necessary because the tutorial imports the actual suite rather than recreating token behavior in a simplified mock. The suite's source files remain under &lt;code&gt;vendor/erc3643&lt;/code&gt;; the tutorial's module and tests stay in separate directories.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[profile.default]&lt;/span&gt;
&lt;span class="py"&gt;src&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"src"&lt;/span&gt;
&lt;span class="py"&gt;test&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"test"&lt;/span&gt;
&lt;span class="py"&gt;libs&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"vendor"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"node_modules"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="py"&gt;solc_version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0.8.17"&lt;/span&gt;
&lt;span class="py"&gt;optimizer&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;optimizer_runs&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;
&lt;span class="py"&gt;remappings&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="py"&gt;["erc3643/&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="err"&gt;vendor/erc&lt;/span&gt;&lt;span class="mi"&gt;3643&lt;/span&gt;&lt;span class="err"&gt;/contracts/&lt;/span&gt;&lt;span class="s"&gt;", "&lt;/span&gt;&lt;span class="py"&gt;@openzeppelin/&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="err"&gt;node_modules/@openzeppelin/&lt;/span&gt;&lt;span class="s"&gt;", "&lt;/span&gt;&lt;span class="py"&gt;@onchain-id/&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="err"&gt;node_modules/@onchain-id/&lt;/span&gt;&lt;span class="s"&gt;"]&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Below, the complete fixture deploys the actual registry storage, claim-topic registry, trusted-issuer registry, identity registry, modular compliance and token. It initializes each instance, binds registry storage and assigns the agent permissions required by the test. The token receives registry-agent authority for the recovery scenario. Alice and Bob start with registered identities in the permitted country.&lt;/p&gt;

&lt;p&gt;Initialization mints 100 indivisible fixture units to Alice and then unpauses ordinary transfers. Using zero decimals makes the balance assertions easy to inspect; production assets need their own denomination and rounding tests. These are directly initialized local contract instances. The fixture does not deploy the suite's proxy factory or validate an implementation-authority upgrade.&lt;/p&gt;

&lt;p&gt;Save the complete file as &lt;code&gt;test/TransferControls.t.sol&lt;/code&gt;. Every test starts from the same fresh setup, so a policy change in one test cannot make another test pass accidentally.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.17;

import "erc3643/token/Token.sol";
import "erc3643/compliance/modular/ModularCompliance.sol";
import "erc3643/registry/implementation/IdentityRegistry.sol";
import "erc3643/registry/implementation/IdentityRegistryStorage.sol";
import "erc3643/registry/implementation/TrustedIssuersRegistry.sol";
import "erc3643/registry/implementation/ClaimTopicsRegistry.sol";
import "../src/RecipientCountryModule.sol";

interface Vm {
    function prank(address) external;
    function expectRevert() external;
    function expectRevert(bytes calldata) external;
}

// Test doubles: no signature verification, expiry, revocation list or real KYC.
contract IssuerStub {
    function isClaimValid(IIdentity, uint256, bytes calldata, bytes calldata)
        external pure returns (bool) { return true; }
}
contract IdentityStub {
    address public issuer;
    bool public present = true;
    mapping(bytes32 =&amp;gt; bool) public management;
    constructor(address source, address wallet) {
        issuer = source;
        management[keccak256(abi.encode(wallet))] = true;
    }
    function setPresent(bool value) external { present = value; }
    function allowRecovery(address wallet) external {
        management[keccak256(abi.encode(wallet))] = true;
    }
    function keyHasPurpose(bytes32 key, uint256 purpose) external view returns (bool) {
        return purpose == 1 &amp;amp;&amp;amp; management[key];
    }
    function getClaim(bytes32 id) external view
        returns (uint256, uint256, address, bytes memory, bytes memory, string memory)
    {
        if (present &amp;amp;&amp;amp; id == keccak256(abi.encode(issuer, uint256(7))))
            return (7, 1, issuer, hex"01", hex"02", "");
        return (0, 0, address(0), "", "", "");
    }
}

contract TransferControlsTest {
    Vm constant vm = Vm(address(uint160(uint256(keccak256("hevm cheat code")))));
    address constant ALICE = address(0xA11CE);
    address constant BOB = address(0xB0B);
    address constant SPENDER = address(0xCA11);
    address constant NEW_WALLET = address(0xBEEF);
    Token token;
    ModularCompliance compliance;
    RecipientCountryModule policy;
    IdentityRegistry registry;
    ClaimTopicsRegistry topics;
    TrustedIssuersRegistry issuers;
    IssuerStub issuer;
    IdentityStub aliceIdentity;
    IdentityStub bobIdentity;

    function setUp() public {
        topics = new ClaimTopicsRegistry(); topics.init(); topics.addClaimTopic(7);
        issuers = new TrustedIssuersRegistry(); issuers.init();
        issuer = new IssuerStub();
        uint256[] memory required = new uint256[](1); required[0] = 7;
        issuers.addTrustedIssuer(IClaimIssuer(address(issuer)), required);
        IdentityRegistryStorage store = new IdentityRegistryStorage(); store.init();
        registry = new IdentityRegistry();
        registry.init(address(issuers), address(topics), address(store));
        store.bindIdentityRegistry(address(registry)); registry.addAgent(address(this));
        aliceIdentity = new IdentityStub(address(issuer), ALICE);
        bobIdentity = new IdentityStub(address(issuer), BOB);
        registry.registerIdentity(ALICE, IIdentity(address(aliceIdentity)), 250);
        registry.registerIdentity(BOB, IIdentity(address(bobIdentity)), 250);
        compliance = new ModularCompliance(); compliance.init();
        token = new Token();
        token.init(address(registry), address(compliance), "ControlFixture", "CFX", 0, address(0));
        token.addAgent(address(this)); registry.addAgent(address(token));
        policy = new RecipientCountryModule(); compliance.addModule(address(policy));
        setCountry(250, true);
        token.mint(ALICE, 100); token.unpause();
    }
    function setCountry(uint16 country, bool enabled) internal {
        compliance.callModuleFunction(
            abi.encodeCall(RecipientCountryModule.setCountry, (country, enabled)), address(policy));
    }
    function send(uint256 amount) internal {
        vm.prank(ALICE); token.transfer(BOB, amount);
    }
    function deny() internal {
        vm.expectRevert(bytes("Transfer not possible")); send(10);
        require(token.balanceOf(ALICE) == 100 &amp;amp;&amp;amp; token.balanceOf(BOB) == 0, "denial mutated balances");
    }
    function testAllowedTransfer() public {
        send(10); require(token.balanceOf(BOB) == 10 &amp;amp;&amp;amp; token.balanceOf(ALICE) == 90);
    }
    function testDeniedCountry() public { registry.updateCountry(BOB, 276); deny(); }
    function testMissingClaim() public { bobIdentity.setPresent(false); deny(); }
    function testIssuerRemoved() public { issuers.removeTrustedIssuer(IClaimIssuer(address(issuer))); deny(); }
    function testPauseIsOutsideCanTransfer() public {
        token.pause(); require(compliance.canTransfer(ALICE, BOB, 10));
        vm.expectRevert(bytes("Pausable: paused")); send(10);
    }
    function testFrozenSender() public {
        token.setAddressFrozen(ALICE, true); vm.expectRevert(bytes("wallet is frozen")); send(10);
    }
    function testFrozenRecipient() public {
        token.setAddressFrozen(BOB, true); vm.expectRevert(bytes("wallet is frozen")); send(10);
    }
    function testPartialFreezeBoundary() public {
        token.freezePartialTokens(ALICE, 85);
        vm.expectRevert(bytes("Insufficient Balance")); send(16);
        send(15); require(token.balanceOf(ALICE) == 85);
    }
    function testTransferFromAllowance() public {
        vm.prank(ALICE); token.approve(SPENDER, 20);
        vm.prank(SPENDER); token.transferFrom(ALICE, BOB, 10);
        require(token.allowance(ALICE, SPENDER) == 10 &amp;amp;&amp;amp; token.balanceOf(BOB) == 10);
    }
    function testTransferFromDeniedCountry() public {
        registry.updateCountry(BOB, 276);
        vm.prank(ALICE); token.approve(SPENDER, 20);
        vm.expectRevert(bytes("Transfer not possible")); vm.prank(SPENDER);
        token.transferFrom(ALICE, BOB, 10);
        require(token.allowance(ALICE, SPENDER) == 20 &amp;amp;&amp;amp; token.balanceOf(BOB) == 0);
    }
    function testDirectModuleConfigurationDenied() public {
        vm.expectRevert(bytes("only bound compliance can call")); policy.setCountry(276, true);
    }
    function testNonOwnerConfigurationDenied() public {
        vm.expectRevert(bytes("Ownable: caller is not the owner")); vm.prank(BOB);
        compliance.callModuleFunction(abi.encodeCall(RecipientCountryModule.setCountry, (276, true)), address(policy));
    }
    function testForcedTransferBypassesOrdinaryGates() public {
        registry.updateCountry(BOB, 276); token.pause(); token.setAddressFrozen(ALICE, true);
        token.freezePartialTokens(ALICE, 70); token.forcedTransfer(ALICE, BOB, 80);
        require(token.balanceOf(BOB) == 80 &amp;amp;&amp;amp; token.getFrozenTokens(ALICE) == 20);
    }
    function testMintChecksCountry() public {
        registry.updateCountry(BOB, 276);
        vm.expectRevert(bytes("Compliance not followed")); token.mint(BOB, 1);
    }
    function testRecoveryPreservesRestrictions() public {
        aliceIdentity.allowRecovery(NEW_WALLET);
        token.freezePartialTokens(ALICE, 20); token.setAddressFrozen(ALICE, true); token.pause();
        token.recoveryAddress(ALICE, NEW_WALLET, address(aliceIdentity));
        require(token.balanceOf(ALICE) == 0 &amp;amp;&amp;amp; token.balanceOf(NEW_WALLET) == 100);
        require(token.getFrozenTokens(NEW_WALLET) == 20 &amp;amp;&amp;amp; token.isFrozen(NEW_WALLET));
        require(!registry.contains(ALICE) &amp;amp;&amp;amp; registry.investorCountry(NEW_WALLET) == 250);
    }
    function testNonAgentForcedTransferDenied() public {
        vm.expectRevert(); vm.prank(BOB); token.forcedTransfer(ALICE, BOB, 1);
    }
    function testEmptyTopicsAdmitRegisteredIdentity() public {
        bobIdentity.setPresent(false); require(!registry.isVerified(BOB));
        topics.removeClaimTopic(7); require(registry.isVerified(BOB)); send(10);
    }
    function testPolicyChangeInvalidatesPreflight() public {
        require(compliance.canTransfer(ALICE, BOB, 10)); setCountry(250, false); deny();
    }
    function testComplianceContextsAreIsolated() public {
        ModularCompliance second = new ModularCompliance(); second.init();
        second.bindToken(address(token)); second.addModule(address(policy));
        require(compliance.canTransfer(ALICE, BOB, 10));
        require(!second.canTransfer(ALICE, BOB, 10));
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run &lt;code&gt;forge test -vv&lt;/code&gt; from the project directory. The recorded local run on September 30, 2026 compiled the fixture with Solidity 0.8.17 and passed all 19 tests, with no skipped tests. That count describes these named examples. It does not measure exhaustive path coverage, successful mainnet integration or resistance to every adversarial input.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prove that a denial test can fail
&lt;/h2&gt;

&lt;p&gt;A passing rejection test is more persuasive when it detects a known defect. For the recorded negative run, the module's final check was deliberately replaced with &lt;code&gt;return true&lt;/code&gt;. The country-change regression then failed because the expected revert did not occur. Restoring the mapping lookup made that same test pass, after which the complete suite passed again.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;--- deliberately-permissive/RecipientCountryModule.sol
&lt;/span&gt;&lt;span class="gi"&gt;+++ corrected/RecipientCountryModule.sol
&lt;/span&gt;&lt;span class="p"&gt;@@ -20,7 +20,7 @@&lt;/span&gt;
     {
         IToken token = IToken(IModularCompliance(compliance).getTokenBound());
         uint16 country = token.identityRegistry().investorCountry(to);
&lt;span class="gd"&gt;-        return true; // DELIBERATE TEST MUTATION: ignore the recipient country.
&lt;/span&gt;&lt;span class="gi"&gt;+        return allowed[compliance][country];
&lt;/span&gt;     }
&lt;span class="err"&gt;
&lt;/span&gt;     function moduleTransferAction(address, address, uint256)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a mutation of our tutorial module, not an upstream security finding. Keep the failing output, the correction diff and the passing output together. Otherwise a demonstration can accidentally compare different test selections or lose the source change that explains the result. Both runs used &lt;code&gt;forge test --match-test testDeniedCountry -vv&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F21fo4bcj6gdctoxxuypb.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F21fo4bcj6gdctoxxuypb.gif" alt="Recorded test replay: deliberately allowing every country makes testDeniedCountry fail; restoring the compliance-scoped mapping lookup makes it pass, followed by 19 passing tests." width="799" height="371"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;small&gt;Replay of recorded local test output: deliberately permissive tutorial variant, correction and passing checks. The animation summarizes the retained outputs; it is not a terminal screen recording.&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;The denial helper checks unchanged balances after rejected ordinary transfers. The delegated-spending test also checks that a failed &lt;code&gt;transferFrom&lt;/code&gt; leaves its allowance intact. Those assertions connect the error to state, which is more useful than merely observing that some call reverted. Extend this approach to event expectations and downstream accounting when adding stateful modules.&lt;/p&gt;

&lt;p&gt;The context-isolation case deserves a precise reading. It binds the same module to a second compliance instance and confirms that the second instance does not inherit the first instance's country permission. Both compliance instances point to the same token solely to keep this probe small. This is a namespace test, not a rehearsal of two separately deployed instruments with different registries and governance.&lt;/p&gt;

&lt;p&gt;For a production shared-module deployment, create that second instrument explicitly. Change the first instrument's country rule and verify the second instrument's full transfer result remains unchanged. Then reverse the direction of the change. Include distinct owners and registry data so that an accidental cross-reference cannot hide behind identical fixture values. Those additional cases are proposed extensions, not part of the reported 19-test result.&lt;/p&gt;

&lt;h2&gt;
  
  
  Review minting and exceptional movement separately
&lt;/h2&gt;

&lt;p&gt;A single assertion that restrictions work hides differences between entry points. The country rule blocks ordinary receipt in the denied country and also blocks minting there in this selected implementation. Our tests exercise both. Forced transfer follows another path: an authorized agent can move tokens while the token is paused and the sender is frozen, without applying this module's ordinary country check.&lt;/p&gt;

&lt;p&gt;The fixture freezes 70 of Alice's 100 units, then forces a transfer of 80. The asserted result is Bob holding 80 and Alice retaining 20 frozen units. This concrete balance transition is the reviewed behavior, not a generic statement that an agent can ignore everything. Recipient identity verification still matters, and the token invokes the transfer callback after moving the balance.&lt;/p&gt;

&lt;p&gt;Our module's callback is empty. A different module may enforce additional behavior there or revert. Test the complete installed module set before describing forced transfer as guaranteed emergency liquidity. Add explicit negative cases for unauthorized agents and invalid recipients, and test your intended handling of an already frozen recipient. The provided suite covers the unauthorized-agent case; it does not claim those additional recipient cases were executed.&lt;/p&gt;

&lt;p&gt;Burning needs its own supply and accounting tests in a production suite. This example implements the required burn hook but does not include a burn scenario. Similarly, the existence of batch entry points does not establish the desired partial-failure semantics for a custody integration. Enumerate the actual entry points the integration exposes and attach an acceptance case to each.&lt;/p&gt;

&lt;h2&gt;
  
  
  Treat pause and recovery as separate controls
&lt;/h2&gt;

&lt;p&gt;The ERC-3643 authors, including Joachim Lebrun, make pausing an explicit requirement in the &lt;a href="https://eips.ethereum.org/EIPS/eip-3643#specification" rel="noopener noreferrer"&gt;specification&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;MUST have the possibility to pause the token&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For an operator, the useful follow-up is the scope of that pause. Our test proves that &lt;code&gt;compliance.canTransfer&lt;/code&gt; can return true while an ordinary transfer reverts because the token is paused. The forced-transfer test proves that an agent operation can still move a balance in that state. An incident runbook must identify which operations remain available after the pause transaction confirms.&lt;/p&gt;

&lt;p&gt;Recovery is another privileged workflow. The fixture authorizes the replacement wallet through the identity stub's management-key check, then exercises the actual token recovery function. It asserts movement of the full balance, preservation of a 20-unit partial freeze and the wallet's frozen flag, removal of the old registry association and retention of country &lt;code&gt;250&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That result does not rotate credentials across every connected service or remove every old key from the identity contract. A real recovery procedure needs separate authorization evidence and updates to offchain custody records. It also needs a negative suite for an unauthorized replacement wallet. The identity stub deliberately makes the positive management-key condition easy to arrange; it cannot establish the security of a real key-management implementation.&lt;/p&gt;

&lt;p&gt;A recovery review should also trace permissions across contract boundaries. The local setup grants the token permission to change the identity registry because the recovery operation needs that interaction. Checking only the caller's token-agent role would miss this dependency. Remove the supporting registry authority in a separate deployment rehearsal and confirm that recovery fails without leaving a partially moved balance or a stranded registry entry.&lt;/p&gt;

&lt;p&gt;Before restoring service after an incident, compare both balance state and identity state with the approved recovery request. Confirm the replacement address through the organization's established process, then reconcile downstream systems against the final transaction receipt. Reusing a previously approved wallet address from an old support ticket is not sufficient evidence that the current request is authorized.&lt;/p&gt;

&lt;h2&gt;
  
  
  Give preflight results an honest lifetime
&lt;/h2&gt;

&lt;p&gt;A frontend can ask the compliance contract whether its modules would permit a movement. It should label the result accordingly. The check does not establish adequate allowance, an unfrozen wallet or an unpaused token. Our paused-token test makes that limitation visible without depending on an RPC provider or a race between transactions.&lt;/p&gt;

&lt;p&gt;The policy-change test establishes another boundary. It observes a positive compliance check, disables the previously permitted country and then confirms that the ordinary transfer fails. In a deployed system, the state used for a preflight call can change before transaction execution. Avoid turning a simulated result into a promise that the eventual transaction will succeed.&lt;/p&gt;

&lt;p&gt;For a transaction preview, simulate the exact entry point with the intended sender, calldata and value against a documented block context. Show the time or block associated with the result, then handle an execution failure as an expected possibility. Preserve the transaction hash and inspect the mined receipt before updating balances as settled. These are integration recommendations; the local fixture does not test a remote provider or mempool ordering.&lt;/p&gt;

&lt;p&gt;Failure messages should also preserve the distinction between identity, policy and token state. The pinned token combines some rejection conditions under one revert string. An application may need separate read-only diagnostics to explain a failed simulation, but it should not claim a uniquely established cause when several conditions are false. Keep sensitive identity material out of public error messages.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make policy changes visible to reviewers
&lt;/h2&gt;

&lt;p&gt;Changing a trusted issuer, removing a required topic or replacing the compliance contract can alter admissible transfers without changing the token's visible name. Treat those operations as changes to the running system's policy. Maintain an inventory of who can perform each one, including any authority that can upgrade the code implementing those permissions.&lt;/p&gt;

&lt;p&gt;The custom module emits &lt;code&gt;CountryPermissionSet&lt;/code&gt; with the compliance address, country and new value. Retain that context in operational records so that a shared module's event is associated with the right token deployment. Compare observed configuration with an approved baseline after changes. An event stream is useful evidence, but a missed subscription message should not prevent reconciliation from current state and historical logs.&lt;/p&gt;

&lt;p&gt;Choose an approval mechanism appropriate to each authority. A multisignature owner may reduce dependence on one key; a timelock may create review time for a configuration change. Neither control automatically governs an agent that retains a separate immediate operation. Draw the complete authority map and test the exceptional route, including who can remove or replace its operator.&lt;/p&gt;

&lt;p&gt;Treat changing the allowlist as a release even when no Solidity file changes. Save a before-and-after view, simulate a permitted and a denied movement under the new configuration, and state whether existing holders are affected. Assign responsibility for reconciliation if an operator discovers that a registry country value was wrong. This preserves the connection between the administrative decision and the transfers it actually governs.&lt;/p&gt;

&lt;p&gt;The final release receipt should identify the exact source commit, compiler settings, dependency lockfile and deployed bytecode. Attach the configured identities and policy addresses, required topics, trusted issuers, permitted countries and role assignments. Then attach the executed test selection and its limitations. Reviewers need enough information to reproduce the result and enough precision to see what remains untested.&lt;/p&gt;

&lt;p&gt;An ERC-3643 implementation is ready for the next engineering review when each permitted movement and each expected rejection has an explicit explanation tied to selected code and configuration. This fixture supplies that explanation for one recipient-country rule and its tested interactions. Expand it around the instrument's real identity system, deployment architecture and authorized operations before treating it as production acceptance evidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  More insights to read
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.bulbapp.io/p/e2580ef7-324b-442d-b818-92b8c8442b3d/five-smart-contract-upgrade-patterns-compared" rel="noopener noreferrer"&gt;Five Smart Contract Upgrade Patterns Compared&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://medium.com/pharos-production/smart-contracts-their-potential-and-real-limitations-part-2-40942c055d79" rel="noopener noreferrer"&gt;Smart Contracts. Their Potential and Real Limitations. Part 2&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dmytronasyrov.medium.com/upgradeable-solidity-smart-contracts-part-1-versioning-7e6e97cafc28" rel="noopener noreferrer"&gt;Upgradeable Solidity Smart Contracts. Part 1 — Versioning&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://medium.com/pharos-production/smart-contracts-their-potential-and-real-limitations-part-1-222fe44ee14c" rel="noopener noreferrer"&gt;Smart Contracts. Their Potential and Real Limitations. Part 1&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://medium.com/pharos-production/web3-smart-contracts-oracles-part-1-3905b127c01d" rel="noopener noreferrer"&gt;Web3. Smart Contracts. Oracles. Part 1&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  About the author
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7wo81ttokb6meiuexdko.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7wo81ttokb6meiuexdko.jpg" width="112" height="112" alt="Portrait of Dmytro Nasyrov wearing a dark suit and light blue shirt against a dark background."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;small&gt;Dmytro Nasyrov. Photo supplied by the author.&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Written by &lt;a href="https://pharosproduction.com/dmytro-nasyrov/" rel="noopener noreferrer"&gt;Dmytro Nasyrov&lt;/a&gt; PhD, software architect with 24 years of production experience. Dmytro is the founder and CTO of Pharos Production. He works on production software architecture for FinTech, AI, Web3 and blockchain systems.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>solidity</category>
      <category>blockchain</category>
      <category>web3</category>
      <category>testing</category>
    </item>
    <item>
      <title>Verify an Indexer Can Recover from a Chain Reorganization</title>
      <dc:creator>Dmytro Nasyrov</dc:creator>
      <pubDate>Sat, 26 Sep 2026 19:57:43 +0000</pubDate>
      <link>https://dev.to/pharos_production/verify-an-indexer-can-recover-from-a-chain-reorganization-1gn0</link>
      <guid>https://dev.to/pharos_production/verify-an-indexer-can-recover-from-a-chain-reorganization-1gn0</guid>
      <description>&lt;p&gt;An indexer can reach the latest block and still serve data from an abandoned chain. Its checkpoint advances, its health endpoint stays green and its token balance includes a transfer that no longer exists in canonical history. Recovery needs a stronger acceptance test: after replacing a branch, every indexed result must match an independent rebuild of the selected canonical history.&lt;/p&gt;

&lt;p&gt;This tutorial turns that requirement into an executable rehearsal. A small Python and SQLite model indexes signed amounts, replaces an orphaned suffix and checks its state against a separate pure replay function. The fixture finishes at block &lt;code&gt;B4&lt;/code&gt; with a total of &lt;code&gt;24&lt;/code&gt;. Repeating the same input changes nothing. Injecting an exception during recovery leaves the previously committed state intact.&lt;/p&gt;

&lt;p&gt;The example is deliberately bounded. It uses synthetic block identifiers and complete, already selected branches. It does not implement Ethereum consensus, validate block hashes or connect to a node. The database represents one chain and one aggregate. Those limits make the assertions easy to inspect before adapting them to a production indexer with multiple projections and concurrent readers.&lt;/p&gt;

&lt;p&gt;The acceptance condition is useful beyond this model. A wallet requires correct ownership and balances; an analytics product requires correct event history and aggregates. Decide which results users depend on before choosing the recovery mechanism.&lt;/p&gt;

&lt;h2&gt;
  
  
  Define the state that must recover
&lt;/h2&gt;

&lt;p&gt;A checkpoint containing only a height cannot identify a chain. Two competing blocks can occupy the same position. Record the block number together with its hash, then retain enough parent relationships to establish which stored blocks belong to the replacement branch. Treat the checkpoint as a claim about fully committed application state, not merely the most recent RPC response.&lt;/p&gt;

&lt;p&gt;For an Ethereum log pipeline, distinguish an observed event from the transaction that produced it. A useful occurrence key contains the chain identity, block hash, transaction hash and log index. The chain identity can live in the database namespace for a strictly single-chain deployment. A transaction hash alone cannot distinguish its appearances across competing histories.&lt;/p&gt;

&lt;p&gt;The Geth documentation makes the delivery consequence explicit:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“a subscription can emit logs for the same transaction multiple times.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Geth documentation, &lt;a href="https://geth.ethereum.org/docs/interacting-with-geth/rpc/pubsub" rel="noopener noreferrer"&gt;Real-time Events&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That statement concerns notification behavior, not a promise that every disconnected consumer will receive every correction. Design storage so repeated observations cannot duplicate an occurrence, while a transaction appearing in a different block can be represented accurately. Its execution context may have changed; do not carry its old derived output into the replacement block without decoding the new receipt.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://pharosproduction.com" rel="noopener noreferrer"&gt;blockchain software engineering scope at Pharos Production&lt;/a&gt; includes applications whose product behavior depends on off-chain data. A reorganization therefore belongs in the application acceptance criteria: the frontend can display a wrong balance even when every smart contract executed correctly. This tutorial supplies a concrete database exercise for that boundary; it does not claim a measured customer outcome.&lt;/p&gt;

&lt;p&gt;Write the recovery contract in terms of visible state. Canonical block membership, active event rows, materialized aggregates and the checkpoint must agree at a committed boundary. If historical orphan records are retained for audit, mark them explicitly and exclude them from current product queries. Deleting them is only one storage policy.&lt;/p&gt;

&lt;p&gt;Also specify what a reader can observe during recovery. A single database transaction can present an old committed state followed by a new committed state, subject to the database's isolation behavior. An asynchronous projection pipeline needs a visible generation or watermark instead. Otherwise an API can combine the new event table with yesterday's aggregate and return a result that belongs to neither branch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build a fork with a result you can calculate
&lt;/h2&gt;

&lt;p&gt;Use a fixture small enough to audit without trusting the implementation. The shared prefix is &lt;code&gt;G → A1&lt;/code&gt;. The old suffix is &lt;code&gt;A2 → A3&lt;/code&gt;; the replacement suffix is &lt;code&gt;B2 → B3 → B4&lt;/code&gt;. The caller selects the replacement branch. The model never decides that a branch wins merely because it has more blocks.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Block&lt;/th&gt;
&lt;th&gt;Parent&lt;/th&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;th&gt;Signed units&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;G&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;Trusted fixture origin&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A1&lt;/td&gt;
&lt;td&gt;G&lt;/td&gt;
&lt;td&gt;deposit&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;Shared prefix&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A2&lt;/td&gt;
&lt;td&gt;A1&lt;/td&gt;
&lt;td&gt;shared-tx&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Old occurrence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A3&lt;/td&gt;
&lt;td&gt;A2&lt;/td&gt;
&lt;td&gt;orphan-tx&lt;/td&gt;
&lt;td&gt;-2&lt;/td&gt;
&lt;td&gt;Orphan-only event&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B2&lt;/td&gt;
&lt;td&gt;A1&lt;/td&gt;
&lt;td&gt;shared-tx&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Replacement occurrence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B3&lt;/td&gt;
&lt;td&gt;B2&lt;/td&gt;
&lt;td&gt;credit&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;New event&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B4&lt;/td&gt;
&lt;td&gt;B3&lt;/td&gt;
&lt;td&gt;debit&lt;/td&gt;
&lt;td&gt;-4&lt;/td&gt;
&lt;td&gt;Replacement tip&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The old total is &lt;code&gt;10 + 7 - 2 = 15&lt;/code&gt;. Undoing its suffix returns the aggregate to &lt;code&gt;10&lt;/code&gt;. Applying the replacement produces &lt;code&gt;10 + 7 + 11 - 4 = 24&lt;/code&gt;. Keeping the old negative event would leave an incorrect result. Keeping both occurrences of the shared transaction would also fail, even if the checkpoint looked correct.&lt;/p&gt;

&lt;p&gt;These amounts describe a synthetic signed counter. They are not an ERC-20 balance implementation: there are no addresses, decimals, fees or contract-specific event semantics. Use integer quantities in the model so arithmetic noise cannot obscure a branch identity defect. A real token indexer must define how its decoder maps each event into the relevant accounts and units.&lt;/p&gt;

&lt;p&gt;Make the oracle independent of the repair path. The example folds the chosen branch directly into the expected block rows, event rows and total. It never reads the database or calls the rollback code. This catches an indexer that agrees with its own checkpoint while retaining wrong rows. It does not validate a faulty decoder shared by both paths; production replay needs separate golden receipt fixtures for that risk.&lt;/p&gt;

&lt;p&gt;An empty block matters too. It advances chain continuity without changing the aggregate. An indexer that checkpoints only blocks containing matching logs has discarded evidence it needs when locating a common ancestor. Keep headers or equivalent ancestry records for the entire retained recovery interval.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make replacement and checkpoint advancement atomic
&lt;/h2&gt;

&lt;p&gt;The replacement operation first checks that its input is a connected branch from the trusted origin. Each block must extend the previous hash and increment the height by one. It then compares stored block identities with the candidate branch until their shared prefix ends. Everything after that point is subject to replacement.&lt;/p&gt;

&lt;p&gt;Bound the amount of history the automatic path may undo. A rollback window is an operational limit, not a claim that deeper reorganizations are impossible. If recovery needs records outside the retained window, stop the normal writer and preserve evidence. Continuing from a guessed ancestor can make a damaged projection look current.&lt;/p&gt;

&lt;p&gt;Inside one transaction, remove the losing suffix in reverse block order, reverse its aggregate contributions and append the replacement suffix in forward order. Advance the checkpoint only after the new rows and aggregate updates succeed. Committing those changes together is what makes the checkpoint meaningful. Writing it last without a shared transaction is insufficient when earlier writes can persist independently.&lt;/p&gt;

&lt;p&gt;Inverse arithmetic works for this additive projection. It does not automatically work for every business object. Reversing a maximum value, an ownership transition with side effects or an order-dependent state machine may require before-images, versioned entities or a replay from a saved boundary. Test the actual projection algorithm rather than assuming every update has a safe subtraction.&lt;/p&gt;

&lt;p&gt;The code uses explicit SQL transaction commands and disables Python's implicit transaction opening with &lt;code&gt;isolation_level=None&lt;/code&gt;. See the &lt;a href="https://docs.python.org/3/library/sqlite3.html#transaction-control" rel="noopener noreferrer"&gt;Python sqlite3 transaction documentation&lt;/a&gt; for the connection behavior. This choice keeps the demonstrated boundary visible. It is not a database configuration recommendation for every deployment.&lt;/p&gt;

&lt;p&gt;There is one writer in this fixture. A production worker pool needs a fence that prevents an older worker from committing after a newer recovery generation takes ownership. A database lock can serialize writers, but lock acquisition alone does not prove that the worker's fetched branch is still acceptable. Validate the generation or expected checkpoint at the commit boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run the storage model
&lt;/h2&gt;

&lt;p&gt;Save the following as &lt;code&gt;reorg_harness.py&lt;/code&gt;. Block hashes are readable fixture labels; the input contract assumes immutable content for each label. There is no remote I/O inside the transaction. The &lt;code&gt;finalized&lt;/code&gt; argument, when supplied, represents an externally verified anchor that the selected branch must contain.&lt;/p&gt;

&lt;p&gt;The dApp delivery process described by Pharos Production includes indexer strategy during discovery and indexer deployment during production readiness. The &lt;a href="https://pharosproduction.com/services/dapp-development-company/" rel="noopener noreferrer"&gt;dApp development and indexer delivery process&lt;/a&gt; provides a place to assign this rehearsal to a release owner. Passing the local example establishes only the behavior demonstrated below; the deployed storage and RPC adapter still need their own evidence.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sqlite3&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dataclasses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dataclass&lt;/span&gt;

&lt;span class="nd"&gt;@dataclass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frozen&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Block&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="nb"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;tuple&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# (transaction hash, log index, signed units)
&lt;/span&gt;
&lt;span class="n"&gt;G&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Block&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;G&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;A1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Block&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;A1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;G&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;deposit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;),))&lt;/span&gt;
&lt;span class="n"&gt;A2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Block&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;A2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;A1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;shared-tx&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;),))&lt;/span&gt;
&lt;span class="n"&gt;A3&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Block&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;A3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;A2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;orphan-tx&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),))&lt;/span&gt;
&lt;span class="n"&gt;B2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Block&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;B2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;A1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;shared-tx&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;),))&lt;/span&gt;
&lt;span class="n"&gt;B3&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Block&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;B3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;B2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;credit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;),))&lt;/span&gt;
&lt;span class="n"&gt;B4&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Block&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;B4&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;B3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;debit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;),))&lt;/span&gt;
&lt;span class="n"&gt;OLD&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;G&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;A1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;A2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;A3&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;NEW&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;G&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;A1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;B2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;B3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;B4&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Index&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sqlite3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;isolation_level&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;executescript&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
        CREATE TABLE IF NOT EXISTS blocks(
          n INTEGER PRIMARY KEY, hash TEXT UNIQUE, parent TEXT);
        CREATE TABLE IF NOT EXISTS events(
          block_hash TEXT, tx TEXT, idx INTEGER, units INTEGER,
          PRIMARY KEY(block_hash, tx, idx));
        CREATE TABLE IF NOT EXISTS state(
          id INTEGER PRIMARY KEY CHECK(id=1),
          n INTEGER, hash TEXT, total INTEGER);
        INSERT OR IGNORE INTO state VALUES(1,-1,&lt;/span&gt;&lt;span class="sh"&gt;''&lt;/span&gt;&lt;span class="s"&gt;,0);
        &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT * FROM blocks ORDER BY n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;fetchall&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT * FROM events ORDER BY 1,2,3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;fetchall&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT n,hash,total FROM state&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;fetchone&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;sync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;branch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_rewind&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;finalized&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fail_at&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="c1"&gt;# Input is an already selected, immutable full branch from G.
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;branch&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;branch&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;G&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wrong trusted genesis&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;branch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;branch&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:]):&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;height&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;height&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parent&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;broken lineage&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;finalized&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;finalized&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;branch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;branch&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nb"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;finalized anchor conflict&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;
        &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;BEGIN IMMEDIATE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;blocks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;expected_tip&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;blocks&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;][:&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;blocks&lt;/span&gt; &lt;span class="nf"&gt;else &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;expected_tip&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;checkpoint ahead or inconsistent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;common&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;blocks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;branch&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                    &lt;span class="k"&gt;break&lt;/span&gt;
                &lt;span class="n"&gt;common&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;height&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;blocks&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;common&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;max_rewind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rewind limit exceeded&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;reversed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;blocks&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;common&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:]):&lt;/span&gt;
                &lt;span class="n"&gt;removed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT COALESCE(SUM(units),0) FROM events WHERE block_hash=?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;,),&lt;/span&gt;
                &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;fetchone&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
                &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;UPDATE state SET total=total-?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;removed&lt;/span&gt;&lt;span class="p"&gt;,))&lt;/span&gt;
                &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DELETE FROM events WHERE block_hash=?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;,))&lt;/span&gt;
                &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DELETE FROM blocks WHERE n=?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,))&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;fail_at&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;after_undo&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;injected after undo&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;branch&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;common&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:]:&lt;/span&gt;
                &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INSERT INTO blocks VALUES(?,?,?)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                           &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;units&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INSERT INTO events VALUES(?,?,?,?)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                               &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;units&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                    &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;UPDATE state SET total=total+?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;units&lt;/span&gt;&lt;span class="p"&gt;,))&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;fail_at&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;before_checkpoint&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;injected before checkpoint&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;tip&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;branch&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;UPDATE state SET n=?,hash=?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tip&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tip&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;COMMIT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;BaseException&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ROLLBACK&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;oracle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;branch&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# Independent fold: no rollback logic and no database reads.
&lt;/span&gt;    &lt;span class="n"&gt;blocks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;branch&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;units&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;branch&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;units&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;branch&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;branch&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nb"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
             &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;units&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;branch&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;units&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;blocks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;state&lt;/code&gt; row deliberately combines checkpoint identity with the materialized total. A wrong total cannot hide behind a correct height in the expected snapshot. The event table has a composite primary key, so a malformed fixture containing the same occurrence twice aborts the transaction instead of silently applying its amount twice.&lt;/p&gt;

&lt;p&gt;Repeated delivery is tested at the branch synchronization boundary. Synchronizing an already committed branch skips its existing prefix and leaves the state unchanged. This is different from accepting inconsistent duplicate payloads. A duplicate occurrence inside a newly supplied block is rejected, because accepting conflicting representations of immutable history would conceal an upstream defect.&lt;/p&gt;

&lt;p&gt;A full history input keeps the demonstration compact. Production recovery normally fetches only the required headers and suffix, starting from a trusted snapshot or retained boundary. Do not copy the full-history interface into a large deployment without changing its memory, retention and fetch behavior. Preserve the same acceptance conditions when replacing the fixture adapter with a bounded branch reader.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;oracle&lt;/code&gt; function is short enough to inspect separately from &lt;code&gt;sync&lt;/code&gt;. Its event ordering is deterministic, and it derives the total from the selected input.&lt;/p&gt;

&lt;p&gt;Comparing only the final amount would be weaker: unrelated mistakes can cancel numerically. The full snapshot catches a correct total accompanied by stale event identities or an incorrect parent chain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Exercise recovery, refusal and restart
&lt;/h2&gt;

&lt;p&gt;Save this second file as &lt;code&gt;test_reorg_harness.py&lt;/code&gt; beside the first and run &lt;code&gt;python3 -m unittest -v test_reorg_harness.py&lt;/code&gt;. It uses the standard library and a temporary database. The ten test methods passed in the local rehearsal for this article. That result is evidence about these fixtures, not a production throughput or durability measurement.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;tempfile&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;unittest&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;reorg_harness&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Block&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;G&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;A1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;OLD&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;oracle&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RecoveryTests&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;unittest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TestCase&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;setUp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tmp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tempfile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;TemporaryDirectory&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tmp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;index.db&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;OLD&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;tearDown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tmp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cleanup&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_switch_matches_independent_fold&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nf"&gt;oracle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;B4&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_repeated_delivery_is_idempotent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;once&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;once&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_reincluded_transaction_has_new_block_identity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;shared-tx&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;B2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;shared-tx&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;)])&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_restart_after_each_injected_failure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;point&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;after_undo&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;before_checkpoint&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subTest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;point&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;point&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="n"&gt;before&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertRaises&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fail_at&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;point&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;before&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nf"&gt;oracle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;OLD&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_deep_reorg_refuses_without_mutation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;before&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertRaisesRegex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rewind limit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_rewind&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;before&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_finalized_anchor_conflict_refuses&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;before&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertRaisesRegex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;finalized anchor&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;finalized&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;A2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;before&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_bad_checkpoint_refuses&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;UPDATE state SET n=99&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;before&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertRaisesRegex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;checkpoint&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;before&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_broken_parent_refuses&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;before&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertRaisesRegex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lineage&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sync&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;G&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;A1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Block&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;X&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wrong&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)])&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;before&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_shorter_selected_branch_and_empty_block&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;branch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;G&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;A1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Block&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;EMPTY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;A1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;branch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nf"&gt;oracle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;branch&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_duplicate_event_payload_aborts_transaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;before&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;duplicate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sqlite3&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertRaises&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sqlite3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IntegrityError&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sync&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;G&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;A1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Block&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DUP&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;A1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;))])&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;before&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;unittest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;verbosity&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The recovery test checks all modeled state against the independent fold. The re-inclusion test isolates the transaction identity problem: only the occurrence under &lt;code&gt;B2&lt;/code&gt; remains active. The duplicate-delivery test verifies idempotency after a successful commit. These failures deserve separate names because a single final-total assertion would make diagnosis harder.&lt;/p&gt;

&lt;p&gt;The refusal tests establish a second kind of correctness. When the requested rollback exceeds the configured limit, the indexer must leave its committed state unchanged. The same applies to a conflicting finalized anchor or a broken parent relationship. Refusal is useful only if it is observable to the operator and prevents the worker from advertising itself as caught up.&lt;/p&gt;

&lt;p&gt;A corruption fixture changes the saved height to &lt;code&gt;99&lt;/code&gt; before attempting recovery. The operation rejects that inconsistent starting state instead of treating the number as authority. A production corruption check needs to cover more than the tip: missing earlier rows or a damaged aggregate can survive a superficial checkpoint comparison. Periodic reconciliation and storage integrity checks serve that separate purpose.&lt;/p&gt;

&lt;p&gt;A shorter-branch fixture prevents an accidental assumption that height must always increase. It also includes a block with no events. The caller remains responsible for deciding which branch is valid. This test establishes that the storage mechanism can install a supplied connected history, including a shorter one, within its configured rollback limit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Distinguish an exception from a process crash
&lt;/h2&gt;

&lt;p&gt;Two injected failures interrupt the transaction: one after undoing the old suffix, another after applying replacement events but before advancing the checkpoint. Each raises an exception, triggers a rollback and closes the connection. Reopening the database must reveal the original committed state. A subsequent recovery must then reach the oracle result.&lt;/p&gt;

&lt;p&gt;That rehearsal verifies application transaction boundaries. It does not simulate an operating-system kill, a machine restart or loss of durable storage. Claiming crash safety from an exception test would skip precisely the behavior that the database and deployment environment contribute. Keep those evidence categories separate in the release record.&lt;/p&gt;

&lt;p&gt;For a process-level extension, run the real worker against an isolated test database and terminate it at instrumented boundaries. Reopen through the normal startup path, then compare its state with the saved canonical fixture. Include termination immediately after commit but before the worker acknowledges success. Retrying that work must not duplicate rows or external deliveries.&lt;/p&gt;

&lt;p&gt;Use deterministic barriers rather than timing guesses. A test coordinator can wait until a worker reports that it has reached a specified boundary, then trigger the failure. Record whether the signal occurs before a write, after a write or after commit. A sleep followed by termination rarely establishes which transaction state was actually exercised.&lt;/p&gt;

&lt;p&gt;Repeat this with the production database engine, transaction isolation and deployment configuration. If projections live in separate stores, one local transaction cannot provide atomicity across all of them. A versioned publication barrier or an explicitly reconciled workflow must cover that gap. Test readers as well as writers: the API should expose a consistent generation or an honest recovery status while downstream projections catch up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reconcile the provider before trusting the next event
&lt;/h2&gt;

&lt;p&gt;An event subscription is a useful notification channel, but it should not be the only recovery record. Geth documents connection-bound subscriptions and the absence of historical event delivery. After reconnecting, reconcile the saved checkpoint against the node's current canonical chain and backfill the required interval. Receiving a new notification proves that the connection works; it does not prove that the gap was empty.&lt;/p&gt;

&lt;p&gt;Ethereum's &lt;a href="https://ethereum.org/developers/docs/apis/json-rpc/" rel="noopener noreferrer"&gt;JSON-RPC reference&lt;/a&gt; exposes block identity and parent identity, along with log fields and block-based lookup methods. An adapter should preserve those identities throughout acquisition. A numeric range fetched while the head changes can otherwise mix observations from different histories. Test the adapter with deliberately inconsistent responses, including a receipt whose block identity does not match the expected header.&lt;/p&gt;

&lt;p&gt;One practical acquisition contract is to capture a target block identity, walk its parent-linked history and verify each returned object against the expected hash. Revalidate the target against the provider's canonical view before publishing the recovered generation. If the target changed, abandon or restart that attempt according to the adapter's bounded retry policy. The chain can change again later, so repeated reconciliation remains necessary.&lt;/p&gt;

&lt;p&gt;Where the provider supports log queries restricted to a block hash, use that identity to reduce ambiguity. Where it only supplies ranges, validate each result and recheck the associated headers. An empty result is not automatically proof of complete history: the provider may have limited retention, truncated a response or failed a request. Preserve explicit completion evidence for the fetch contract your provider actually offers.&lt;/p&gt;

&lt;p&gt;Add a second fork during recovery to the adapter suite. Also test reconnects, duplicate notifications and switching to a provider with a different observed head. The desired result is either a coherent committed generation or a bounded refusal. A mixture assembled from two provider views should never pass merely because all requested heights were returned.&lt;/p&gt;

&lt;h2&gt;
  
  
  Give finality and external effects separate policies
&lt;/h2&gt;

&lt;p&gt;Confirmation depth and protocol finality answer different questions. A configurable number of later blocks is an application risk policy; it is not a universal finality guarantee. Ethereum's &lt;a href="https://ethereum.org/developers/docs/consensus-mechanisms/pos/gasper/" rel="noopener noreferrer"&gt;Gasper explanation&lt;/a&gt; describes how justified and finalized checkpoints relate to fork choice. Map your application's publication boundary to the actual chain and provider semantics you operate.&lt;/p&gt;

&lt;p&gt;The JSON-RPC API includes &lt;code&gt;safe&lt;/code&gt; and &lt;code&gt;finalized&lt;/code&gt; block tags, but a product spanning multiple networks must verify support and meaning for each network. In particular, a rollup's observed execution head and its settlement conditions require network-specific treatment. Do not reuse an Ethereum execution-layer assumption as proof of settlement on another system.&lt;/p&gt;

&lt;p&gt;The model's finalized-anchor check is intentionally conservative. If the selected branch conflicts with the externally supplied anchor, it refuses mutation. An operator then investigates the provider, chain identity and saved anchor. The model does not explain how to resolve a consensus failure or choose between conflicting trusted sources.&lt;/p&gt;

&lt;p&gt;A database rollback also cannot retract an email already delivered or automatically reverse an action in another system. Store outbound work with its originating event identity and recovery generation. Decide which effects wait for the relevant finality policy, which can be canceled before dispatch and which require a separately authorized compensation process after dispatch.&lt;/p&gt;

&lt;p&gt;Test the dispatcher race explicitly. A queued event can become orphaned between selection and delivery. A local validity check narrows that window but cannot make a remote action atomic with chain consensus. For consequential effects, the acceptance contract must state the remaining exposure and the permitted response. Avoid promising exactly-once real-world behavior from a database uniqueness constraint.&lt;/p&gt;

&lt;h2&gt;
  
  
  Retain the evidence needed for the next rollback
&lt;/h2&gt;

&lt;p&gt;History retention sets a practical limit on recovery. If the database keeps only current entity values, it may lack the information required to reverse an old transition. Define the retained header interval together with the event journal, decoder versions and any projection snapshots. Keeping headers longer than the data needed to rebuild their effects does not extend the usable recovery window.&lt;/p&gt;

&lt;p&gt;Rehearse pruning as part of the lifecycle. Build a snapshot at a verified boundary, retain the required suffix and prove that replay from that snapshot produces the same state as the full fixture. Then present a fork whose ancestor lies before the retained boundary. The expected result is an explicit recovery-plan failure, not a partial rewind followed by a successful health check.&lt;/p&gt;

&lt;p&gt;Keep operational watermarks separate from retention policy. A consumer finishing a block does not necessarily mean every dependent projection or outbound queue has finished using its evidence. Document which components must acknowledge a generation before its journal becomes eligible for pruning. Otherwise a routine cleanup can remove the very rows an incident runbook assumes are available.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep a release receipt that can be rerun
&lt;/h2&gt;

&lt;p&gt;A useful recovery receipt identifies the software revision, database configuration and fixture hashes. It records the old checkpoint, common ancestor and selected target, then stores the expected and observed projection snapshots. Save a structured diff when they disagree. A log line announcing completion cannot replace that comparison.&lt;/p&gt;

&lt;p&gt;Measure operational behavior separately from logical correctness. Record the rollback depth, recovery duration, backlog and time until each public projection reaches the recovered generation. Set budgets from the product's requirements and actual workload. The tiny fixture here supplies no defensible latency target for a deployed indexer.&lt;/p&gt;

&lt;p&gt;Before release, run the same acceptance contract through the real decoder, storage adapter and API read path. Include a replay from a retained snapshot so missing historical data becomes visible before an incident. Confirm that an operator can recognize a refusal, preserve evidence and resume from an approved boundary without editing the checkpoint by hand.&lt;/p&gt;

&lt;p&gt;The release gate is concrete: the supported reorganization cases converge to independently expected state, repeated work has no additional effect and interrupted work resumes without exposing mixed generations. Unsupported depth or conflicting finality evidence produces an explicit stop. Keep the failed fixtures as regression tests; they describe the exact circumstances under which a green health check once failed to tell the whole story.&lt;/p&gt;

&lt;h2&gt;
  
  
  More insights to read
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.bulbapp.io/p/e2580ef7-324b-442d-b818-92b8c8442b3d/five-smart-contract-upgrade-patterns-compared" rel="noopener noreferrer"&gt;Five Smart Contract Upgrade Patterns Compared&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dmytronasyrov.medium.com/upgradeable-solidity-smart-contracts-part-1-versioning-7e6e97cafc28" rel="noopener noreferrer"&gt;Upgradeable Solidity Smart Contracts. Part 1 — Versioning&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://medium.com/pharos-production/web3-smart-contracts-oracles-part-1-3905b127c01d" rel="noopener noreferrer"&gt;Web3. Smart Contracts. Oracles. Part 1&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://medium.com/pharos-production/smart-contracts-their-potential-and-real-limitations-part-1-222fe44ee14c" rel="noopener noreferrer"&gt;Smart Contracts. Their Potential and Real Limitations. Part 1&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://medium.com/pharos-production/smart-contracts-their-potential-and-real-limitations-part-2-40942c055d79" rel="noopener noreferrer"&gt;Smart Contracts. Their Potential and Real Limitations. Part 2&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  About the author
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F10rbqvs1846bo9ibwb4j.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F10rbqvs1846bo9ibwb4j.jpg" width="112" height="112" alt="Portrait of Dmytro Nasyrov wearing a dark suit and light blue shirt against a dark background."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;small&gt;Dmytro Nasyrov. Photo supplied by the author.&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Written by &lt;a href="https://pharosproduction.com/dmytro-nasyrov/" rel="noopener noreferrer"&gt;Dmytro Nasyrov&lt;/a&gt; PhD, software architect with 24 years of production experience. Dmytro is the founder and CTO of Pharos Production. He works on production software architecture for FinTech, AI, Web3 and blockchain systems.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>blockchain</category>
      <category>testing</category>
      <category>database</category>
      <category>web3</category>
    </item>
    <item>
      <title>Healthcare RAG: Test the Claim-to-Source Contract</title>
      <dc:creator>Dmytro Nasyrov</dc:creator>
      <pubDate>Sat, 26 Sep 2026 08:18:29 +0000</pubDate>
      <link>https://dev.to/pharos_production/healthcare-rag-test-the-claim-to-source-contract-2036</link>
      <guid>https://dev.to/pharos_production/healthcare-rag-test-the-claim-to-source-contract-2036</guid>
      <description>&lt;p&gt;A healthcare RAG answer can carry a working citation link and still fail review. The page exists, the passage loads and the answer sounds reasonable. Yet the passage may support only half the sentence, belong to an older edition or describe a different population. A link check cannot resolve those failures.&lt;/p&gt;

&lt;p&gt;Before accepting a retrieval-augmented generation feature, specify a claim-to-source contract: what the product retains about each assertion, which checks it performs and what the user sees when a check fails. Treat that contract as a deliverable shared by ingestion, retrieval, answer rendering and review. The schema and acceptance cases below describe a proposed implementation for an educational healthcare knowledge product. They are not a clinical validation or a report of a deployed system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with an assertion that can fail
&lt;/h2&gt;

&lt;p&gt;Consider a fictional learning assistant whose approved handbook describes how a training module is organized. A generated answer says that the module has an introductory lesson and an assessment. The cited passage describes the lesson but says nothing about assessment. Every component can appear healthy: ingestion succeeded, retrieval returned a relevant passage and the link opens the right page.&lt;/p&gt;

&lt;p&gt;The unsupported addition is still visible to the learner.&lt;/p&gt;

&lt;p&gt;Your first acceptance rule should therefore operate on assertions, rather than on paragraphs that happen to end with a reference. Divide independently checkable statements into separate claims. Retain which passage supports each one, including any qualification needed to keep its meaning intact.&lt;/p&gt;

&lt;p&gt;Do not assume one sentence equals one claim. A sentence can combine a population, a benefit and a comparison. Conversely, one claim may require two passages read together. Let the representation support a set of evidence references and explain why that set is sufficient. A mechanical requirement for exactly one reference would force the content into the wrong shape.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://arxiv.org/abs/2305.14627" rel="noopener noreferrer"&gt;ALCE research paper&lt;/a&gt; evaluates fluency, correctness and citation quality as separate dimensions. That distinction is useful for an acceptance plan: polished output and correct links are insufficient evidence of supported claims. The practical design decision here is to retain those checks separately so an attractive answer cannot hide a failed evidence check.&lt;/p&gt;

&lt;p&gt;For teams commissioning this work, the boundary between a prototype and an implementation should also appear in the statement of work. The &lt;a href="https://pharosproduction.com" rel="noopener noreferrer"&gt;software product development scope&lt;/a&gt; should identify which evidence interactions are demonstrated and which are backed by working retrieval. A reviewer needs to know whether an opened passage was handpicked for the demo or selected by the system for that request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep evidence identity separate from its location
&lt;/h2&gt;

&lt;p&gt;A source URL is a locator.&lt;/p&gt;

&lt;p&gt;It is not enough to identify the exact material behind a saved answer. The same address can serve a corrected document tomorrow. A source record should distinguish the publisher's identifier, the retrieved version and the particular passage used in an answer.&lt;/p&gt;

&lt;p&gt;For a document corpus, retain a content fingerprint with the ingestion run. Bind extracted passages to that fingerprint and to an extraction version. A new extraction can change reading order or table structure even when the original file has not changed. Without both identities, a reviewer may be comparing an old answer against new extracted text without realizing it.&lt;/p&gt;

&lt;p&gt;Page labels need similar care. A viewer's page index and the number printed on a book page can differ. Front matter can use a different numbering system. Store each independently and verify their mapping during ingestion. A fixed subtraction is appropriate only when the source actually has a verified fixed offset. Otherwise use an explicit page map, including pages without printed labels.&lt;/p&gt;

&lt;p&gt;The passage should retain enough context to inspect its qualifications. Extracting a sentence while dropping a table heading can remove the population or unit that makes it meaningful. Preserve the relevant parent heading, table headers and footnotes with a reference to the original location. If extraction cannot capture the context, mark that passage for review before using it as answer evidence.&lt;/p&gt;

&lt;p&gt;Here is a compact record shape. Values are synthetic and the identifiers intentionally describe test fixtures. This is a data contract, not a ready-made clinical schema:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"answer_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"fixture-answer-01"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"answer_version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"corpus_snapshot_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"fixture-corpus-a"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"policy_version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"education-only-v1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"claims"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"claim_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"c1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"The module contains an introductory lesson."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"evidence_refs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"p1"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"support_status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pending_review"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"passages"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"passage_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"p1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"source_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"fixture-handbook"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"source_version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"fixture-edition-a"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"content_fingerprint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"fixture-fingerprint-a"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"extraction_version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"fixture-extractor-a"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"source_class"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"handbook"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"locator"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"viewer_page"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"printed_page"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"3"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"This module begins with an introductory lesson."&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"review"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In an implementation, validate the fingerprint's real algorithm and format; the fixture string above is only a readable stand-in. Likewise, a support label is a decision to justify. It does not become true because a generator emitted the field. Store the verifier or reviewer decision separately and bind it to the answer version it actually examined.&lt;/p&gt;

&lt;h2&gt;
  
  
  Give structural checks a narrow job
&lt;/h2&gt;

&lt;p&gt;Deterministic validation can establish whether referenced records exist, whether identifiers are unique and whether a page mapping resolves within the identified document. It can reject an answer with a dangling passage reference before the renderer builds a citation marker. Those are useful guarantees because they have precise failure conditions.&lt;/p&gt;

&lt;p&gt;This Python function implements only the reference-integrity portion, assuming the record has already passed type and required-field validation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;reference_errors&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;claims&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claims&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;passages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;passages&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;errors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;claim_ids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claim_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;claims&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;passage_ids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;passage_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;passages&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;claim_ids&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;claim_ids&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;duplicate_claim_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;passage_ids&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;passage_ids&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;duplicate_passage_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;known&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;passage_ids&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;claim&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;claims&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;refs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;claim&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;evidence_refs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;refs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;missing_evidence_ref&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ref&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;known&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;ref&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;refs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dangling_evidence_ref&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I checked this function locally against the synthetic receipt and four mutations: a duplicated claim identifier, a duplicated passage identifier, an empty reference list and an unknown reference. The control returned an empty list; each mutation returned its corresponding error. Those five checks establish the illustrated reference behavior only. They do not verify source authenticity, passage meaning, access policy or page mapping. An empty claim list also needs a separate product rule if the caller expects an answer.&lt;/p&gt;

&lt;p&gt;String matching cannot establish that a clinical statement is justified. Exact copying can omit a negation immediately before the excerpt. A correct paraphrase can share few words with its source. Keep a distinct semantic review stage with a written rubric for support, contradiction and unresolved cases.&lt;/p&gt;

&lt;p&gt;For the fictional fixture, a structural check should resolve &lt;code&gt;c1&lt;/code&gt; to &lt;code&gt;p1&lt;/code&gt;, confirm the passage belongs to the declared corpus snapshot and verify that its locator opens the intended source version. A semantic review should compare the claim with the complete passage context. Adding an assessment to the claim should fail that review while leaving the structural result unchanged.&lt;/p&gt;

&lt;p&gt;This separation makes failures diagnosable. If the source cannot be opened, investigate source availability or access. If the wrong page opens, investigate the locator. If the correct page opens but does not support the claim, investigate generation or the support decision. Routing every problem into one hallucination counter discards information the team needs to repair it. Define the review unit before calculating any percentage. Citation completeness asks whether claims that need evidence have it. Support correctness asks whether the attached evidence supports those claims. A dashboard should name the denominator and preserve unresolved cases rather than silently removing them. Otherwise a system can improve its reported score by making difficult claims disappear from the evaluation set.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make source authority an explicit constraint
&lt;/h2&gt;

&lt;p&gt;A handbook explanation, a research finding and a regulatory record answer different questions. A product should preserve those source classes through extraction and rendering, instead of reducing them to interchangeable links. The reviewer must be able to tell what kind of evidence is attached before interpreting what it establishes.&lt;/p&gt;

&lt;p&gt;An article about a device cannot stand in for a regulator's record of that device's status. A regulatory record cannot, by itself, support every claim about comparative effectiveness. In your source policy, define which class may support which assertion type and require review when an answer crosses that boundary. Keep jurisdiction and retrieval date visible where they affect interpretation.&lt;/p&gt;

&lt;p&gt;This distinction appeared during Pharos Production's healthcare AI design work. Our published case reports that an audit found 6 of 7 demo citations wrong, including 4 invented citations. These were findings about our own mockups, not measurements of a deployed model. The response included verified source passages and separate presentation of handbook, research and regulatory evidence.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://pharosproduction.com/insights/healthcare/ai-citation-ux-aesthetic-medicine/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=healthcare_citation_ux_20260926&amp;amp;utm_content=claim_source_contract" rel="noopener noreferrer"&gt;healthcare AI citation UX case study&lt;/a&gt; shows the source audit and illustrative screens behind those decisions, including the work still outstanding. Use it when reviewing an implementation proposal: compare the proposed evidence panel with the concrete design problems the case documents. The engagement remains in the design phase; external retrieval connectors shown in the demo are planned, not live.&lt;/p&gt;

&lt;p&gt;For an engineering team, the next step is to turn a chosen source policy into fixtures. Create a claim that uses the right passage under the wrong source class. The acceptance result must expose that mismatch even if the text appears relevant. Source authority should survive a rendering refactor and a retrieval-provider change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separate support from permission to answer
&lt;/h2&gt;

&lt;p&gt;A supported statement can still be inappropriate for the product's intended use. An educational assistant should not drift into patient-specific treatment instructions because a passage contains information that looks relevant. Keep the source-support decision separate from the policy decision governing what this product may present.&lt;/p&gt;

&lt;p&gt;That separation needs visible states. A missing source, an unavailable source service and a request outside the product's permitted scope are different situations. The user needs a different explanation and next action for each. Internally, retaining separate reasons prevents the team from trying to fix a policy refusal by increasing the number of retrieved passages.&lt;/p&gt;

&lt;p&gt;The FDA's January 29, 2026, guidance discusses enabling a healthcare professional to "independently review the basis" for recommendations. That phrase concerns one part of the US non-device clinical decision support criteria; a citation interface alone does not establish a product's regulatory classification. The &lt;a href="https://www.fda.gov/media/109618/download" rel="noopener noreferrer"&gt;current FDA guidance&lt;/a&gt; is the source for that boundary, rather than a certification supplied by this design pattern.&lt;/p&gt;

&lt;p&gt;For the proposed educational product, define a policy result alongside the support result. A fully supported explanation can proceed only when its use is allowed. An unresolved support decision should remain visibly unresolved. A denied use should select the intended boundary response even if retrieval found relevant material. Have the responsible domain owner approve that behavior before teams encode it in templates.&lt;/p&gt;

&lt;p&gt;The priority between states also matters. Suppose retrieval times out on a request the product must not fulfill. Retrying the request automatically could produce prohibited output when the service recovers. Evaluate the intended-use boundary independently and avoid making an infrastructure error the only reason the answer was withheld.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build a small fixture suite with explicit oracles
&lt;/h2&gt;

&lt;p&gt;Start with a bounded corpus you are entitled to use and questions whose expected evidence can be inspected. Each fixture needs a reason it belongs in the suite, an expected user-visible outcome and an owner for disagreements. A collection of convenient questions without answerability labels will not reveal whether the product refuses too often.&lt;/p&gt;

&lt;p&gt;Keep synthetic examples visibly synthetic.&lt;/p&gt;

&lt;p&gt;Use neutral learning-content statements for plumbing tests so a malformed fixture cannot be mistaken for medical advice. Domain-specific acceptance cases need appropriately qualified review and controlled source material. Do not copy sensitive patient information into a shared test repository simply because a scenario would look more realistic.&lt;/p&gt;

&lt;p&gt;The following matrix is a proposed starting point. Its rows represent different failure classes, not a measured test result or a completeness claim:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Fixture&lt;/th&gt;
&lt;th&gt;Deliberate condition&lt;/th&gt;
&lt;th&gt;Required observable result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Supported control&lt;/td&gt;
&lt;td&gt;Approved learning claim with an exact supporting passage&lt;/td&gt;
&lt;td&gt;Claim displays and its marker opens the correct version and location&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unsupported addition&lt;/td&gt;
&lt;td&gt;Add an assertion absent from the otherwise relevant passage&lt;/td&gt;
&lt;td&gt;Added assertion is withheld or enters review; the supported part remains distinguishable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Locator mismatch&lt;/td&gt;
&lt;td&gt;Keep the passage text but point to another printed page&lt;/td&gt;
&lt;td&gt;Citation validation fails; the interface does not present the location as verified&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Authority mismatch&lt;/td&gt;
&lt;td&gt;Attach an assertion to a source class that cannot establish it&lt;/td&gt;
&lt;td&gt;Source-policy failure is visible and retained in the receipt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Source unavailable&lt;/td&gt;
&lt;td&gt;Make the evidence service unavailable for the request&lt;/td&gt;
&lt;td&gt;An availability state appears; no fabricated passage fills the gap&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Covered question missed&lt;/td&gt;
&lt;td&gt;A gold passage exists but the retriever misses it&lt;/td&gt;
&lt;td&gt;Evaluation records a retrieval miss rather than treating the refusal as success&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Intended-use boundary&lt;/td&gt;
&lt;td&gt;Ask the educational product for a patient-specific directive&lt;/td&gt;
&lt;td&gt;Approved boundary response appears, independent of source availability&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Changed source&lt;/td&gt;
&lt;td&gt;Replace a source version behind a saved answer&lt;/td&gt;
&lt;td&gt;The saved evidence remains identifiable and its review status is reconsidered&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Run the supported control alongside negative cases. A system that suppresses every answer can pass many refusal tests while failing its main purpose. The covered-question fixture provides a second safeguard: the absence of retrieved evidence does not prove the absence of evidence in the corpus.&lt;/p&gt;

&lt;p&gt;For each failure, check both the visible result and the retained record. A correct warning with a misleading success status in the receipt is still a defect. A correct internal rejection with an unsupported claim left on screen is also a defect. The acceptance oracle spans the service boundary and the rendered answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Exercise the contract through the user interface
&lt;/h2&gt;

&lt;p&gt;Component tests should prove that a claim marker opens its bound passage, rather than whichever passage happens to occupy the same array position. Reorder the evidence list while preserving its identifiers. The marker must still lead to the same evidence. This catches a simple implementation mistake with consequences that a visually unchanged snapshot may miss.&lt;/p&gt;

&lt;p&gt;Also test the path back to the answer. After inspecting a passage, a reader should retain the context of the claim they were checking. If the interface opens a large document at its beginning, the citation may be technically reachable but practically difficult to review. Record that as a usability finding instead of converting it into an invented accuracy metric.&lt;/p&gt;

&lt;p&gt;Use representative intended users for that review. Ask them to locate the evidence, explain what it supports and identify a qualification. Observe where they mistake source provenance for review approval or assume a familiar publisher validates the whole answer. The product owner should decide what failure in that task blocks acceptance; a developer should not infer the threshold from a click-through rate.&lt;/p&gt;

&lt;p&gt;Mobile layouts deserve the same evidence identity checks. A compact popover may omit the source edition that appears in a desktop rail. A downloaded or copied answer may lose the marker entirely. Specify which export formats preserve traceability, then verify them. If a format cannot preserve it, the exported artifact should make that limitation apparent to its reader.&lt;/p&gt;

&lt;p&gt;Accessibility belongs in the acceptance scope too. A visual connection drawn by color alone does not tell every reader which claim is selected. Give markers descriptive names, support keyboard navigation and verify focus behavior when an evidence panel opens and closes. These are proposed product requirements that need testing in the actual implementation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Invalidate review when its inputs change
&lt;/h2&gt;

&lt;p&gt;A review decision should refer to an answer version, its evidence versions and the policy under which it was accepted. Changing any of those inputs can invalidate the decision. Avoid a permanent approved flag attached only to the conversation, because later answers can inherit approval for content nobody reviewed.&lt;/p&gt;

&lt;p&gt;For example, a reviewer accepts the synthetic lesson claim against edition A. An editor publishes edition B and moves the passage. Keep the old answer and its original evidence identity available under the applicable retention policy. Create a new review event for the changed source relationship. Do not silently rewrite the old receipt until it appears to describe edition B all along.&lt;/p&gt;

&lt;p&gt;Some changes require semantic review; others need narrower checks. A viewer-coordinate correction with unchanged passage content may need locator verification. A changed qualification requires reassessment of support. A new intended-use policy requires reassessment of permission even if the text is identical. Write these invalidation rules down so routine maintenance has predictable scope.&lt;/p&gt;

&lt;p&gt;The same rule applies to model and retrieval changes. Passing the contract tests on a new configuration establishes only the tested behavior for that configuration and fixture set. Preserve the model identifier and relevant retrieval settings with the run. If a provider cannot guarantee identical future behavior behind an identifier, record that reproducibility limit rather than promising exact replay.&lt;/p&gt;

&lt;h2&gt;
  
  
  Budget for the review work the schema cannot perform
&lt;/h2&gt;

&lt;p&gt;Mechanical checks are cheap to repeat once the representation is stable. Semantic review has a different cost and requires an agreed rubric. An automated verifier can help prioritize disagreements, but its output needs evaluation against the domain reviewers' judgments. Agreement on familiar examples does not establish reliability on unsupported, ambiguous or contradictory ones.&lt;/p&gt;

&lt;p&gt;Assign ownership at the point where a decision can become stuck. The ingestion owner resolves missing context. The product owner decides whether the intended user can complete the evidence task. The domain reviewer judges whether the claim is supported within its scope. Engineering preserves those decisions and makes the software follow them. One undifferentiated review queue makes these responsibilities harder to see. Set an unresolved-case policy before measuring performance. If reviewers disagree about whether a passage supports a claim, keep that uncertainty in the fixture metadata. It can be a useful test of the escalation route. Forcing consensus merely to obtain a tidy score removes the scenario that needs the clearest product behavior.&lt;/p&gt;

&lt;p&gt;Measure operational burden as well as answer quality. Count cases awaiting review and record why they remain open. Track whether the evidence panel gives reviewers enough context to decide without searching an unrelated document store. Use those observations to improve the workflow; do not describe shorter handling time as proof of safer clinical decisions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Retain evidence without retaining everything
&lt;/h2&gt;

&lt;p&gt;An auditable answer does not require copying every input into an unrestricted log. Decide which records belong in the evidence store, which belong in a restricted operational system and which should not be retained. The educational fixture can be public because it contains invented learning content. A real request may carry information that changes those decisions.&lt;/p&gt;

&lt;p&gt;Give the receipt stable references to protected records where appropriate. Apply access checks when the evidence panel resolves them. A citation should not become an alternate route around the permissions that protect the underlying document.&lt;/p&gt;

&lt;p&gt;Test this with an authorized reader and a reader who cannot access the same source; the second reader must not receive the passage through a cached answer.&lt;/p&gt;

&lt;p&gt;Retention also creates an acceptance edge case. If the supporting document is no longer available under the applicable policy, the product cannot honestly present the saved answer as fully inspectable. Preserve the reason for that limitation without inventing a replacement passage. The owner needs to decide whether the answer remains visible with a warning, becomes restricted or is removed through the established retention process.&lt;/p&gt;

&lt;p&gt;Agree on that behavior before promising reproducibility in a delivery contract. Exact replay may depend on source licenses, retained snapshots and provider behavior outside the application's control. A useful receipt states what can still be verified today and what was verified at the original review. Those are different claims, and a buyer should be able to distinguish them without investigating your storage architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  Put the evidence contract in the delivery packet
&lt;/h2&gt;

&lt;p&gt;For a healthcare AI development proposal, request the schema, the source-version policy and the fixture matrix together. Require a sample answer receipt that can be followed from the rendered claim to its source passage. Ask for both a passing control and a failure that remains visible to the user. Those artifacts make an implementation scope concrete enough to inspect.&lt;/p&gt;

&lt;p&gt;The packet should also name what has not been demonstrated. A clickable prototype, a working ingestion job and a tested retrieval path are different deliverables. Record which one exists, which sources it uses and who accepted its limits. That gives the buyer a basis for deciding what the next development milestone must prove.&lt;/p&gt;

&lt;p&gt;Before commissioning the next phase, bring an authorized sample corpus, a representative learning task and a named review owner to the technical discussion. Use the case linked above to examine the discovery work, then use this contract to ask how the implementation will preserve it. A credible proposal should identify the first claim it will prove, the fixture that can break it and the person responsible for resolving the failure.&lt;/p&gt;

&lt;h2&gt;
  
  
  More insights to read
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://hackmd.io/@dmytro-nasyrov/rag-citation-audit-claim-source-evidence" rel="noopener noreferrer"&gt;Make RAG Citations Auditable, One Claim at a Time&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pharosengineeringnotes.wordpress.com/2026/09/19/choose-rag-supplier-retrieval-evaluation-evidence/" rel="noopener noreferrer"&gt;How to Choose a RAG Supplier by Retrieval Evaluation Evidence&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pharosengineeringnotes.wordpress.com/2026/09/18/rag-vendor-retrieval-evaluation-deliverables/" rel="noopener noreferrer"&gt;How to Specify Retrieval Evaluation Deliverables for a RAG Vendor&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dmytronasyrov.substack.com/p/enterprise-rag-procurement-brief" rel="noopener noreferrer"&gt;How to Write an Enterprise RAG Procurement Brief&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dmytronasyrov.substack.com/p/choose-rag-development-partner-before-paid-pilot" rel="noopener noreferrer"&gt;How to Choose a RAG Development Partner Before a Paid Pilot&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  About the author
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feaenhy6t8vj9yop1fx91.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feaenhy6t8vj9yop1fx91.jpg" width="112" height="112" alt="Portrait of Dmytro Nasyrov wearing a dark suit and light blue shirt against a dark background."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;small&gt;Dmytro Nasyrov. Photo supplied by the author.&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Written by &lt;a href="https://pharosproduction.com/dmytro-nasyrov/" rel="noopener noreferrer"&gt;Dmytro Nasyrov&lt;/a&gt; PhD, software architect with 24 years of production experience. Dmytro is the founder and CTO of Pharos Production. He works on production software architecture for FinTech, AI, Web3 and blockchain systems.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>rag</category>
      <category>testing</category>
      <category>architecture</category>
    </item>
    <item>
      <title>ISO 20022 Address Migration Is a Data-Lineage Problem</title>
      <dc:creator>Dmytro Nasyrov</dc:creator>
      <pubDate>Thu, 24 Sep 2026 05:12:22 +0000</pubDate>
      <link>https://dev.to/dmytronasyrov/iso-20022-address-migration-is-a-data-lineage-problem-5eo7</link>
      <guid>https://dev.to/dmytronasyrov/iso-20022-address-migration-is-a-data-lineage-problem-5eo7</guid>
      <description>&lt;p&gt;A payment can carry a plausible town and country while nobody can explain where either value came from. The XML may validate. The address may look cleaner than the original. Yet the migration has created an operational liability: an investigator cannot distinguish customer supplied data from a parser's guess, or reproduce the decision after the customer record changes.&lt;/p&gt;

&lt;p&gt;ISO 20022 address migration needs field level lineage. Every outgoing address component should remain connected to its source revision, the relevant payment party, the transformation that produced it, and the rules used to approve its use. This article develops a six-gate engineering runbook for that connection, including a synthetic evidence record and a rehearsal that deliberately breaks it.&lt;/p&gt;

&lt;h2&gt;
  
  
  First, correct the migration calendar
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Status checked on September 24, 2026:&lt;/strong&gt; the previously advertised November 2026 cutover is no longer a reliable universal planning assumption. Swift's current extension announcement says:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Swift will defer all payments changes.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That statement comes from &lt;a href="https://www.swift.com/swift-accepts-community-request-extend-structured-address-migration-iso-20022-payment-messages" rel="noopener noreferrer"&gt;Swift's structured address migration announcement&lt;/a&gt;. Swift is consulting on the timing and approach for structured addresses and promises an update by December at the latest. Its separate Q1 2027 timing for securities, trade and other changes must not be substituted for a payments address deadline.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://www.bankofengland.co.uk/payments/rtgs-renewal-programme/iso-20022" rel="noopener noreferrer"&gt;Bank of England's current RTGS and CHAPS implementation page&lt;/a&gt; also says the November 2026 release has been deferred in its entirety. Its expectation is a twelve-month deferral into November 2027, including removal of unstructured address fields, subject to confirmation and wider validation. That is a qualified expectation for that implementation, not a confirmed global Swift deadline.&lt;/p&gt;

&lt;p&gt;This matters architecturally. A rule package, its approval status and its activation date are different facts. A migration service that hardcodes a date from an old presentation has already lost one kind of provenance. Keep the document reference, retrieval date, applicable scheme and activation decision alongside the rules. Continue improving address capture while the external timetable changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the six gates establish
&lt;/h2&gt;

&lt;p&gt;The useful delivery unit is an address decision with evidence. A database backfill count alone cannot show that the correct party's address reached the correct message field. The following gates provide an internal acceptance contract; they are a proposed engineering design, not a Swift certification procedure.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Gate&lt;/th&gt;
&lt;th&gt;Question to answer&lt;/th&gt;
&lt;th&gt;Evidence required to proceed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1. Source&lt;/td&gt;
&lt;td&gt;Which record did this decision use?&lt;/td&gt;
&lt;td&gt;Versioned source reference and capture context&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2. Party&lt;/td&gt;
&lt;td&gt;Whose address is being represented?&lt;/td&gt;
&lt;td&gt;Stable party identity, role and address purpose&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3. Meaning&lt;/td&gt;
&lt;td&gt;What supports each structured value?&lt;/td&gt;
&lt;td&gt;Field mapping, evidence and unresolved ambiguities&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4. Rules&lt;/td&gt;
&lt;td&gt;Which requirements were applied?&lt;/td&gt;
&lt;td&gt;Scheme profile, version and activation decision&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5. Transport&lt;/td&gt;
&lt;td&gt;What reached the outgoing message?&lt;/td&gt;
&lt;td&gt;Field comparison across every relevant adapter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6. Operations&lt;/td&gt;
&lt;td&gt;Can the decision be investigated and replaced?&lt;/td&gt;
&lt;td&gt;Exception ownership, revision history and rehearsal receipt&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These gates span data ownership, payment orchestration and integration. That boundary is why &lt;a href="https://pharosproduction.com" rel="noopener noreferrer"&gt;financial software development&lt;/a&gt; needs an explicit migration acceptance contract. Pharos Production lists financial systems among its development services; the procurement question is which team will own the evidence across those systems, including the interfaces outside a supplier's immediate codebase.&lt;/p&gt;

&lt;p&gt;A useful implementation produces one linked record at each gate. It also permits a stop. Missing country evidence should create a visible exception with an owner. Quietly filling the field may improve a completeness dashboard while making the next investigation harder.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsdzzx5mgvhw3nu9ixqb6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsdzzx5mgvhw3nu9ixqb6.png" alt="Six address lineage gates connect source revision, party role, field evidence, scheme rules, message comparison and an operational receipt; uncertain decisions branch to review." width="800" height="611"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The six gates preserve the path from a source revision to a message decision. An exception returns through validation after correction.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Gate 1: capture a source revision you can retrieve
&lt;/h2&gt;

&lt;p&gt;Begin with an inventory of where addresses enter the payment flow: customer onboarding, beneficiary maintenance, enterprise resource planning imports, treasury files and service APIs. For each source, identify the owner and whether it contains structured fields, free text or both. Separate what the system stores from what an upstream team believes it stores.&lt;/p&gt;

&lt;p&gt;Use a source reference with a stable record identifier and an immutable revision. A pointer to the latest customer profile is insufficient. If the customer changes an address tomorrow, an investigation of yesterday's payment still needs the version used yesterday. Capture both the source's effective time, when known, and the time your system observed it. A late correction can make those timestamps differ substantially.&lt;/p&gt;

&lt;p&gt;Preserve the original representation inside an appropriately controlled evidence store. The operational event can carry an opaque reference and an integrity digest instead of another copy of the full address. A digest establishes whether bytes changed; it does not establish that the address was accurate, collected lawfully or associated with the right person.&lt;/p&gt;

&lt;p&gt;Define retention, access and retrieval together. A lineage identifier that points to an expired object may be useless during an investigation. Conversely, retaining every raw import indefinitely creates unnecessary personal data exposure. Agree the evidence lifecycle with the responsible data and compliance owners, and make expiry an explicit state that downstream systems can recognize. Test the boundary with a mundane change: revise the source record between extraction and serialization. The release path should detect the revision mismatch and re-evaluate the address. It should never combine the old street with the newly edited country merely because both values were individually available in a cache.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gate 2: bind the address to a payment party
&lt;/h2&gt;

&lt;p&gt;An address without a role is easy to misuse. A corporate profile may contain a registered office, an operating location and a correspondence address. A payment instruction may identify the debtor, creditor, ultimate parties and several financial institutions. Those relationships need explicit modeling before any text is parsed.&lt;/p&gt;

&lt;p&gt;Use a binding that includes a stable party identifier, the role in this payment context and the address purpose selected under the applicable policy. Store the selection reason. Avoid joining records by display name: two subsidiaries can share a trading name, while one legal entity can have several legitimate locations.&lt;/p&gt;

&lt;p&gt;The distinction also affects requirements. &lt;a href="https://www.swift.com/de/node/309567" rel="noopener noreferrer"&gt;Swift's corporate address guidance&lt;/a&gt; describes hybrid addresses with structured town and country, plus up to two address lines without duplicating the structured elements. It explains that a beneficiary bank identified by BIC generally does not also require its postal address. Apply those format and role distinctions using current scheme guidance; the same FAQ still contains older November 2026 wording, which the extension announcement supersedes for scheduling.&lt;/p&gt;

&lt;p&gt;Do not generalize an agent's BIC exception into permission to omit a creditor address. Equally, do not manufacture a bank address merely because a common internal object has mandatory address properties. Model applicability explicitly so that absent, unknown, required and not applicable remain distinguishable states.&lt;/p&gt;

&lt;p&gt;A useful negative fixture contains two parties with the same town but different identities. Swap their source references while leaving the outgoing text unchanged. A validator concerned only with postal values will accept the substitution. The lineage gate should fail because evidence for one party cannot authorize a statement about another.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gate 3: separate parsing from evidence
&lt;/h2&gt;

&lt;p&gt;A parser proposes how text maps to fields. It does not automatically establish the truth of those fields. Consider a legacy address containing a building name, a district and a town separated inconsistently across lines. Several tokenizations may produce well-formed output. Choosing one requires evidence or an explicit uncertainty decision.&lt;/p&gt;

&lt;p&gt;Classify each result by how it was obtained. Directly captured structured data, deterministic normalization, reference-data enrichment and human resolution have different failure modes. Keep those categories in the record. If a model proposes a country from surrounding text, record the proposal and its supporting input; do not silently upgrade the proposal to customer confirmed data.&lt;/p&gt;

&lt;p&gt;A model confidence score can help prioritize review. It is not a substitute for source authority. Nor should a bank account's country, an intermediary bank's BIC, a browser locale or an IP location become evidence for a payment party's address without a justified, applicable business rule. These attributes describe different things.&lt;/p&gt;

&lt;p&gt;The synthetic record below illustrates the shape of a decision. Its identifiers and versions are invented examples. It is an internal provenance envelope, not an ISO 20022 message or a complete production schema. The source's structured town and country were captured directly; no geographic inference is claimed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"decision_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"address-decision-demo-42"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"party_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"party-demo-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"payment_role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"creditor"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"address_purpose"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"registered_office"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"record_ref"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"evidence:beneficiary-demo-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"revision"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"observed_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-09-24T04:00:00Z"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"fields"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"TwnNm"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Brussels"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"source_path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"registered_address.town"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"method"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"direct_capture"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Ctry"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"BE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"source_path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"registered_address.country"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"method"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"direct_capture"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"transform_version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"address-mapper-demo-3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"rule_profile"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"bank-sandbox-hybrid-demo-2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"decision"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"eligible_for_sandbox_serialization"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In a real envelope, also bind the exact source digest, the selected field values, the review identity when needed, and the full rule profile digest. Protect the record against unauthorized modification. An approval should refer to that exact combination. Editing a country after approval must invalidate the decision even if the record identifier stays the same.&lt;/p&gt;

&lt;p&gt;Maintain an explicit list of unresolved fields and conflicting sources. A customer portal may disagree with an imported treasury file. The resolution policy should identify who can decide, which evidence they must inspect and how the correction propagates. Last writer wins is a concurrency strategy, not an address authority policy.&lt;/p&gt;

&lt;p&gt;For teams outsourcing this boundary, Pharos Production's &lt;a href="https://pharosproduction.com/services/payment-solutions-development/" rel="noopener noreferrer"&gt;payment solutions development&lt;/a&gt; scope includes integration layers and ISO 20022 message parsers. Make field provenance and conflict handling explicit acceptance requirements when commissioning that work. A service description establishes relevant scope; it does not prove that a particular migration has passed these gates. Ask for one complete, anonymized decision walkthrough during acceptance. The reviewer should follow an individual field from collection through transformation and serialization, then see a failed case enter the correction process. That demonstration tests the promised boundary more directly than a presentation listing supported message formats.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gate 4: version the scheme rules separately
&lt;/h2&gt;

&lt;p&gt;ISO 20022 supplies a message vocabulary and structure. The actual payment flow also depends on its usage guidelines, market infrastructure rules, bank interface requirements and applicable release. Treating one schema validation result as complete approval collapses these layers and hides which requirement was checked.&lt;/p&gt;

&lt;p&gt;Represent the applicable profile with at least the scheme or channel, message definition, guideline version, supported address representation and activation status. Retain the authoritative reference and the local approval that enabled the profile. Do not label an internal ruleset simply current: that name becomes meaningless when replaying an earlier decision.&lt;/p&gt;

&lt;p&gt;Keep a distinction between a profile that is available in a sandbox, one approved for a bilateral test and one enabled for live traffic. A deferred industry release does not automatically disable a bank's existing support for structured addresses. It also does not authorize a local system to enforce future rejection rules across every route.&lt;/p&gt;

&lt;p&gt;For each profile, specify the checks at their proper layer. Schema validation addresses structural conformance. Guideline validation checks the selected usage constraints. Business validation checks party applicability and the organization's accepted evidence policy. Route validation checks what the receiving connection actually supports.&lt;/p&gt;

&lt;p&gt;Record separate outcomes so that a failed business rule cannot disappear inside a generic XML success flag.&lt;/p&gt;

&lt;p&gt;When rules change, calculate the affected population from stored profile identifiers and field dependencies. Re-evaluate only decisions whose assumptions changed, while keeping their previous results available for investigation. A rule change affecting permissible address lines need not imply that every party identity must be recollected. A source authority change may require review even when the serialized message remains identical.&lt;/p&gt;

&lt;p&gt;Keep historical reproduction separate from present eligibility. Reproduction asks whether the archived input, mapper and rules generate the recorded output. Present eligibility asks whether that address may be used now, under today's source state and active route policy. Store both answers with their own evaluation times. Re-running everything against the latest profile destroys the distinction and can make a previously valid decision look inexplicable.&lt;/p&gt;

&lt;p&gt;For example, a reviewer may discover that a source country was corrected after a payment left the system. Preserve the earlier decision and attach the correction as new evidence. Investigate which subsequent payments inherited the old revision. The corrected value should not silently rewrite the historical message or imply that the earlier reviewer had information that arrived later. The migration calendar is itself a test case for this design. Archive the superseded activation assumption, attach the newer announcement and record who changed the operational profile. Historical decisions remain explainable without presenting an obsolete deadline as today's requirement.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gate 5: prove that adapters preserve meaning
&lt;/h2&gt;

&lt;p&gt;An address may be correctly structured in the customer database and still degrade before it reaches the payment message. A file export can concatenate fields. An integration gateway can truncate a line. A legacy API can omit a country field because its contract never carried one. Testing only the final serializer misses the earlier loss.&lt;/p&gt;

&lt;p&gt;Map the actual route and compare the relevant values at its boundaries. Preserve field identity where possible rather than repeatedly flattening and reparsing text. If an interface requires a combined representation, document how structured values survive the transformation and what information cannot be recovered. Make loss visible before live release.&lt;/p&gt;

&lt;p&gt;Use exact byte comparisons where the representation is meant to remain unchanged. Use defined semantic comparisons where an approved transformation changes encoding, case or ordering. The comparator's normalization policy needs its own version; otherwise a permissive comparison can conceal the same defect it was introduced to detect.&lt;/p&gt;

&lt;p&gt;Build fixtures around the specific transformations your route performs. Include non-ASCII town names, long building identifiers, missing postcodes, multiple address lines and conflicting duplicate fields. Expected outcomes should come from the relevant profile and evidence policy. Do not treat every unfamiliar international address as invalid merely because it fails a domestic postcode pattern.&lt;/p&gt;

&lt;p&gt;Pay particular attention to truncation and transliteration. If an adapter shortens a value, retain the pre-transformation value and a reasoned decision about whether the result remains usable. If the information cannot be preserved under the receiving interface's rules, send the case to remediation. A successful HTTP response is no evidence that a shortened address still identifies the intended location.&lt;/p&gt;

&lt;p&gt;Attach the serialized message reference to the decision before transport, and correlate it with the transport acknowledgement or rejection afterward. Keep these states separate from settlement. A network acceptance says something about delivery or validation at that boundary; it does not establish the real-world accuracy of the customer's address or the final economic outcome of the payment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gate 6: rehearse exceptions and controlled replacement
&lt;/h2&gt;

&lt;p&gt;The exception queue is part of the migration design. Give every stopped decision a machine-readable reason, an accountable owner, the affected party and a reference to the evidence that can resolve it. A free-text note saying bad address is insufficient for either automation or operational planning.&lt;/p&gt;

&lt;p&gt;Separate unresolved evidence from technical delivery failures. An ambiguous town needs a data correction or an authorized resolution. A temporary transport failure needs the payment system's existing recovery procedure. Mixing these cases encourages operators to edit customer data to clear infrastructure errors, or to retry payments whose address decision was never approved.&lt;/p&gt;

&lt;p&gt;A review action should create a new decision revision linked to the previous one. Preserve who changed which field, the supporting evidence and the reason. Require revalidation through every affected gate. If a source revision or ruleset changes while the item waits in the queue, invalidate the stale review rather than applying it to newer data.&lt;/p&gt;

&lt;p&gt;Rehearse the following bounded scenarios in an isolated environment with synthetic records. The expected results are acceptance criteria for the proposed design, not reported measurements from a deployed bank system.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Injected condition&lt;/th&gt;
&lt;th&gt;Expected control response&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Town field exists but has no source mapping&lt;/td&gt;
&lt;td&gt;Stop for missing field evidence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Correct values point to another party's record&lt;/td&gt;
&lt;td&gt;Stop for party binding mismatch&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Source revision changes after review&lt;/td&gt;
&lt;td&gt;Invalidate approval and re-evaluate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Adapter silently drops a country field&lt;/td&gt;
&lt;td&gt;Fail the boundary comparison&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Profile activation is withdrawn before dispatch&lt;/td&gt;
&lt;td&gt;Reassess eligibility under the active route policy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Operator corrects a rejected address&lt;/td&gt;
&lt;td&gt;Create a new decision; retain the earlier evidence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Delivery result is unknown after a timeout&lt;/td&gt;
&lt;td&gt;Investigate transport state before any payment resubmission&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The last row prevents a dangerous category error. Replaying address transformation is a data operation. Releasing another payment instruction may create an additional financial obligation. The rehearsal must prove that a developer can reproduce the former without accidentally triggering the latter. Use a transport stub or disabled dispatch boundary and assert that no live send capability is available.&lt;/p&gt;

&lt;p&gt;Roll back a defective mapper by selecting a previously approved software version for future evaluations, then review the affected decisions. Do not erase new source evidence to make an older mapper appear compatible. Messages already sent require investigation under the payment operation's procedures; changing a database value cannot retract them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measure the gaps that block release
&lt;/h2&gt;

&lt;p&gt;A migration dashboard should distinguish capture coverage from demonstrated lineage. The proportion of records with town and country populated is useful, but it says nothing about whether those values came from defensible sources. Add measures for retrievable source revisions, valid party bindings, approved field decisions, adapter preservation and unresolved exceptions by route.&lt;/p&gt;

&lt;p&gt;Define denominators explicitly. A party for which a postal address is not applicable should not inflate missing-address counts. An unused beneficiary record should not mask problems in the population scheduled for payment this week. Segment readiness by the actual source systems and routes that will be enabled together.&lt;/p&gt;

&lt;p&gt;Track exception age and recurrence, including cases reopened after source or rule changes. Set operational thresholds using the team's demonstrated review capacity and the risk of the affected flow. Avoid selecting a universal target percentage without understanding which records remain outside it. One ambiguous high-impact route can matter more than a large volume of clean, inactive records.&lt;/p&gt;

&lt;p&gt;Also inspect the reason distribution. A rising share of missing source references calls for an ingestion repair; repeated role mismatches call for a party model correction. Sending both populations to the same manual queue can conceal a systematic defect behind apparently productive review activity. Choose remediation at the boundary that caused the problem, then retain a small regression fixture showing why the correction was necessary.&lt;/p&gt;

&lt;p&gt;Use the results to sequence rollout. Start with a bounded population whose source owner, message route and evidence lifecycle are understood. Expand only when the operational team can retrieve the evidence and resolve failures within its agreed process. The extension of an external deadline is an opportunity to fix weak capture boundaries, not evidence that the remaining data is ready.&lt;/p&gt;

&lt;h2&gt;
  
  
  The release packet a reviewer should receive
&lt;/h2&gt;

&lt;p&gt;Deliver a compact manifest tying together the mapper version, source snapshot references, rule profiles, synthetic fixture results, adapter comparisons and exception procedures. Include the enabled routes and excluded populations. State which authoritative documents were checked and when, especially while release timing remains under consultation.&lt;/p&gt;

&lt;p&gt;Ask a reviewer to select one passing record and one rejected record without help from the implementation author. They should be able to locate the relevant source revision, explain every structured field, identify the applicable profile and recover the message comparison. For the rejected record, they should identify exactly what evidence would permit a new decision.&lt;/p&gt;

&lt;p&gt;Then change one dependency and repeat the exercise. A source correction should produce a traceable successor decision. A revised rule should explain which previous decision is no longer eligible. A replay should preserve the historical result while making today's assessment separately visible. If these distinctions cannot be recovered, adding more populated fields will not repair the migration.&lt;/p&gt;

&lt;p&gt;Address migration is ready for controlled release when the team can explain how a particular address reached a particular message under a particular rule decision, and can stop or replace that decision when its evidence changes. That is the engineering capability worth building while the industry settles the next timetable.&lt;/p&gt;

&lt;h2&gt;
  
  
  More insights to read
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://pharosengineeringnotes.wordpress.com/2026/09/12/blockchain-ledger-integration-hub-contracts-and-acceptance-evidence/" rel="noopener noreferrer"&gt;Blockchain Ledger Integration Hub: Contracts and Acceptance Evidence&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pharosengineeringnotes.wordpress.com/2026/09/11/how-to-specify-a-blockchain-ledger-integration-contract/" rel="noopener noreferrer"&gt;How to Specify a Blockchain Ledger Integration Contract&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pharosengineeringnotes.wordpress.com/2026/09/11/how-to-choose-a-blockchain-integrator-for-an-existing-fintech-ledger/" rel="noopener noreferrer"&gt;How to Choose a Blockchain Integrator for an Existing FinTech Ledger&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dmytronasyrov.substack.com/p/how-to-scope-a-fintech-blockchain-discovery-sprint" rel="noopener noreferrer"&gt;How to Scope a FinTech Blockchain Discovery Sprint&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dmytronasyrov.substack.com/p/fintech-blockchain-procurement-hub-scope-shortlist" rel="noopener noreferrer"&gt;FinTech Blockchain Procurement Hub: From Scope to Shortlist&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  About the author
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6a0lb25mqnjcqqx3ve1j.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6a0lb25mqnjcqqx3ve1j.jpg" width="112" height="112" alt="Portrait of Dmytro Nasyrov wearing a dark suit and light blue shirt against a dark background."&gt;&lt;/a&gt;&lt;br&gt;
&lt;small&gt;Dmytro Nasyrov. Photo supplied by the author.&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Written by &lt;a href="https://pharosproduction.com/dmytro-nasyrov/" rel="noopener noreferrer"&gt;Dmytro Nasyrov&lt;/a&gt; PhD, software architect with 24 years of production experience. Dmytro is the founder and CTO of Pharos Production. He works on production software architecture for FinTech, AI, Web3 and blockchain systems.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>fintech</category>
      <category>architecture</category>
      <category>database</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Smart Contract Engineering in 2026: Development, Audits, Upgrades and Release Evidence</title>
      <dc:creator>Dmytro Nasyrov</dc:creator>
      <pubDate>Tue, 22 Sep 2026 21:52:33 +0000</pubDate>
      <link>https://dev.to/pharos_production/smart-contract-engineering-in-2026-development-audits-upgrades-and-release-evidence-27jn</link>
      <guid>https://dev.to/pharos_production/smart-contract-engineering-in-2026-development-audits-upgrades-and-release-evidence-27jn</guid>
      <description>&lt;p&gt;A smart contract can pass its tests, receive an audit and still reach production with the wrong upgrade authority or an unreviewed initialization call. Each document may be accurate while describing a different version of the system. The release decision fails at the joins.&lt;/p&gt;

&lt;p&gt;Smart contract engineering in 2026 needs a traceable connection between intended behavior, reviewed source, executable artifacts, administrative powers and observed deployment. This guide provides that connection: a route through six published technical articles, an evidence acceptance table and a release receipt you can adapt to your repository. The scope is Solidity and EVM systems. The year identifies this reading route, checked on September 23, 2026. It does not imply an industry survey or a universal security standard.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choose the guide that answers your release question
&lt;/h2&gt;

&lt;p&gt;Start with the decision you cannot currently defend. The following articles each contain a different working artifact. Reading them in publication order is unnecessary.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Your question&lt;/th&gt;
&lt;th&gt;Guide and its specific job&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;What should a repository handoff contain?&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://dev.to/pharos_production/how-to-choose-a-smart-contract-development-company-8-repository-checks-9m"&gt;Smart Contract Release Audit: 8 Repository Artifacts&lt;/a&gt; identifies the records to request before accepting delivery.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;How do automated checks block a bad candidate?&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://dev.to/pharos_production/smart-contract-cicd-foundry-slither-and-audit-gates-4ael"&gt;Smart Contract CI/CD: Foundry, Slither, and Audit Gates&lt;/a&gt; connects build and analysis jobs to release gates.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Who can change the deployed system?&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://dev.to/pharos_production/smart-contract-upgrade-authority-timelocks-multisigs-and-emergency-controls-p6o"&gt;Smart Contract Upgrade Authority: Timelocks, Multisigs, and Emergency Controls&lt;/a&gt; maps normal and emergency administrative paths.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What happens if the new implementation fails?&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://dev.to/pharos_production/how-to-rehearse-smart-contract-rollback-before-calling-a-system-upgradeable-3532"&gt;How to Rehearse Smart-Contract Rollback Before Calling a System Upgradeable&lt;/a&gt; defines recovery exercises and the limits of code reversal.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;How should I evaluate a development partner?&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://dev.to/pharos_production/10-smart-contract-development-companies-compared-by-repository-and-release-evidence-in-2026-m59"&gt;10 Smart Contract Development Companies Compared by Repository and Release Evidence in 2026&lt;/a&gt; separates public evidence from project-specific delivery questions.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;How do I test behavior across an upgrade?&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://dev.to/pharos_production/property-based-testing-for-upgradeable-smart-contracts-a-stateful-invariant-harness-45j3"&gt;Property-Based Testing for Upgradeable Smart Contracts: A Stateful Invariant Harness&lt;/a&gt; supplies a bounded teaching harness with handlers, ghost state and a deliberately broken upgrade.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Use the vendor comparison during selection, then move to repository acceptance when a team delivers your code. Public engineering activity can support a shortlist. It cannot establish that the people assigned to your project reviewed its economics, reproduced its build or rehearsed its deployment.&lt;/p&gt;

&lt;p&gt;For a delivery organization offering &lt;a href="https://pharosproduction.com" rel="noopener noreferrer"&gt;blockchain software development&lt;/a&gt;, such as Pharos Production, this distinction matters at the handoff: service scope identifies the work offered, while the candidate's artifacts establish what was actually completed. Apply the same acceptance questions to an internal team and an external supplier.&lt;/p&gt;

&lt;h2&gt;
  
  
  Give every piece of evidence a subject
&lt;/h2&gt;

&lt;p&gt;A filename such as &lt;code&gt;final-audit.pdf&lt;/code&gt; is insufficient identification. Define the release subject using the repository and full commit, build configuration, dependency revisions, target chain and intended contract addresses. An upgrade also needs the currently deployed implementation and the candidate implementation. Keep intended values separate from observations read from the network.&lt;/p&gt;

&lt;p&gt;The identity must extend beyond Solidity files. A deployment script can pass the wrong administrator without changing the implementation. Compiler settings can alter bytecode. An oracle address can change system behavior while every repository test remains unchanged. Include these inputs in the candidate and explain which checks cover them.&lt;/p&gt;

&lt;p&gt;The following acceptance table is a proposed engineering policy. Adapt its owners and stop conditions to the actual system. It is not a report of completed checks.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Evidence&lt;/th&gt;
&lt;th&gt;Acceptance question&lt;/th&gt;
&lt;th&gt;What invalidates it?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Build record&lt;/td&gt;
&lt;td&gt;Can the recorded inputs reproduce the intended artifact?&lt;/td&gt;
&lt;td&gt;Source, compiler, dependencies or settings change.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Behavior results&lt;/td&gt;
&lt;td&gt;Were the relevant properties exercised under declared assumptions?&lt;/td&gt;
&lt;td&gt;Properties, handlers, configuration or relevant code change.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Audit disposition&lt;/td&gt;
&lt;td&gt;Is the candidate inside the reviewed boundary?&lt;/td&gt;
&lt;td&gt;An unclassified change crosses that boundary.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Authority snapshot&lt;/td&gt;
&lt;td&gt;Do the actual accounts and roles match the approved model?&lt;/td&gt;
&lt;td&gt;Owners, thresholds, modules, roles or delays change.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Recovery rehearsal&lt;/td&gt;
&lt;td&gt;Does the procedure preserve the required state and access?&lt;/td&gt;
&lt;td&gt;Migration logic, starting state or recovery powers change.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deployment observation&lt;/td&gt;
&lt;td&gt;Did the intended code and configuration reach this chain?&lt;/td&gt;
&lt;td&gt;Wrong target, bytecode mismatch or insufficient confirmation.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Operations handoff&lt;/td&gt;
&lt;td&gt;Can an assigned operator detect and handle the specified failure?&lt;/td&gt;
&lt;td&gt;The monitored system or response ownership changes.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgxf5qk87k5pkw54fn1oe.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgxf5qk87k5pkw54fn1oe.png" alt="Release evidence diagram: candidate build, behavior tests, audit and recovery records meet observed chain code and authority at one identity check; a mismatch blocks release, and changes invalidate affected evidence." width="800" height="480"&gt;&lt;/a&gt;&lt;br&gt;
&lt;small&gt;Release evidence joins candidate records to a chain observation. A changed input reopens its affected checks.&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;Do not replace this table with a weighted readiness score. A strong test suite cannot compensate for an unknown upgrade administrator. A documented exception needs its own scope, reason and authorized owner. An arithmetic average cannot accept it on their behalf.&lt;/p&gt;
&lt;h2&gt;
  
  
  Develop the behavior contract before the implementation
&lt;/h2&gt;

&lt;p&gt;Write properties in business terms first. Which balances represent customer claims? When is a request irrevocably accepted? Who may mint, withdraw, cancel or change a limit? Which external data must be fresh? These answers define observable obligations that an implementation and its tests can share.&lt;/p&gt;

&lt;p&gt;Consider a withdrawal queue. A useful specification distinguishes a requested withdrawal from a claimable one and a paid one. It says whether pausing new requests must preserve existing claims. It defines how rounding is allocated and whether a later parameter change applies to pending requests. Without these decisions, two correct-looking functions can disagree about the same liability.&lt;/p&gt;

&lt;p&gt;Translate those obligations into contract boundaries, explicit permissions and testable state transitions. Keep an inventory of external assumptions: token transfer behavior, oracle updates, keeper availability and chain-specific execution conditions. An assumption belongs beside the property that depends on it, so a reviewer can see what a passing assertion leaves unresolved.&lt;/p&gt;

&lt;p&gt;Pin the compiler and dependencies, and retain the settings used for the release. Check the selected compiler against Solidity's &lt;a href="https://docs.soliditylang.org/en/latest/bugs.html" rel="noopener noreferrer"&gt;known-bug list&lt;/a&gt;, including the conditions under which a listed bug applies. A compiler version number alone cannot answer whether your settings and code encounter the issue. Record the review outcome with the build evidence.&lt;/p&gt;

&lt;p&gt;Upgradeable contracts add initialization obligations. OpenZeppelin states:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Do not leave an implementation contract uninitialized.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;OpenZeppelin, &lt;a href="https://docs.openzeppelin.com/upgrades-plugins/writing-upgradeable#initializing-the-implementation-contract" rel="noopener noreferrer"&gt;Writing Upgradeable Contracts&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The same guide describes locking the implementation with &lt;code&gt;_disableInitializers&lt;/code&gt; and treating proxy initialization separately. Turn that distinction into explicit checks for each deployed address. The intended owner, initialized version and repeat-call behavior belong in the deployment review, not in an operator's memory.&lt;/p&gt;
&lt;h2&gt;
  
  
  Read test results as bounded evidence
&lt;/h2&gt;

&lt;p&gt;A unit test checks a particular behavior. Fuzzing explores inputs within its configured domain. Stateful invariant testing explores sequences and asks whether a property survives them. These methods answer related questions, but their results should retain enough context to distinguish what actually ran.&lt;/p&gt;

&lt;p&gt;For an upgrade, include behavior before the transition, the transition itself and behavior afterward. A fresh deployment of version two can pass every test while a populated version-one proxy fails during migration. Likewise, a successful upgrade transaction says little about whether old withdrawal claims remain redeemable.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://getfoundry.sh/forge/invariant-testing" rel="noopener noreferrer"&gt;Foundry invariant-testing documentation&lt;/a&gt; explains sequence campaigns, handlers and configuration. Retain the tool version, selected profile, targeted operations, campaign bounds and failure reproduction data. Inspect successful calls and reverts as well as the final result. A handler that filters every difficult input may make an invariant easy to satisfy without exercising its intended risk.&lt;/p&gt;

&lt;p&gt;Use a negative control when the property is important enough to gate release. Introduce a deliberate defect in an isolated test fixture and verify that the relevant assertion detects it. For example, a migration that resets an aggregate should fail the conservation property that compares aggregate and per-account obligations. Record that result as evidence about the test's sensitivity, not as proof that all migration defects are covered.&lt;/p&gt;

&lt;p&gt;The linked stateful harness is a teaching example, not a production asset protocol. Adapting its structure requires replacing the model, actions and assumptions with your system's behavior. Copying its campaign settings or green result does not transfer its evidence to your release.&lt;/p&gt;

&lt;p&gt;Keep static analysis alongside these tests. Preserve findings, reviewed suppressions and the analysis configuration. When a result disappears, determine whether code was fixed, coverage changed or a detector was disabled. The release owner needs the disposition, not just an empty findings panel.&lt;/p&gt;

&lt;p&gt;Assign a question to every retained result. A conservation test might compare claims against assets under a defined token model. An authorization test should show that a forbidden actor cannot reach the protected state change. A liveness exercise should demonstrate progress under its stated assumptions about keepers and available liquidity. These are different obligations, so combining their totals into one test count hides information the reviewer needs.&lt;/p&gt;

&lt;p&gt;When a test uses a fork, distinguish captured state from simulated future behavior. Record the fork block, dependency addresses and any substituted responses. A simulated oracle update or signer impersonation can be useful for isolating contract behavior, but it removes part of the operational problem. List that substitution so the release decision can require separate evidence for the real dependency or signing procedure.&lt;/p&gt;
&lt;h2&gt;
  
  
  Bind the audit to the release candidate
&lt;/h2&gt;

&lt;p&gt;A smart contract audit has a scope: repository, revision, files, assumptions, exclusions and review method. Ask for those identifiers before treating its conclusions as relevant to a pending deployment. A report about the project can be authentic and still omit the migration you intend to execute tomorrow.&lt;/p&gt;

&lt;p&gt;Create a finding-to-fix record. Each relevant finding should point to the remediation change, a regression check where appropriate, the follow-up review and any residual limitation. A team marking its own ticket closed is different evidence from the reviewer confirming the fix. Preserve that difference in the receipt.&lt;/p&gt;

&lt;p&gt;Compare the audited revision with the candidate. Classify changes by affected behavior instead of accepting them because the diff is small. One address replacement can redirect authority. A single arithmetic adjustment can change claims. Documentation-only changes may need no new security review, but the classification itself should be visible and attributable.&lt;/p&gt;

&lt;p&gt;This is also where delivery scope must become concrete. When development and audit handoffs lose the relationship between fixes and the deployable candidate, Pharos Production's &lt;a href="https://pharosproduction.com/services/smart-contracts-development/" rel="noopener noreferrer"&gt;smart contract development and audit preparation&lt;/a&gt; provides a relevant service scope spanning implementation, testing and security-review preparation. The acceptance record should still require evidence from the particular engagement. The service description does not establish an independent audit result.&lt;/p&gt;

&lt;p&gt;Record accepted risks with the condition that made acceptance reasonable. If a limitation was accepted because withdrawals were capped, raising the cap should reopen the decision. Otherwise, a valid historical exception becomes an undocumented expansion of risk.&lt;/p&gt;
&lt;h2&gt;
  
  
  Review the complete upgrade authority path
&lt;/h2&gt;

&lt;p&gt;Start at the function that can replace the implementation and work outward to every account or contract that can authorize it. Include powers that can change those permissions. A diagram showing a multisig at the top is incomplete if another role can replace the controlling administrator.&lt;/p&gt;

&lt;p&gt;OpenZeppelin's &lt;a href="https://docs.openzeppelin.com/contracts/5.x/access-control" rel="noopener noreferrer"&gt;access-control documentation&lt;/a&gt; describes ownership, role-based controls and timelocks. A threshold wallet distributes approval among signers. A timelock introduces delay for scheduled operations. Neither mechanism, by itself, establishes that the approved payload preserves user obligations. The control model must describe proposal, execution, cancellation and administration together.&lt;/p&gt;

&lt;p&gt;Record the current threshold, owners, relevant modules and the roles on each controlled contract. Separate accounts allowed to propose a change from those able to execute or cancel it. Include changes to the delay itself. Read these values from the intended network at a recorded block. Deployment notes may describe an earlier configuration.&lt;/p&gt;

&lt;p&gt;Then examine emergency powers. A pause role may block deposits while withdrawals remain available, or it may freeze both. An emergency upgrade path may bypass the ordinary delay. State the consequence explicitly and test unauthorized calls through each reachable entry point. A bypass justified for an incident remains a live administrative capability afterward.&lt;/p&gt;

&lt;p&gt;Rehearse absence as well as compromise. Determine whether the required signers can actually act, whether a cancellation can arrive in time and who owns the response when one signer is unavailable. Do not choose a universal delay from a blog post. Its usefulness depends on detection, decision time, transaction inclusion and the actions users can take during the window.&lt;/p&gt;
&lt;h2&gt;
  
  
  Separate storage compatibility from recovery
&lt;/h2&gt;

&lt;p&gt;A compatible storage layout is necessary for many proxy upgrades, but it cannot establish that the old implementation understands state written by the new one. A field can retain its type and slot while changing units, meaning or permitted values. Recovery must account for those semantic changes.&lt;/p&gt;

&lt;p&gt;For example, suppose version one records a claim in whole units and version two migrates it into scaled units. Reverting the implementation pointer may succeed technically while causing version one to interpret the scaled value incorrectly. A rollback rehearsal must execute meaningful reads and transactions after reversal, rather than stopping at a successful administrative call.&lt;/p&gt;

&lt;p&gt;OpenZeppelin's &lt;a href="https://docs.openzeppelin.com/upgrades-plugins/foundry/api/upgrades" rel="noopener noreferrer"&gt;Foundry upgrade-validation API&lt;/a&gt; compares a new implementation against a declared reference. Preserve the reference identity and validation settings. A comparison against the wrong predecessor provides no useful answer about the proxy you operate.&lt;/p&gt;

&lt;p&gt;Define the recovery strategy before approving migration: return to the old code where its state assumptions still hold, deploy a reviewed corrective implementation, or follow a bounded migration procedure. Document conditions that make each option unavailable. A pause can buy time while retaining state, but it does not reverse transfers or restore a previous storage snapshot.&lt;/p&gt;

&lt;p&gt;Use representative populated state and retain the rehearsal's starting block or fixture. Include pending claims, active permissions and operations already in flight. Verify the obligations after recovery and the operator's ability to complete the next required action. If the procedure leaves the system permanently paused, explain how users regain access before calling the rehearsal successful.&lt;/p&gt;
&lt;h2&gt;
  
  
  Join the approved artifact to an observed deployment
&lt;/h2&gt;

&lt;p&gt;Before signing, compare the full intended operation with the reviewed one: chain, target, value, calldata and administrative path. Decode parameters for the reviewer while retaining their exact encoded form. A familiar function name is insufficient when its arguments choose a different implementation or initialization recipient.&lt;/p&gt;

&lt;p&gt;After execution, retain the transaction receipt and apply the confirmation or finality policy appropriate to the chain. Record the block number and block hash used for verification. A transaction accepted by a node is an earlier observation than a deployment accepted under that policy. Give these states separate names.&lt;/p&gt;

&lt;p&gt;Compare deployed runtime code with the expected deployment artifact, accounting explicitly for linked libraries, immutable values and compiler metadata where relevant. Do not silently strip differences to obtain a match. Preserve the comparison method so another engineer can reproduce the result from the same build inputs.&lt;/p&gt;

&lt;p&gt;For relevant proxy types, &lt;a href="https://eips.ethereum.org/EIPS/eip-1967" rel="noopener noreferrer"&gt;ERC-1967&lt;/a&gt; specifies implementation, beacon and administrator storage slots and associated events. Inspect the mechanism the system actually uses. A beacon proxy adds a dependency through the beacon. A nonstandard proxy requires its own documented inspection path. An explorer badge does not replace this identification work.&lt;/p&gt;

&lt;p&gt;Finally, check the initialized application state and authority configuration. Confirm the intended implementation, roles, limits and external dependencies. Where a check would create an irreversible effect, use a suitable simulation or a separately authorized bounded operation. Verification should not invent an unreviewed transaction merely to demonstrate that the deployment is alive.&lt;/p&gt;
&lt;h2&gt;
  
  
  Keep one receipt with explicit evidence states
&lt;/h2&gt;

&lt;p&gt;The following YAML is an unfilled template, not an executable validator or a completed release. Every check begins at &lt;code&gt;NOT_RUN&lt;/code&gt;. Null identifiers must be replaced with observed values. Copying the file cannot authorize a deployment.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;release-evidence-example/v1&lt;/span&gt;
&lt;span class="na"&gt;candidate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;repository&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
  &lt;span class="na"&gt;source_commit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
  &lt;span class="na"&gt;build_input_sha256&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
  &lt;span class="na"&gt;compiler_and_settings_ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
  &lt;span class="na"&gt;dependency_lock_ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
&lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;chain_id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
  &lt;span class="na"&gt;proxy_address&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
  &lt;span class="na"&gt;current_implementation&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
  &lt;span class="na"&gt;candidate_implementation&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
  &lt;span class="na"&gt;reviewed_operation_sha256&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
&lt;span class="na"&gt;evidence&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;NOT_RUN&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;artifact_ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;null&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;behavior&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;NOT_RUN&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;artifact_ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;null&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;audit_scope_and_fixes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;NOT_RUN&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;artifact_ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;null&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;authority&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;NOT_RUN&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;artifact_ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;null&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;recovery&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;NOT_RUN&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;artifact_ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;null&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;deployment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;NOT_RUN&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;artifact_ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;null&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;operations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;NOT_RUN&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;artifact_ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;null&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
&lt;span class="na"&gt;observation&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;transaction_hash&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
  &lt;span class="na"&gt;block_number&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
  &lt;span class="na"&gt;block_hash&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
  &lt;span class="na"&gt;finality_policy_ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
&lt;span class="na"&gt;decision&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;NOT_READY&lt;/span&gt;
  &lt;span class="na"&gt;reviewer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
  &lt;span class="na"&gt;accepted_exceptions_ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each referenced artifact should contain its subject identifiers, producer, observation time, result and limitations. Define a small state vocabulary: not run, passed, failed, invalidated and not applicable with a reason. Keep release authorization distinct from both a technical check and a transaction submission.&lt;/p&gt;

&lt;p&gt;Retain a pre-submission decision and a separate post-deployment acceptance record. Deployment observations cannot exist before deployment, so an all-fields-passed rule would be circular. The first decision authorizes an exact operation after the required preparatory evidence is accepted. The second confirms what happened and whether operational ownership can transfer.&lt;/p&gt;

&lt;p&gt;Hashing records helps detect changed bytes when a trusted comparison value exists. It does not prove that their claims are true or that their signer was authorized. Preserve review identity and artifact provenance alongside integrity values. Keep secrets and private signing material outside the evidence packet.&lt;/p&gt;

&lt;p&gt;Make artifact references durable enough for an incident investigation. A temporary CI download link may expire before the next upgrade. Retain the relevant build inputs, reports and replay instructions in an access-controlled archive with the release identifier. Test whether the receiving engineer can retrieve the packet using their own access, rather than relying on the original author's workstation or browser session.&lt;/p&gt;

&lt;h2&gt;
  
  
  Treat configuration changes as releases with their own scope
&lt;/h2&gt;

&lt;p&gt;A new implementation is only one way to alter a smart contract system. An oracle replacement, a fee adjustment or a role transfer can change user outcomes without changing runtime bytecode. Give such changes an exact subject and acceptance path instead of exempting them because no compiler ran.&lt;/p&gt;

&lt;p&gt;For a parameter update, record its old and proposed values, permitted bounds and affected obligations. Determine how it applies to already accepted work. A fee update may be valid for new requests while breaking the economics of a pending queue if applied retroactively. The required evidence should follow that behavior change. Rebuilding unchanged code is not a substitute for reviewing the parameter's effect.&lt;/p&gt;

&lt;p&gt;For a dependency replacement, establish the interface and failure behavior your application relies on. The same function signature does not guarantee equivalent units, freshness, permissions or revert behavior. Preserve the chosen address and the assumptions checked against it, then update monitoring to observe the replacement. Treat a change to those assumptions as a reason to revisit the dependent properties.&lt;/p&gt;

&lt;p&gt;For an authority transfer, verify that the recipient can exercise its intended role and that the previous principal has only the powers the plan allows. Some transitions require an acceptance step. The receipt should distinguish transfer requested from transfer completed and should identify the observation that resolves the distinction.&lt;/p&gt;

&lt;p&gt;Scope the work proportionately. A configuration-only release can reuse unchanged build evidence while requiring new parameter, authority or operational checks. A code upgrade may require the whole path. The useful question is which dependency changed and which conclusion relied on it. Write that relationship down before selecting the checks, then preserve the rejected and superseded records so the history explains why a new decision was necessary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Walk a candidate through rejection and acceptance
&lt;/h2&gt;

&lt;p&gt;Imagine a hypothetical upgrade with passing behavior tests and an audit covering its source commit. During deployment preparation, the operator changes the migration recipient. The source tests remain green, but the reviewed operation no longer matches. The receipt should invalidate the operation review and any rehearsal dependent on that recipient.&lt;/p&gt;

&lt;p&gt;The fix is to review the changed parameter, rerun affected checks and issue a new decision for the exact payload. Unaffected evidence can remain valid if its dependencies are unchanged and that conclusion is recorded. This avoids both careless reuse and an expensive ritual of rerunning everything without asking what changed.&lt;/p&gt;

&lt;p&gt;Now suppose deployment succeeds but the observed administrator differs from the approved configuration. Record the actual state and keep operational acceptance blocked. Diagnose the mismatch and use the authorized incident or correction procedure. Never edit the expected value after execution merely to make the comparison pass.&lt;/p&gt;

&lt;p&gt;Operations should receive alerts tied to the same model: implementation or role changes, violated accounting properties, stale dependencies and failures of required off-chain services. Every alert needs a destination, an accountable responder and an available action. A notification without authority to respond leaves the handoff unfinished.&lt;/p&gt;

&lt;p&gt;The release is ready for operational acceptance when the approved candidate, observed chain state and assigned response process agree within their documented limits. Keep the receipt with the release, reopen affected decisions when inputs change, and use the linked specialist guide for the next missing piece of evidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  More insights to read
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.bulbapp.io/p/e2580ef7-324b-442d-b818-92b8c8442b3d/five-smart-contract-upgrade-patterns-compared" rel="noopener noreferrer"&gt;Five Smart Contract Upgrade Patterns Compared&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dmytronasyrov.medium.com/upgradeable-solidity-smart-contracts-part-1-versioning-7e6e97cafc28" rel="noopener noreferrer"&gt;Upgradeable Solidity Smart Contracts. Part 1 — Versioning&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://medium.com/pharos-production/web3-smart-contracts-oracles-part-1-3905b127c01d" rel="noopener noreferrer"&gt;Web3. Smart Contracts. Oracles. Part 1&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://medium.com/pharos-production/smart-contracts-their-potential-and-real-limitations-part-2-40942c055d79" rel="noopener noreferrer"&gt;Smart Contracts. Their Potential and Real Limitations. Part 2&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://medium.com/pharos-production/smart-contracts-their-potential-and-real-limitations-part-1-222fe44ee14c" rel="noopener noreferrer"&gt;Smart Contracts. Their Potential and Real Limitations. Part 1&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  About the author
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3y3gv5cvg1m0yfn97ofb.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3y3gv5cvg1m0yfn97ofb.jpg" width="112" height="112" alt="Portrait of Dmytro Nasyrov wearing a dark suit and light blue shirt against a dark background."&gt;&lt;/a&gt;&lt;br&gt;
&lt;small&gt;Dmytro Nasyrov. Photo supplied by the author.&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Written by &lt;a href="https://pharosproduction.com/dmytro-nasyrov/" rel="noopener noreferrer"&gt;Dmytro Nasyrov&lt;/a&gt; PhD, software architect with 24 years of production experience. Dmytro is the founder and CTO of Pharos Production. He works on production software architecture for FinTech, AI, Web3 and blockchain systems.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>solidity</category>
      <category>web3</category>
      <category>security</category>
      <category>devops</category>
    </item>
    <item>
      <title>GitHub Release to Zenodo DOI</title>
      <dc:creator>Dmytro Nasyrov</dc:creator>
      <pubDate>Thu, 17 Sep 2026 07:21:09 +0000</pubDate>
      <link>https://dev.to/dmytronasyrov/github-release-to-zenodo-doi-265f</link>
      <guid>https://dev.to/dmytronasyrov/github-release-to-zenodo-doi-265f</guid>
      <description>&lt;p&gt;&lt;small&gt;Cover illustration: evolving source becomes one preserved release.&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;A GitHub release can exist before its Zenodo archive is ready. A DOI can resolve while the metadata still misrepresents the files. A discovery service can list the record without having checked whether the software works. Treating those events as one success state makes a release difficult to reproduce and surprisingly easy to describe incorrectly.&lt;/p&gt;

&lt;p&gt;This guide follows a real software release through those boundaries. The example is version 1.1.0 of an LLM-as-a-judge cost calculator, published on August 16, 2026. Its GitHub release, archived files, DOI resolution, DataCite metadata, OAI-PMH response and OpenAIRE entry were checked again on September 17. The useful deliverable is a small verification receipt that tells the next developer exactly what was preserved and what each check established.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with the object someone will cite
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://github.com/PharosProduction/llm-as-a-judge-cost-calculator/releases/tag/v1.1.0" rel="noopener noreferrer"&gt;calculator release&lt;/a&gt; is a browser application with calculation code, tests, methodology and dated pricing registries. That combination gives another developer something to inspect and reuse. A repository containing only an announcement would offer much less reason to preserve a particular version.&lt;/p&gt;

&lt;p&gt;The release work behind &lt;a href="https://pharosproduction.com" rel="noopener noreferrer"&gt;Pharos Production software development&lt;/a&gt; supplies the concrete example here: a cost model packaged with explicit assumptions and separate terms for code and data. The engineering problem is retaining the inputs behind an estimate when the application and vendor prices later change. Archiving a named release preserves those inputs; it does not keep them current.&lt;/p&gt;

&lt;p&gt;The identities checked for this example are:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Object&lt;/th&gt;
&lt;th&gt;Observed identifier&lt;/th&gt;
&lt;th&gt;What it identifies&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;GitHub release&lt;/td&gt;
&lt;td&gt;&lt;code&gt;v1.1.0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The named release and its notes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Git commit&lt;/td&gt;
&lt;td&gt;&lt;code&gt;67b6d1d9d741214a986ea4d7ce7111a99605bbf3&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The source revision resolved from that tag&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Zenodo record&lt;/td&gt;
&lt;td&gt;&lt;code&gt;21963489&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The published archive and its metadata&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Version DOI&lt;/td&gt;
&lt;td&gt;&lt;code&gt;10.5281/zenodo.21963489&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;This particular archived version&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Concept DOI&lt;/td&gt;
&lt;td&gt;&lt;code&gt;10.5281/zenodo.21963488&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The work across its versions&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;On the verification date, the &lt;a href="https://zenodo.org/records/21963489" rel="noopener noreferrer"&gt;public Zenodo record&lt;/a&gt; contained one ZIP file of 133,258 bytes. Its advertised MD5 checksum matched the downloaded archive. The four citation and license files inside that ZIP also matched their counterparts at the pinned Git commit byte for byte. This is a bounded file comparison, not a claim that every executable path was retested for this article.&lt;/p&gt;

&lt;p&gt;Record the commit separately from the tag. In this case GitHub's release response reported &lt;code&gt;immutable: false&lt;/code&gt;; the existence of a release URL alone therefore did not establish GitHub release immutability. The version DOI and retained archive checksum provide different evidence from a mutable repository page.&lt;/p&gt;

&lt;h2&gt;
  
  
  Complete preflight before enabling automatic deposits
&lt;/h2&gt;

&lt;p&gt;Choose the smallest package that supports independent use. Include source, installation or execution instructions, dependency information, representative tests and the data needed for the documented example. If a benchmark depends on a private dataset that cannot be shared, say what remains unavailable and how that limits reproduction.&lt;/p&gt;

&lt;p&gt;Check the actual archive contents, particularly when a project uses generated binaries, submodules, large files or separately hosted assets. A successful source-code archive does not by itself show that every resource mentioned in the README was deposited. The calculator example has no uploaded GitHub release assets; its archived ZIP is the object inspected here.&lt;/p&gt;

&lt;p&gt;For reproduction, preserve the invocation as well as the program. A calculator result needs its workload values; a benchmark needs its configuration and measurement procedure. A DOI pointing to the right code does not tell a reader which options produced a reported number. Put those inputs in a small example or run manifest, with expected outputs and tolerances where relevant. If an external service is required, document that dependency and the resulting limits. A source archive can preserve an algorithm while leaving the environment needed to execute it only partly reproducible.&lt;/p&gt;

&lt;p&gt;Confirm that publishing every included file is intended. Remove secrets, private configuration, customer records and accidental build outputs before tagging. Decide authorship from contributions rather than copying whoever happens to operate the release account. Verify the creator's name, affiliation and ORCID against the intended citation, and retain that decision with the release review.&lt;/p&gt;

&lt;p&gt;GitHub's official guidance states:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Make sure to include a license in your repository so readers know how they can reuse your work.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;GitHub, &lt;a href="https://docs.github.com/en/repositories/archiving-a-github-repository/referencing-and-citing-content" rel="noopener noreferrer"&gt;Referencing and citing content&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;That instruction belongs in preflight because an archive cannot repair missing permission by assigning an identifier. A useful review asks which files are covered by each license, where the applicable terms are stored and whether third-party materials have separate conditions. A repository-level badge is too coarse for a package containing differently licensed components.&lt;/p&gt;

&lt;p&gt;For the integration itself, GitHub documents public-repository access and notes that an organization owner may need to approve the Zenodo application. Review that access deliberately. Then use Zenodo's GitHub settings to synchronize the repository list and enable the intended repository. Do not interpret a successful account connection as evidence that the correct repository is enabled. The &lt;a href="https://help.zenodo.org/docs/github/enable-repository/" rel="noopener noreferrer"&gt;enable-repository guide&lt;/a&gt; describes these as separate steps.&lt;/p&gt;

&lt;h2&gt;
  
  
  Give citation metadata one clear owner
&lt;/h2&gt;

&lt;p&gt;Keep &lt;code&gt;CITATION.cff&lt;/code&gt; in the repository root when you want GitHub to offer a citation through its interface. It describes the software for people and citation tools. The example's archived file includes the title, version, author, ORCID, repository URL and a description of its two license scopes.&lt;/p&gt;

&lt;p&gt;Zenodo also accepts &lt;code&gt;.zenodo.json&lt;/code&gt;. Its &lt;a href="https://help.zenodo.org/docs/github/describe-software/zenodo-json/" rel="noopener noreferrer"&gt;current metadata documentation&lt;/a&gt; makes the precedence explicit: when both files exist, the GitHub archiving integration uses &lt;code&gt;.zenodo.json&lt;/code&gt; and ignores &lt;code&gt;CITATION.cff&lt;/code&gt;. It does not merge them. A corrected author in CFF cannot compensate for a stale creator in the JSON file.&lt;/p&gt;

&lt;p&gt;Choose an owner for each repeated field. For example, a release review can require title, version and creators to agree across the two files, while reserving Zenodo-specific related identifiers for JSON. This is a proposed consistency check, not a claim that either service enforces your repository's policy automatically.&lt;/p&gt;

&lt;p&gt;A short excerpt from the actual archived JSON shows why reviewing fields individually matters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1.1.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"upload_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"software"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"access_right"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"open"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"license"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"other-open"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"language"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"eng"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is an excerpt, not a complete upload template. In particular, &lt;code&gt;other-open&lt;/code&gt; describes the historical release input; it is not a recommendation to replace specific license declarations with a generic value. The full file contains creators, description, keywords, related identifiers and notes defining the code/data boundary.&lt;/p&gt;

&lt;p&gt;Validate the exact files contained in the tag. Checking only the default branch after publication can inspect a later correction that never entered the archive. JSON syntax validation catches malformed JSON, but it does not prove that an identifier, relationship or license choice is appropriate. Review the supported fields and then inspect the resulting record.&lt;/p&gt;

&lt;p&gt;The same distinction applies to CFF validation. Schema validation and correct citation intent are separate checks. A well-formed file can still point to the wrong software version or credit an unrelated paper. Keep the intended citation in the review so a mechanically valid result can be compared with it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choose a version DOI for reproducible claims
&lt;/h2&gt;

&lt;p&gt;Zenodo's &lt;a href="https://support.zenodo.org/help/en-gb/1-upload-deposit/97-what-is-doi-versioning" rel="noopener noreferrer"&gt;DOI versioning explanation&lt;/a&gt; distinguishes a specific version from the concept representing all versions. The first publication creates both identifiers; later versions receive their own version identifiers. The relationships belong in metadata rather than in an invented suffix appended to the DOI.&lt;/p&gt;

&lt;p&gt;Use the version DOI when a result depends on the exact software or data you used. A cost estimate based on the calculator's August pricing registry should identify that release, its workload inputs and the registry's verification date. A general project description may instead refer to the concept DOI because its subject is the evolving work.&lt;/p&gt;

&lt;p&gt;Both identifiers resolved to record 21963489 during this check. That shared destination does not make their meanings interchangeable. It only describes their resolution at the time of observation. A receipt should store both identifiers with explicit field names, rather than a single ambiguous &lt;code&gt;doi&lt;/code&gt; field copied from whichever badge was easiest to find.&lt;/p&gt;

&lt;p&gt;Likewise, keep release version, publication date and data-verification date separate. Version 1.1.0 was published on August 16; its pricing sources were described as verified on August 13. Neither date makes those prices current in September. This article verifies the preserved object and its metadata, not today's commercial pricing.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://pharosproduction.com/insights/engineering/llm-observability-cost/" rel="noopener noreferrer"&gt;LLM observability cost methodology&lt;/a&gt; explains the problem the calculator addresses: different billing entities require explicit workload assumptions. Keeping that methodological context beside the versioned artifact helps a reader understand an estimate. The company article remains contextual documentation, not independent validation of the software.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preserve mixed licenses across representations
&lt;/h2&gt;

&lt;p&gt;The example assigns MIT to source code and the static application. Its &lt;code&gt;DATA-LICENSE.md&lt;/code&gt; assigns CC BY 4.0 to normalized pricing records in &lt;code&gt;data/&lt;/code&gt; and &lt;code&gt;docs/data/&lt;/code&gt;. Those paths matter: two license names without file scope could be misread as offering a choice of terms for every file.&lt;/p&gt;

&lt;p&gt;Zenodo's &lt;a href="https://help.zenodo.org/docs/deposit/describe-records/licenses/" rel="noopener noreferrer"&gt;license guidance&lt;/a&gt; permits declaring multiple licenses for mixed uploads. Keep the file-level explanation inside the archive as well as in record metadata. A downloaded ZIP should remain understandable without requiring the reader to reconstruct an earlier state of its landing page.&lt;/p&gt;

&lt;p&gt;The September inspection exposed three different representations of the same release. The archived JSON retained &lt;code&gt;other-open&lt;/code&gt;. The current legacy Zenodo Records API exposed a single MIT license object plus descriptive notes. DataCite's &lt;code&gt;rightsList&lt;/code&gt; and the OAI-PMH response each included MIT and CC BY 4.0. The public record's metadata had therefore evolved beyond the original release input.&lt;/p&gt;

&lt;p&gt;This is why reading only &lt;code&gt;metadata.license.id&lt;/code&gt; would have produced an incomplete account. A consumer using that field alone could miss the data license even though it was present in other exports and in the archive. Compare the rights arrays, human-readable notes and actual license files before declaring that the metadata lost or preserved every condition.&lt;/p&gt;

&lt;p&gt;Do not generalize this observation into a promise that the integration automatically reconstructs mixed licensing. The evidence establishes the archived input and the current published outputs. It does not establish that every intermediate transformation was automatic. Keep any post-publication metadata correction in the release history so later maintainers can explain the difference.&lt;/p&gt;

&lt;p&gt;A practical review assigns a row to each materially different file group: paths, copyright holder or attribution, license file and exported license identifier. If a component's rights are unclear, resolve that uncertainty before depositing it. Adding a second identifier to metadata is not a substitute for permission to distribute a file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Publish once, then reconcile the integration result
&lt;/h2&gt;

&lt;p&gt;Prepare the release from the reviewed commit, confirm the intended tag and publish through the normal GitHub release workflow. Enabling the repository is preparation; creating the GitHub release triggers the integration. A tag pushed by itself should not be treated as evidence that this documented release workflow completed.&lt;/p&gt;

&lt;p&gt;Zenodo's &lt;a href="https://help.zenodo.org/docs/github/archive-software/github-upload/" rel="noopener noreferrer"&gt;archiving guide&lt;/a&gt; explicitly includes a processing interval. Wait for the integration result, open the resulting record and compare it with the intended release. The existence of a GitHub release page is only the first observation in this sequence.&lt;/p&gt;

&lt;p&gt;Use separate operational states: release published, ingestion pending, record published, DOI resolved and metadata verified. Store a timestamp and evidence URL for each completed state. These are suggested states for your own release process; they are not a list of status names returned by every service.&lt;/p&gt;

&lt;p&gt;When ingestion reports a metadata error, inspect the error for that exact release. Correct the responsible metadata before deciding on a new release. When the result is merely unknown because a request timed out, first look for an existing matching record. Repeating publication immediately can create another object without resolving the uncertainty about the first attempt.&lt;/p&gt;

&lt;p&gt;Keep a retry decision tied to repository, tag and resolved commit. A background worker should never interpret an empty response as unconditional permission to make a fresh deposit. Record the unresolved state and the next read-only reconciliation step. This also helps a human maintainer resume after an interrupted browser session without guessing which actions occurred.&lt;/p&gt;

&lt;p&gt;For CI, put preflight checks before release creation and verification after it. A read-only verification job can fail without creating a second release. Keep publication credentials out of the verifier; reading the public evidence below does not require them. That separation gives reviewers a useful tool without handing every verification run authority to publish.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl03560xv3svyhh43fof8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl03560xv3svyhh43fof8.png" alt="Separate evidence checks connect a pinned Git commit to a Zenodo archive and version DOI, then branch to DataCite, OAI-PMH and OpenAIRE. Software Heritage remains independently unconfirmed." width="800" height="471"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;small&gt;Each box requires its own observation. A successful archive does not certify software correctness or completion in every downstream service.&lt;/small&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Verify the archive and the exported metadata
&lt;/h2&gt;

&lt;p&gt;The following Python 3 script makes a bounded set of public, read-only requests. Run it in an empty directory: it saves four response snapshots and the archive for inspection. It does not execute downloaded software, create a deposit, modify permissions or require a token. Any failed request or assertion stops the check.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;urllib.request&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;urlopen&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;xml.etree.ElementTree&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;ET&lt;/span&gt;

&lt;span class="n"&gt;DOI&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;10.5281/zenodo.21963489&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;RECORD&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;21963489&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;urlopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;write_bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;

&lt;span class="n"&gt;record&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://zenodo.org/api/records/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;RECORD&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;record.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;doi&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;DOI&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;metadata&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;version&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1.1.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;published&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="n"&gt;files&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;files&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;files&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;  &lt;span class="c1"&gt;# Expected for this particular release.
&lt;/span&gt;&lt;span class="n"&gt;archive&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;files&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;links&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;self&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;release.zip&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;archive&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;files&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;size&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;md5:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;md5&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;archive&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;files&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;checksum&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;attributes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.datacite.org/dois/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;DOI&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;datacite.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;))[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;attributes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;doi&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;DOI&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;state&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;findable&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;version&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1.1.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;rights&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rightsIdentifier&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rightsList&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cc-by-4.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;rights&lt;/span&gt;

&lt;span class="n"&gt;oai&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ET&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromstring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://zenodo.org/oai2d?verb=GetRecord&amp;amp;metadataPrefix=oai_dc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;amp;identifier=oai:zenodo.org:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;RECORD&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;oai.xml&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;ns&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;o&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://www.openarchives.org/OAI/2.0/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;oai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;o:error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ns&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;oai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findtext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.//o:header/o:identifier&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;namespaces&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ns&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;oai:zenodo.org:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;RECORD&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;openaire&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.openaire.eu/search/software?doi=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;DOI&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;&amp;amp;format=json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;openaire.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;openaire&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;response&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;header&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;total&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;$&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Archive SHA-256:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;archive&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bounded checks passed; inspect the saved metadata and match identity.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The script's expected record, version, file count and two rights identifiers are deliberately specific. Change those expectations only after reviewing a different release. Weakening the assertions until an unrelated record passes would remove the reason to run the check.&lt;/p&gt;

&lt;p&gt;The observed archive SHA-256 was &lt;code&gt;3e72a12ee4ef08095bd57e35c92ef061fe995b153cd15251bfedcb870ae30655&lt;/code&gt;. Keep that digest with the timestamp and original download URL. The service-provided MD5 is useful for checking a completed download against its record; the separately retained SHA-256 supplies a stronger content fingerprint. Neither digest establishes who authored the code or whether it is correct.&lt;/p&gt;

&lt;h2&gt;
  
  
  Read each verification result at its actual scope
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://support.datacite.org/docs/rest-api" rel="noopener noreferrer"&gt;DataCite's public API&lt;/a&gt; exposes findable DOI metadata. For this release it returned the expected creator, version, both rights declarations and an &lt;code&gt;IsVersionOf&lt;/code&gt; relationship to the concept DOI. Those checks establish registered, discoverable metadata with the expected fields. They do not establish peer review or successful software execution.&lt;/p&gt;

&lt;p&gt;OAI-PMH answers another question: can a harvester retrieve this repository record in the requested metadata format? The saved &lt;code&gt;GetRecord&lt;/code&gt; response contained the expected OAI identifier, title, DOI and both rights declarations. Inspect the XML body as well as HTTP status, because a protocol-level error can still arrive in a successful HTTP response.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://api.openaire.eu/search/software?doi=10.5281/zenodo.21963489&amp;amp;format=json" rel="noopener noreferrer"&gt;OpenAIRE software query&lt;/a&gt; returned one matching result. Its title, creator, original OAI identifier and landing-page URL matched the example. Its returned status included &lt;code&gt;UNDER_CURATION&lt;/code&gt;; the article therefore claims observed discovery, not completed curation. A result count alone would be insufficient without those identity comparisons.&lt;/p&gt;

&lt;p&gt;The script checks that count but leaves the richer OpenAIRE identity comparison visible in the saved response. In a maintained verifier, make the expected identifier and landing URL explicit assertions too. Avoid a title-only comparison: titles can change, be reused or appear on related records.&lt;/p&gt;

&lt;p&gt;Software Heritage is a further boundary. The Zenodo API response contained an empty &lt;code&gt;swh&lt;/code&gt; object during this inspection, so completed Software Heritage preservation was not established. Keep that result unconfirmed rather than promoting it to success because the DOI resolves. A later verified archival identifier can update that particular observation.&lt;/p&gt;

&lt;p&gt;These services provide useful persistence and discovery infrastructure. Their presence in a release record should not be presented as endorsement by CERN, a security audit, a license-compatibility opinion or a guarantee of search traffic. State exactly which response was inspected and what matched.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make a failed check actionable
&lt;/h2&gt;

&lt;p&gt;A verifier should distinguish a content mismatch from an unavailable observation. If the downloaded archive has the wrong checksum, stop treating that download as the expected object. Preserve the response details and compare the advertised file identity before investigating further. Do not replace the expected checksum with the newly observed value simply to restore a passing result.&lt;/p&gt;

&lt;p&gt;If a public API returns an access error, timeout or rate limit, record the affected service and leave its observation unresolved. That result does not prove the record was removed. It also does not establish that the previous successful response still describes current metadata. Retain the last verified snapshot with its original timestamp and keep the fresh failure separate.&lt;/p&gt;

&lt;p&gt;If DataCite resolves the expected DOI but shows an unexpected creator or rights list, classify the problem as a metadata mismatch. Inspect the published Zenodo metadata and the corresponding export before editing repository files. Changing the current branch cannot retroactively change which bytes were archived in the existing ZIP, so it may be the wrong repair for the observed defect.&lt;/p&gt;

&lt;p&gt;If OpenAIRE returns no matching result while the record and DOI are available, report that particular discovery check as unconfirmed. Choose a bounded later recheck appropriate to the release process. Do not claim a guaranteed indexing delay or create another deposit to make a search result appear. Discovery latency and archive identity require different responses.&lt;/p&gt;

&lt;p&gt;These branches deserve a simple responsibility map. A release maintainer owns the tag and package contents. A metadata reviewer owns creator, relationship and rights corrections. A verifier records observations and can block acceptance, but should not invent an alternative publication when a downstream service is unavailable. A small project can assign all three responsibilities to one person while still keeping their decisions distinct.&lt;/p&gt;

&lt;p&gt;Preserve enough context for another operator to diagnose a failure: request URL, UTC time, response status, expected identity and the comparison that failed. Avoid putting tokens or private account responses into a public receipt. The example uses public endpoints, so its relevant evidence can be shared without exposing publication credentials.&lt;/p&gt;

&lt;p&gt;Finally, a successful rerun should append a new observation. It should not erase the failed attempt or turn its timestamp into the time of eventual success. That history explains whether the release was initially wrong, temporarily unavailable or waiting for another service. It also prevents an incident review from confusing a later repair with what readers could access at publication time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Correct metadata without disguising a new release
&lt;/h2&gt;

&lt;p&gt;A misspelled creator or incomplete description is a metadata correction. Zenodo's &lt;a href="https://help.zenodo.org/docs/deposit/manage-records/" rel="noopener noreferrer"&gt;record-management guidance&lt;/a&gt; permits published metadata to be edited without changing the DOI. Preserve the previous and corrected values, the reason and the verification timestamp, especially when downstream exports are part of the acceptance criteria.&lt;/p&gt;

&lt;p&gt;Changes to calculation logic or pricing files create a different reproducibility object. Use the &lt;a href="https://help.zenodo.org/docs/deposit/manage-versions/" rel="noopener noreferrer"&gt;version-management workflow&lt;/a&gt; for substantive file changes, then cite the new version when reporting results produced with it. Keep the old version identifiable for people who used its earlier behavior.&lt;/p&gt;

&lt;p&gt;Avoid an absolute claim that published files can never change. The current &lt;a href="https://help.zenodo.org/docs/deposit/manage-files/" rel="noopener noreferrer"&gt;file-management documentation&lt;/a&gt; describes a limited correction window: minor file edits can be initiated within 30 days, with the draft published within 45 days of the original publication. Outside that window, justified exceptional cases go through support. That exception makes a retained archive digest worthwhile even when a DOI is stable.&lt;/p&gt;

&lt;p&gt;For this calculator, a new vendor price or formula changes the inputs behind an estimate and should be reviewed as a new version, not silently described as the old reproducible result. Updating an explanatory sentence about license scope is a different operation. Classify the change by what a reader would need to reproduce, then preserve the corresponding history.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finish with a release receipt someone else can use
&lt;/h2&gt;

&lt;p&gt;Keep the final receipt small: repository, tag, commit, record URL, version DOI, concept DOI, archive filename and digest, expected creators, license scopes, check timestamps and unresolved observations. Link each conclusion to the response or file that supports it. A green job badge without these identities is weak evidence for a later investigation.&lt;/p&gt;

&lt;p&gt;Accept the release when the intended archive is retrievable, its metadata describes the right work and the version citation resolves correctly. Report downstream discovery separately, including pending or unconfirmed services. This leaves the next maintainer with a precise starting point: a preserved object, a readable citation and a documented boundary around what was actually verified.&lt;/p&gt;

&lt;h2&gt;
  
  
  More insights to read
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://medium.com/pharos-production/the-ai-agent-reported-14-green-checks-acceptance-still-failed-8dd5320d4da7" rel="noopener noreferrer"&gt;The AI Agent Reported 14 Green Checks. Acceptance Still Failed.&lt;/a&gt; — Dmytro Nasyrov&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dmytronasyrov.medium.com/the-ai-agents-were-done-the-delivery-clock-wasnt-526401552928" rel="noopener noreferrer"&gt;The AI Agents Were Done. The Delivery Clock Wasn’t.&lt;/a&gt; — Dmytro Nasyrov&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://blog.zenodo.org/2017/11/02/version-field/" rel="noopener noreferrer"&gt;Version field launched!&lt;/a&gt; — Alex Ioannidis&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.blog/news-insights/company-news/enhanced-support-citations-github/" rel="noopener noreferrer"&gt;Enhanced support for citations on GitHub&lt;/a&gt; — Arfon Smith&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://blog.zenodo.org/2017/05/30/doi-versioning-launched/" rel="noopener noreferrer"&gt;Zenodo now supports DOI versioning!&lt;/a&gt; — Lars Holm Nielsen&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  About the author
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvvv1zfhgi0f5dtx93xa4.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvvv1zfhgi0f5dtx93xa4.jpg" width="112" height="112" alt="Portrait of Dmytro Nasyrov wearing a dark suit and light blue shirt against a dark background."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;small&gt;Dmytro Nasyrov. Photo supplied by the author.&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Written by &lt;a href="https://pharosproduction.com/dmytro-nasyrov/" rel="noopener noreferrer"&gt;Dmytro Nasyrov&lt;/a&gt; PhD, software architect with 24 years of production experience. Dmytro is the founder and CTO of Pharos Production. He works on production software architecture for FinTech, AI, Web3 and blockchain systems.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>github</category>
      <category>opensource</category>
      <category>devops</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Property-Based Testing for Upgradeable Smart Contracts: A Stateful Invariant Harness</title>
      <dc:creator>Dmytro Nasyrov</dc:creator>
      <pubDate>Wed, 16 Sep 2026 07:04:05 +0000</pubDate>
      <link>https://dev.to/pharos_production/property-based-testing-for-upgradeable-smart-contracts-a-stateful-invariant-harness-45j3</link>
      <guid>https://dev.to/pharos_production/property-based-testing-for-upgradeable-smart-contracts-a-stateful-invariant-harness-45j3</guid>
      <description>&lt;p&gt;&lt;small&gt;Cover illustration: independent state checking across a change of contract logic.&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;An upgrade test can pass while the upgraded contract has already lost its accounting history. The test may check that the implementation address changed, that a new method returns the expected value, or that deployment completed. None of those checks establishes that earlier users still have the same rights after the transition.&lt;/p&gt;

&lt;p&gt;A stateful invariant harness makes a stronger, bounded claim: after each generated action, the contract must still agree with an independently maintained model. The upgrade itself becomes one action in that sequence. Grants, spending, pauses and rejected calls happen on both sides of it.&lt;/p&gt;

&lt;p&gt;The example below runs against an actual UUPS proxy. Its correct candidate passed a seeded campaign of 128 sequences with 64 handler calls each. A deliberately broken migration failed, and Foundry reduced its failing sequence to &lt;code&gt;pause(true)&lt;/code&gt; followed by &lt;code&gt;upgrade()&lt;/code&gt;. These are results from a teaching fixture, not evidence that an unrelated protocol is safe.&lt;/p&gt;

&lt;h2&gt;
  
  
  Define the state that must survive
&lt;/h2&gt;

&lt;p&gt;Consider a small credit ledger. An administrator grants integer credits to registered actors. Each actor can spend its own credits. Pausing blocks grants and spending. Version two adds a migration marker while retaining existing balances and ownership.&lt;/p&gt;

&lt;p&gt;There are no deposits, token transfers, exchange rates or redeemable assets. Removing those features keeps the example focused on upgrade continuity. A vault handling money needs additional properties for custody, withdrawal behavior and external calls; copying this ledger would not provide them.&lt;/p&gt;

&lt;p&gt;For an upgrade project, the acceptance scope of &lt;a href="https://pharosproduction.com" rel="noopener noreferrer"&gt;Pharos Production blockchain development services&lt;/a&gt; should specify which existing rights must survive a release. A statement that the proxy is upgradeable leaves that question open. The useful deliverable is a property that a reviewer can run against the proposed implementation.&lt;/p&gt;

&lt;p&gt;Write the properties before constructing the handler. Otherwise, it is easy to model whatever the implementation happens to do and call that behavior correct.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Property&lt;/th&gt;
&lt;th&gt;Independent expectation&lt;/th&gt;
&lt;th&gt;Where the example checks it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Historical credits survive&lt;/td&gt;
&lt;td&gt;Each tracked actor retains credits minus successful spending&lt;/td&gt;
&lt;td&gt;Model invariant after generated calls&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Accounting stays consistent&lt;/td&gt;
&lt;td&gt;Contract total equals modeled actor balances and the model accumulator&lt;/td&gt;
&lt;td&gt;Same invariant&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Upgrade authority stays fixed&lt;/td&gt;
&lt;td&gt;Only the handler acting as administrator can upgrade&lt;/td&gt;
&lt;td&gt;Rejection probe and owner comparison&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pause behavior survives&lt;/td&gt;
&lt;td&gt;A paused actor cannot spend; resuming permits the selected lifecycle to continue&lt;/td&gt;
&lt;td&gt;Exact revert check and deterministic lifecycle&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Initialization cannot repeat&lt;/td&gt;
&lt;td&gt;Proxy and implementation initialization reject a second or direct attempt&lt;/td&gt;
&lt;td&gt;Rejection probe&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Candidate activation is observable&lt;/td&gt;
&lt;td&gt;Implementation slot, version and migration marker match the intended state&lt;/td&gt;
&lt;td&gt;Same invariant&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Failed migration is atomic&lt;/td&gt;
&lt;td&gt;An unpaused migration attempt leaves the original observable state intact&lt;/td&gt;
&lt;td&gt;Separate deterministic test&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The third column matters. A property named in a document but absent from executable assertions is still an assumption. Conversely, a test that checks only a total can miss a redistribution between users. The example compares individual accounts as well as the aggregate.&lt;/p&gt;

&lt;p&gt;A migration does not always preserve every value literally. A system might replace one unit with another or split an account into separate records. In that case, define a compatibility relation before testing: which economic entitlement stays equivalent, which metadata may change and what conversion rule determines the expected result. Equality is appropriate for this fixture because its upgrade promises no accounting conversion.&lt;/p&gt;

&lt;p&gt;Write down who can change each expected value. Spending changes one actor and the aggregate. A grant changes the recipient and the aggregate. Pausing changes only the operational flag. The intended upgrade changes implementation metadata and the migration marker. This compact transition contract helps reviewers notice an assertion that accidentally permits migration to rewrite unrelated balances.&lt;/p&gt;

&lt;p&gt;The model can still be wrong. Adding the same mistaken fee formula to both implementation and handler would produce agreement without correctness. Review the expected transition against product requirements, accounting rules and representative examples before relying on randomized exploration. Independence is a design choice, not a consequence of storing variables in a different contract.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pin a small, reproducible environment
&lt;/h2&gt;

&lt;p&gt;This run used Forge 1.5.1, Solidity 0.8.28, forge-std v1.9.7 and OpenZeppelin Contracts v5.4.0. Those are reproduction pins, not a claim that they are the newest versions available. The EVM target is Cancun and the optimizer uses 200 runs.&lt;/p&gt;

&lt;p&gt;Start in a new directory. The dependency revisions below are the exact commits used for the example. Install Forge 1.5.1 through the official Foundry distribution before running the commands. No RPC endpoint, wallet or funded account is required.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;upgrade-invariants
&lt;span class="nb"&gt;cd &lt;/span&gt;upgrade-invariants
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; src &lt;span class="nb"&gt;test &lt;/span&gt;lib
git clone &lt;span class="nt"&gt;--branch&lt;/span&gt; v1.9.7 &lt;span class="nt"&gt;--depth&lt;/span&gt; 1   https://github.com/foundry-rs/forge-std.git lib/forge-std
git clone &lt;span class="nt"&gt;--branch&lt;/span&gt; v5.4.0 &lt;span class="nt"&gt;--depth&lt;/span&gt; 1   https://github.com/OpenZeppelin/openzeppelin-contracts.git lib/openzeppelin-contracts
&lt;span class="c"&gt;# Verify dependency revisions:&lt;/span&gt;
git &lt;span class="nt"&gt;-C&lt;/span&gt; lib/forge-std rev-parse HEAD
&lt;span class="c"&gt;# 77041d2ce690e692d6e03cc812b57d1ddaa4d505&lt;/span&gt;
git &lt;span class="nt"&gt;-C&lt;/span&gt; lib/openzeppelin-contracts rev-parse HEAD
&lt;span class="c"&gt;# c64a1edb67b6e3f4a15cca8909c9482ad33a02b0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save this as &lt;code&gt;foundry.toml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[profile.default]&lt;/span&gt;
&lt;span class="py"&gt;src&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"src"&lt;/span&gt;
&lt;span class="py"&gt;test&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"test"&lt;/span&gt;
&lt;span class="py"&gt;libs&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"lib"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="py"&gt;solc_version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0.8.28"&lt;/span&gt;
&lt;span class="py"&gt;evm_version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"cancun"&lt;/span&gt;
&lt;span class="py"&gt;optimizer&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;optimizer_runs&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;
&lt;span class="py"&gt;remappings&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="py"&gt;["forge-std/&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="err"&gt;lib/forge-std/src/&lt;/span&gt;&lt;span class="s"&gt;", "&lt;/span&gt;&lt;span class="py"&gt;@openzeppelin/contracts/&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="err"&gt;lib/openzeppelin-contracts/contracts/&lt;/span&gt;&lt;span class="s"&gt;"]&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;
&lt;span class="nn"&gt;[profile.default.invariant]&lt;/span&gt;
&lt;span class="py"&gt;runs&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;128&lt;/span&gt;
&lt;span class="py"&gt;depth&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;64&lt;/span&gt;
&lt;span class="py"&gt;fail_on_revert&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;show_metrics&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The configuration makes unexpected handler reverts fail the campaign. Expected denials are asserted inside the handler, so they do not appear as failed handler calls. That distinction will explain the zero-revert metric later.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://getfoundry.sh/forge/invariant-testing?highlight=invariant" rel="noopener noreferrer"&gt;official Foundry invariant-testing guide&lt;/a&gt; documents handler targeting and ghost variables. This article applies those mechanisms to one specific upgrade transition. It does not require increasing a fuzz budget until an attractive result appears.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep the implementation intentionally small
&lt;/h2&gt;

&lt;p&gt;Save the following as &lt;code&gt;src/Ledger.sol&lt;/code&gt;. The first implementation stores credits and their total. The second adds one field and a versioned migration. The final contract is an intentionally defective candidate used only to check that the test can fail.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// SPDX-License-Identifier: MIT
pragma solidity 0.8.28;

import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol";
import {UUPSUpgradeable} from "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol";

// Teaching fixture: credits are integers, not redeemable assets.
contract LedgerV1 is Initializable, UUPSUpgradeable {
    address public owner;
    bool public paused;
    mapping(address =&amp;gt; uint256) public credit;
    uint256 public total;
    error Unauthorized();
    error Paused();
    error Insufficient();
    error MustPause();

    constructor() { _disableInitializers(); }
    modifier onlyOwner() {
        if (msg.sender != owner) revert Unauthorized();
        _;
    }
    function initialize(address admin) external initializer { owner = admin; }
    function setPaused(bool value) external onlyOwner { paused = value; }
    function grant(address user, uint256 amount) external onlyOwner {
        if (paused) revert Paused();
        credit[user] += amount;
        total += amount;
    }
    function spend(uint256 amount) external {
        if (paused) revert Paused();
        if (credit[msg.sender] &amp;lt; amount) revert Insufficient();
        credit[msg.sender] -= amount;
        total -= amount;
    }
    function version() external pure virtual returns (uint256) { return 1; }
    function _authorizeUpgrade(address) internal override onlyOwner {}
}

contract LedgerV2 is LedgerV1 {
    uint256 public migrationMarker;
    function initializeV2() public virtual reinitializer(2) onlyOwner {
        if (!paused) revert MustPause();
        migrationMarker = 2;
    }
    function version() external pure override returns (uint256) { return 2; }
}

// Deliberate negative control. Never deploy this candidate.
contract BrokenV2 is LedgerV2 {
    function initializeV2() public override reinitializer(2) onlyOwner {
        if (!paused) revert MustPause();
        migrationMarker = 2;
        total = 0;
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The proxy receives encoded initializer data during construction. Its owner becomes the handler, which will represent the authorized administrator. Each implementation locks its own initializer in its constructor. These are different storage contexts: initializing the proxy does not make direct initialization of an implementation a useful or safe operation.&lt;/p&gt;

&lt;p&gt;OpenZeppelin gives the relevant warning directly:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Do not leave an implementation contract uninitialized.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;OpenZeppelin, &lt;a href="https://docs.openzeppelin.com/upgrades-plugins/writing-upgradeable#initializing-the-implementation-contract" rel="noopener noreferrer"&gt;Writing Upgradeable Contracts&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The example inherits the pinned UUPS implementation and defines its authorization hook with &lt;code&gt;onlyOwner&lt;/code&gt;. Inspect the &lt;a href="https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v5.4.0/contracts/proxy/utils/UUPSUpgradeable.sol" rel="noopener noreferrer"&gt;v5.4.0 UUPS source&lt;/a&gt; when reproducing this behavior with a different dependency version. A method named &lt;code&gt;upgradeToAndCall&lt;/code&gt; alone does not establish who may execute it.&lt;/p&gt;

&lt;p&gt;Version two deliberately preserves the base declaration order and appends its marker. This is a simple fixture, not a general storage-layout approval procedure. Real changes involving inheritance, packed values or namespaced storage require their own compatibility analysis.&lt;/p&gt;

&lt;p&gt;The pause requirement belongs to &lt;code&gt;initializeV2&lt;/code&gt;, not to every possible upgrade call. An authorized owner could choose another candidate or omit migration data. The positive campaign models the intended release procedure; it does not prove that a malicious administrator cannot bypass that procedure. If pausing before every upgrade is a production requirement, encode and test that requirement at the authorization boundary.&lt;/p&gt;

&lt;p&gt;The broken candidate resets &lt;code&gt;total&lt;/code&gt; without changing account credits. It is intentionally obvious. Its purpose is to establish that the oracle notices a state discontinuity at migration time, before anyone needs to spend afterward.&lt;/p&gt;

&lt;h2&gt;
  
  
  Model history outside the proxy
&lt;/h2&gt;

&lt;p&gt;The handler keeps a separate expected balance for each of three actors. It also maintains an expected total, pause state and implementation address. None of those expectations is copied from the proxy after an upgrade.&lt;/p&gt;

&lt;p&gt;That separation is the heart of the test. If the handler read the migrated total and adopted it as the new expected value, the broken migration would teach the test to accept its own corruption. The model must describe the intended effect of an accepted action, not merely repeat the result returned by the system.&lt;/p&gt;

&lt;p&gt;Seed each actor with 100 credits before fuzzing. An empty ledger would make a reset-to-zero mutation invisible until later activity created a difference. A small nonzero history makes this defect observable even when the generated sequence contains only a pause and an upgrade.&lt;/p&gt;

&lt;p&gt;The actor set is deliberately closed. Grants target only these addresses, which makes their sum meaningful. In a real protocol, maintain a registry of every account the campaign can credit or debit, including fee recipients and custody contracts. Otherwise, an aggregate comparison can exclude balances that the test created itself.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;bound&lt;/code&gt; limits grants to positive amounts and successful spending to the actor's modeled balance. It helps the campaign reach useful states, but it also narrows the tested domain. Overspending, zero-value behavior and arithmetic extremes are outside that success path and deserve targeted tests when they matter to the actual contract.&lt;/p&gt;

&lt;p&gt;The model changes only after a successful call. A paused spending attempt expects the precise &lt;code&gt;Paused&lt;/code&gt; error and leaves expected balances unchanged. Catching every exception and continuing would obscure both an unexpected denial and a defect in the handler.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F54gkm7i5d58ratjn2qnh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F54gkm7i5d58ratjn2qnh.png" alt="Stateful invariant harness with a fuzzer feeding five handler actions, a proxy whose implementation changes from V1 to V2, and an independent model compared after every action." width="800" height="490"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;small&gt;The handler drives the proxy. The model records intended effects independently; assertions compare the two after each generated action.&lt;/small&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Make the allowed actions explicit
&lt;/h2&gt;

&lt;p&gt;Create &lt;code&gt;test/UpgradeInvariant.t.sol&lt;/code&gt; with the next two code blocks in order. The first contains its imports and the handler. The second contains the test contract in the same file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// SPDX-License-Identifier: MIT
pragma solidity 0.8.28;

import {Test} from "forge-std/Test.sol";
import {StdInvariant} from "forge-std/StdInvariant.sol";
import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol";
import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
import {LedgerV1, LedgerV2, BrokenV2} from "../src/Ledger.sol";

contract Handler is Test {
    LedgerV1 public ledger;
    address public immutable first;
    address public immutable next;
    address public expectedImplementation;
    uint256[3] public expected;
    uint256 public expectedTotal;
    bool public expectedPaused;
    bool public upgraded;
    uint256 public grants;
    uint256 public spends;
    uint256 public blockedSpends;
    uint256 public probes;
    uint256 public upgrades;
    uint256 public postUpgradeSpends;

    constructor(address candidate) {
        next = candidate;
        first = address(new LedgerV1());
        ledger = LedgerV1(address(new ERC1967Proxy(
            first, abi.encodeCall(LedgerV1.initialize, (address(this)))
        )));
        expectedImplementation = first;
        for (uint256 i; i &amp;lt; 3; ++i) {
            ledger.grant(actor(i), 100);
            expected[i] = 100;
            expectedTotal += 100;
        }
    }

    function actor(uint256 i) public pure returns (address) {
        return address(uint160(0x100 + i));
    }

    function grant(uint256 who, uint256 raw) external {
        uint256 i = who % 3;
        uint256 amount = bound(raw, 1, 10_000);
        if (expectedPaused) {
            vm.expectRevert(LedgerV1.Paused.selector);
            ledger.grant(actor(i), amount);
            return;
        }
        ledger.grant(actor(i), amount);
        expected[i] += amount;
        expectedTotal += amount;
        grants++;
    }

    function spend(uint256 who, uint256 raw) external {
        uint256 i = who % 3;
        if (expectedPaused) {
            vm.expectRevert(LedgerV1.Paused.selector);
            vm.prank(actor(i));
            ledger.spend(1);
            blockedSpends++;
            return;
        }
        if (expected[i] == 0) return;
        uint256 amount = bound(raw, 1, expected[i]);
        vm.prank(actor(i));
        ledger.spend(amount);
        expected[i] -= amount;
        expectedTotal -= amount;
        spends++;
        if (upgraded) postUpgradeSpends++;
    }

    function pause(bool value) external {
        ledger.setPaused(value);
        expectedPaused = value;
    }

    function upgrade() external {
        if (upgraded || !expectedPaused) return;
        ledger.upgradeToAndCall(next, abi.encodeCall(LedgerV2.initializeV2, ()));
        expectedImplementation = next;
        upgraded = true;
        upgrades++;
    }

    function probe() external {
        vm.expectRevert(LedgerV1.Unauthorized.selector);
        vm.prank(actor(0));
        ledger.upgradeToAndCall(next, "");
        vm.expectRevert(LedgerV1.Unauthorized.selector);
        vm.prank(actor(0));
        ledger.setPaused(!expectedPaused);
        vm.expectRevert(Initializable.InvalidInitialization.selector);
        ledger.initialize(actor(0));
        vm.expectRevert(Initializable.InvalidInitialization.selector);
        LedgerV1(first).initialize(actor(0));
        vm.expectRevert(Initializable.InvalidInitialization.selector);
        LedgerV1(next).initialize(actor(0));
        if (upgraded) {
            vm.expectRevert(Initializable.InvalidInitialization.selector);
            LedgerV2(address(ledger)).initializeV2();
        }
        probes++;
    }
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;probe&lt;/code&gt; groups rejected operations whose expected outcome is unchanged state. It attempts an unauthorized upgrade and pause change, repeats proxy initialization and tries to initialize both implementation contracts directly. After a successful upgrade, it also repeats the version-two initializer.&lt;/p&gt;

&lt;p&gt;The probe uses specific errors. An unrelated revert is not acceptable evidence that access control worked. The model invariant then checks the owner and implementation slot as well as accounting state. Keeping those checks together helps expose a control-plane change even when user balances still look correct.&lt;/p&gt;

&lt;p&gt;The testing methods listed in &lt;a href="https://pharosproduction.com/services/smart-contracts-development/" rel="noopener noreferrer"&gt;smart contract development services&lt;/a&gt; at Pharos Production include Foundry fuzz testing. For an upgrade release, that activity becomes reviewable when its handover includes the actual handler selectors, independent model and failing counterexample. The service description supplies context for the method; the executable fixture supplies this article's evidence.&lt;/p&gt;

&lt;p&gt;There are five fuzz targets: grant, spend, pause, upgrade and probe. Public getters and inherited test helpers are excluded from the selector list. Targeting the handler contract explicitly also prevents automatically discovered implementation contracts from becoming unintended campaign targets.&lt;/p&gt;

&lt;p&gt;The outer caller chosen by the fuzzer is not a modeled end user. Authorized calls originate from the handler, while spending uses &lt;code&gt;vm.prank&lt;/code&gt; to represent one of the fixed actors. That keeps the authority model understandable. Expanding the sender population without defining roles would change the experiment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check the whole state, then force a complete lifecycle
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;contract UpgradeInvariantTest is StdInvariant, Test {
    Handler internal handler;
    bytes32 internal constant IMPLEMENTATION_SLOT =
        0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;

    function setUp() public {
        address candidate = vm.envOr("BROKEN", false)
            ? address(new BrokenV2()) : address(new LedgerV2());
        handler = new Handler(candidate);
        bytes4[] memory selectors = new bytes4[](5);
        selectors[0] = Handler.grant.selector;
        selectors[1] = Handler.spend.selector;
        selectors[2] = Handler.pause.selector;
        selectors[3] = Handler.upgrade.selector;
        selectors[4] = Handler.probe.selector;
        targetContract(address(handler));
        targetSelector(FuzzSelector(address(handler), selectors));
    }

    function invariant_modelMatchesProxy() public view {
        LedgerV1 v = handler.ledger();
        uint256 sum;
        for (uint256 i; i &amp;lt; 3; ++i) {
            assertEq(v.credit(handler.actor(i)), handler.expected(i), "actor credit");
            sum += handler.expected(i);
        }
        assertEq(v.total(), sum, "total vs model");
        assertEq(v.total(), handler.expectedTotal(), "model accumulator");
        assertEq(v.owner(), address(handler), "owner");
        assertEq(v.paused(), handler.expectedPaused(), "pause state");
        assertEq(address(uint160(uint256(vm.load(address(v), IMPLEMENTATION_SLOT)))),
            handler.expectedImplementation(), "implementation");
        assertEq(v.version(), handler.upgraded() ? 2 : 1, "version");
        if (handler.upgraded()) {
            assertEq(LedgerV2(address(v)).migrationMarker(), 2, "migration");
        }
    }

    function test_failedMigrationIsAtomic() public {
        LedgerV1 v = handler.ledger();
        address candidate = handler.next();
        vm.expectRevert(LedgerV1.MustPause.selector);
        vm.prank(address(handler));
        v.upgradeToAndCall(candidate, abi.encodeCall(LedgerV2.initializeV2, ()));
        invariant_modelMatchesProxy();
    }

    function test_requiredLifecycle() public {
        handler.grant(0, 17);
        handler.spend(1, 9);
        handler.pause(true);
        handler.spend(0, 1);
        handler.probe();
        handler.upgrade();
        invariant_modelMatchesProxy();
        handler.probe();
        handler.pause(false);
        handler.spend(0, 1);
        invariant_modelMatchesProxy();
        assertGt(handler.grants(), 0);
        assertGt(handler.spends(), 0);
        assertGt(handler.blockedSpends(), 0);
        assertEq(handler.upgrades(), 1);
        assertGt(handler.postUpgradeSpends(), 0);
        assertEq(handler.probes(), 2);
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;a href="https://eips.ethereum.org/EIPS/eip-1967#logic-contract-address" rel="noopener noreferrer"&gt;ERC-1967 specification&lt;/a&gt; identifies the implementation storage slot. Reading it gives the test an observation independent of &lt;code&gt;version()&lt;/code&gt;. A candidate that simply reports version two must not satisfy an assertion that a particular implementation was installed.&lt;/p&gt;

&lt;p&gt;All persistent state checks live in one invariant function. They therefore inspect the same sequence at each check. The assertions cover the chosen accounts, total, owner, pause state and active implementation, followed by the migration marker when version two should be active.&lt;/p&gt;

&lt;p&gt;A successful call to &lt;code&gt;upgrade&lt;/code&gt; does not reset the model. It changes only expected implementation metadata. Preserving the ghost balances across that boundary is what makes a reset or redistribution visible.&lt;/p&gt;

&lt;p&gt;Random selection does not promise that every sequence completes the entire release lifecycle. &lt;code&gt;upgrade&lt;/code&gt; returns without action before pausing or after an earlier upgrade. &lt;code&gt;spend&lt;/code&gt; can return when its selected account has no credits. Those choices are visible limits on exploration, not evidence that every generated call accomplished useful work.&lt;/p&gt;

&lt;p&gt;The deterministic lifecycle test closes one specific gap. It guarantees a grant and spend before migration, a denied spend while paused, an upgrade, a repeated-initialization probe and successful spending after resumption. Its counter assertions make that path explicit. They do not establish transition coverage for all random campaigns.&lt;/p&gt;

&lt;p&gt;The atomicity test exercises a different branch: migration while unpaused must fail, and the invariant must still describe version one afterward. Notice that &lt;code&gt;handler.next()&lt;/code&gt; is read before arming &lt;code&gt;expectRevert&lt;/code&gt; and &lt;code&gt;prank&lt;/code&gt;. An external getter placed between those cheatcodes and the intended call can consume the next-call expectation and produce a fixture failure unrelated to migration behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run the positive case and the negative control
&lt;/h2&gt;

&lt;p&gt;Run the normal suite with the fixed seed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;FOUNDRY_CACHE_PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cache-positive   forge &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;--fuzz-seed&lt;/span&gt; 0x9162026 &lt;span class="nt"&gt;-vv&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The September 16, 2026 run returned three passing tests: the model invariant, failed-migration atomicity and the required lifecycle. The invariant campaign executed 8,192 handler calls across 128 runs at depth 64. No unexpected handler revert or discarded call was reported.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Handler selector&lt;/th&gt;
&lt;th&gt;Calls reported in the passing campaign&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;grant&lt;/td&gt;
&lt;td&gt;1,661&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pause&lt;/td&gt;
&lt;td&gt;1,653&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;probe&lt;/td&gt;
&lt;td&gt;1,591&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;spend&lt;/td&gt;
&lt;td&gt;1,636&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;upgrade&lt;/td&gt;
&lt;td&gt;1,651&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These figures are selector invocations, not successful economic operations. A paused grant follows an expected rejection path. An upgrade invocation can do nothing because its precondition is absent or migration already happened. The explicit success counters in the lifecycle test answer a narrower, separate question.&lt;/p&gt;

&lt;p&gt;Now select the broken candidate without changing the property:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;BROKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true &lt;/span&gt;&lt;span class="nv"&gt;FOUNDRY_CACHE_PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cache-negative   forge &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;--match-test&lt;/span&gt; invariant_modelMatchesProxy   &lt;span class="nt"&gt;--fuzz-seed&lt;/span&gt; 0x9162026 &lt;span class="nt"&gt;-vv&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This command is expected to exit unsuccessfully. In the observed run, Foundry found an eight-call failing sequence and shrank it to two calls: pause, then upgrade. The initial seeded credits explain why those two calls suffice. Resetting the aggregate violates the model even without a generated grant.&lt;/p&gt;

&lt;p&gt;Separate cache directories keep the negative-control failure corpus apart from the positive campaign. The candidate switch is explicit through &lt;code&gt;BROKEN&lt;/code&gt;; it does not rewrite the invariant or relax an assertion. Do not run the negative control as an ordinary green CI job and then ignore its exit status.&lt;/p&gt;

&lt;p&gt;A passing negative control would block acceptance of this harness. It would mean the intended defect was not reached, the observation was missing, or the oracle had accepted the defect. Raising the run count would not be the first repair; inspect the target path and expected state first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Review the harness as executable code
&lt;/h2&gt;

&lt;p&gt;Inspect every early return. In this handler, an upgrade outside the modeled paused state is intentionally skipped. That helps explore the intended workflow, but it removes unauthorized sequencing from that action's domain. The separate atomicity test covers an unpaused migration attempt; it does not cover every possible authorized upgrade payload. A reviewer should be able to map each exclusion to a separate check or an explicit limitation.&lt;/p&gt;

&lt;p&gt;Inspect every privilege shortcut as well. &lt;code&gt;vm.prank&lt;/code&gt; changes the caller for testing; it does not demonstrate that a deployed administrator can assemble signatures or execute a governance proposal. The fixed actor addresses are identities inside the local test environment. They are not funded production accounts, and the campaign makes no statement about key custody.&lt;/p&gt;

&lt;p&gt;Expected-revert tests need a successful neighboring path. If all spending reverted for an unrelated reason, a pause denial alone could look reassuring. The lifecycle therefore includes spending before pausing and after resumption. This establishes that the tested denial sits between working paths for the selected actor and amount. It remains a concrete example, not a proof of universal withdrawal availability.&lt;/p&gt;

&lt;p&gt;Finally, distinguish state safety from progress. Equality checks can show that an attempted action did not corrupt the observed ledger. They cannot show that a necessary upgrade will eventually be proposed, approved or executed. Operational readiness requires a separate owner and execution procedure. A property called eventual recovery would need a defined time model and assumptions about the actors who must act.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preserve the failure as release evidence
&lt;/h2&gt;

&lt;p&gt;A seed alone is not a portable proof. Keep the source revision with compiler settings and dependency commits. Preserve the generated sequence, the candidate-selection environment and the tool version. A later toolchain can make different generation or shrinking decisions even when the numeric seed matches.&lt;/p&gt;

&lt;p&gt;For a real defect, convert the reduced sequence into a named regression test. Include its required initial state. Here, removing the seeded balances would change the meaning of the two-call reproducer, so a bare list of method names is incomplete evidence.&lt;/p&gt;

&lt;p&gt;Keep the first failing log as well as the minimized trace. Do not treat every number in a failure message as a value recomputed from the shortest sequence; inspect the replay before reporting intermediate balances. The article's reproducible claim is the accounting failure and the reduced action sequence, not a guessed value at an unseen trace step.&lt;/p&gt;

&lt;p&gt;When diagnosing a failed campaign, identify whether the failing assertion belongs to the protocol model, an expected-revert probe or the harness itself. A bad caller, wrong selector or misplaced cheatcode can stop useful exploration. Correct the fixture without weakening the requirement, then rerun the affected campaign with a retained explanation.&lt;/p&gt;

&lt;p&gt;Release evidence should name the exact candidate it covers. A green result for yesterday's implementation is not automatically evidence for a rebuild, a dependency change or different initializer arguments. Treat those changes as new inputs to the relevant checks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Extend the model to the real release boundary
&lt;/h2&gt;

&lt;p&gt;Replace the integer ledger only after writing the properties of the actual system. For a share-based vault, model the intended relationship between deposits, shares and withdrawals, including permitted rounding. Avoid computing the expected answer by calling the same conversion function that the implementation uses; both sides would inherit the same mistake.&lt;/p&gt;

&lt;p&gt;External assets introduce additional behavior. Fee-on-transfer tokens, callbacks and unusual return values can invalidate a handler that assumes a simple transfer. Those cases require explicit fixtures and observations. This example neither executes external transfers nor tests reentrancy.&lt;/p&gt;

&lt;p&gt;A governance-controlled system needs its real authorization route. Represent proposal creation, delay and execution through the timelock or multisig rather than using a privileged prank for every update. Keep expected denials for unauthorized callers. A mock owner proves only the owner-based boundary implemented here.&lt;/p&gt;

&lt;p&gt;Migration can be incremental. If users are converted lazily, the model needs pre-migration and post-migration account states with rules for crossing between them. A single global version flag cannot describe users whose data is at different stages. Define which operations remain valid at each stage and which historical quantities must still reconcile.&lt;/p&gt;

&lt;p&gt;Events need their own observations when off-chain consumers depend on them. This harness reads contract state and does not assert event contents or ordering. An indexer could therefore receive an incorrect migration event while every property here passes. Add an event assertion or integration test where an external consumer relies on that message to change its interpretation of stored data.&lt;/p&gt;

&lt;p&gt;Similarly, an authorization probe for upgrading does not establish authorization on every administrator method. The example exercises a particular set of denied calls. A production review should enumerate privileged entry points and identify the test responsible for each one, including newly introduced methods in the candidate implementation.&lt;/p&gt;

&lt;p&gt;A production pause policy may permit withdrawals while blocking deposits. Encode that asymmetry directly instead of copying this fixture's blanket pause on grants and spending. Likewise, distinguish safe resumption from a claim that reverting implementation code restores all earlier state. An invariant campaign does not undo transactions or establish rollback feasibility.&lt;/p&gt;

&lt;p&gt;When adding another property, identify the defect it would catch that existing assertions miss. A second assertion of the same total under another name adds little. A check for an account that was previously absent from the actor registry changes coverage. A check for an external recipient changes the observation boundary. Keep that distinction visible in code review so the suite grows with the system's behavior rather than with a checklist of reassuring names.&lt;/p&gt;

&lt;p&gt;Retain layout validation alongside behavioral tests. This fixture checks three known accounts and a few public fields; it cannot inspect every possible mapping key or prove that all storage interpretations are compatible. Static compatibility checks, deployment validation and a fork rehearsal each answer questions the local campaign leaves open.&lt;/p&gt;

&lt;p&gt;The practical release decision is specific: the reviewed candidate agrees with the stated model over the executed paths, the required lifecycle completes, and a controlled violation causes failure. List the untested paths beside that result. That gives the next engineer something to reproduce, challenge and extend before the upgrade reaches users.&lt;/p&gt;

&lt;h2&gt;
  
  
  More insights to read
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.bulbapp.io/p/e2580ef7-324b-442d-b818-92b8c8442b3d/five-smart-contract-upgrade-patterns-compared" rel="noopener noreferrer"&gt;Five Smart Contract Upgrade Patterns Compared&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dmytronasyrov.medium.com/upgradeable-solidity-smart-contracts-part-1-versioning-7e6e97cafc28" rel="noopener noreferrer"&gt;Upgradeable Solidity Smart Contracts. Part 1 — Versioning&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://medium.com/pharos-production/smart-contracts-their-potential-and-real-limitations-part-1-222fe44ee14c" rel="noopener noreferrer"&gt;Smart Contracts. Their Potential and Real Limitations. Part 1&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://medium.com/pharos-production/smart-contracts-their-potential-and-real-limitations-part-2-40942c055d79" rel="noopener noreferrer"&gt;Smart Contracts. Their Potential and Real Limitations. Part 2&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://medium.com/pharos-production/web3-smart-contracts-oracles-part-1-3905b127c01d" rel="noopener noreferrer"&gt;Web3. Smart Contracts. Oracles. Part 1&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  About the author
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzoazi4278bedr68mju8l.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzoazi4278bedr68mju8l.jpg" width="112" height="112" alt="Portrait of Dmytro Nasyrov wearing a dark suit and light blue shirt against a dark background."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;small&gt;Dmytro Nasyrov. Photo supplied by the author.&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Written by &lt;a href="https://pharosproduction.com/dmytro-nasyrov/" rel="noopener noreferrer"&gt;Dmytro Nasyrov&lt;/a&gt; PhD, software architect with 24 years of production experience. Dmytro is the founder and CTO of Pharos Production. He works on production software architecture for FinTech, AI, Web3 and blockchain systems.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>solidity</category>
      <category>web3</category>
      <category>testing</category>
      <category>security</category>
    </item>
    <item>
      <title>10 Smart Contract Development Companies Compared by Repository and Release Evidence in 2026</title>
      <dc:creator>Dmytro Nasyrov</dc:creator>
      <pubDate>Tue, 15 Sep 2026 08:05:41 +0000</pubDate>
      <link>https://dev.to/pharos_production/10-smart-contract-development-companies-compared-by-repository-and-release-evidence-in-2026-m59</link>
      <guid>https://dev.to/pharos_production/10-smart-contract-development-companies-compared-by-repository-and-release-evidence-in-2026-m59</guid>
      <description>&lt;p&gt;A smart contract development company should be able to connect the code it proposes to ship with the tests, review decisions and deployment records that justify shipping it. That connection is the basis of this comparison. A service page establishes what a company offers; a repository can expose implementation artifacts; a release record must explain which artifacts reached which environment.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pharosproduction.com" rel="noopener noreferrer"&gt;Pharos Production's smart contract development team&lt;/a&gt; prepared this list and selected its order. The positions are editorial, with the publishing company first. They are not security scores or a claim that the first company has the strongest public repository.&lt;/p&gt;

&lt;p&gt;The review cutoff is &lt;strong&gt;September 15, 2026&lt;/strong&gt;. All ten companies have relevant published service descriptions. For four, this bounded review also inspected attributable public repository metadata and file trees. No builds were executed, no companies were contacted, and no client audit-to-deployment chain was independently verified. The comparison therefore separates observed artifacts from the release evidence still needed before a hiring decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the comparison can establish
&lt;/h2&gt;

&lt;p&gt;Each company receives the same four fields: published scope, repository observation, release evidence to request, and a limitation. A missing repository observation means this review has no attributable sample for that company. It does not mean the company has no repositories or cannot provide confidential evidence.&lt;/p&gt;

&lt;p&gt;For the inspected samples, a commit identifier fixes the file-tree observation. A test directory proves that files exist at that commit; it does not prove that the tests run or detect meaningful failures. A package release proves that a release was published; it does not establish that a customer's contracts were audited or deployed from it.&lt;/p&gt;

&lt;p&gt;Use the company profiles to decide what to inspect next. Use the common release packet and hard stops below to decide whether the evidence is sufficient. Keep those decisions separate from price, staffing availability and contractual terms, which this review did not assess.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Pharos Production: Smart Contract Development Company
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Published scope.&lt;/strong&gt; The smart contract service description covers Solidity engineering, architecture, automated testing and deployment pipelines. For a buyer facing a gap between tested code and deployment, those services provide a relevant scope to discuss; delivery quality still requires artifacts from the proposed engagement.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repository observation.&lt;/strong&gt; The company-linked GitHub organization includes the &lt;code&gt;openzeppelin-solidity&lt;/code&gt; fork. At commit &lt;code&gt;1238d8f&lt;/code&gt;, its tree contains contracts, tests, a dependency lock and audit material inherited within the repository. The inspected repository's releases endpoint returned no releases.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Release evidence to request.&lt;/strong&gt; Ask for an attributable delivery sample, the team's changes, test execution records and a deployment manifest tied to the reviewed commit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Limitation.&lt;/strong&gt; An upstream library fork and its audit files do not establish the company's own audit coverage, client release history or current delivery-team competence.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. ScienceSoft
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Published scope.&lt;/strong&gt; Its smart contract development page describes consulting, implementation, testing, blockchain deployment and oracle integration with external systems.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repository observation.&lt;/strong&gt; The inspected service page did not supply an attributable repository sample for this review. Its statements about testing and audits remain published service claims, rather than independently reproduced release evidence.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Release evidence to request.&lt;/strong&gt; For an oracle-dependent contract, request the release commit, external-data configuration and tests covering stale observations, invalid values and unavailable providers. The deployment record should identify the actual data source and the authority allowed to replace it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Limitation.&lt;/strong&gt; The public scope does not establish how a proposed team handles those failure cases. A successful integration example would also need its operational assumptions and excluded dependencies before it could support a release decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. PixelPlex
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Published scope.&lt;/strong&gt; Its smart contract page describes requirements discovery, architecture, implementation, security testing, controlled deployment and subsequent support.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repository observation.&lt;/strong&gt; No attributable project repository was selected from that service-page inspection. The described sequence supplies useful questions for a release review, but it does not provide a commit, reproducible result or deployment receipt.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Release evidence to request.&lt;/strong&gt; Ask the proposed lead to trace one contract change through implementation, review, tests, deployment configuration and post-launch checks. For a token or NFT system, include minting permissions, transfer restrictions and the handling of administrative changes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Limitation.&lt;/strong&gt; A published development process does not establish that every engagement follows it. Review the actual release package and responsibility split, especially when the buyer or a separate auditor owns part of the delivery process.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. SoluLab
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Published scope.&lt;/strong&gt; The current dApp development page covers smart contracts alongside application development, testing and deployment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repository observation.&lt;/strong&gt; Its verified GitHub organization contains an Ethereum boilerplate fork and a separate public &lt;code&gt;Internal-ChatBids-SmartContract&lt;/code&gt; repository. The latter's tree at &lt;code&gt;6cef7c7&lt;/code&gt; includes program sources, lockfiles, tests and a deployment migration. Its releases endpoint returned no releases.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Release evidence to request.&lt;/strong&gt; Request a recent project on the intended chain, including the application's contract interface, deployment configuration and a trace of a failed transaction through the user interface and backend.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Limitation.&lt;/strong&gt; The inspected sample establishes file availability, not test success, production use or current support. Its Rust program structure should not be treated as evidence of equivalent Solidity delivery without a relevant EVM sample.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Boosty Labs
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Published scope.&lt;/strong&gt; The company describes blockchain engineering and provides a direct link to its GitHub organization. Its engagement scope includes development capacity that can participate in a wider delivery team.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repository observation.&lt;/strong&gt; The public &lt;code&gt;ultimatedivision-smartcontracts&lt;/code&gt; tree at &lt;code&gt;93b1abf&lt;/code&gt; contains Solidity contracts, tests, deployment migrations and a dependency lock. The commit is dated September 6, 2022; the inspected releases endpoint returned no releases.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Release evidence to request.&lt;/strong&gt; Ask for a current comparable sample and identify who owns code review, audit remediation, deployment approval and operational handover when engineers join the buyer's team.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Limitation.&lt;/strong&gt; This older repository cannot establish current release practices or the capability of engineers assigned in 2026. Repository push metadata and the date of the inspected commit are different facts and should not be substituted for one another.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Unicsoft
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Published scope.&lt;/strong&gt; Its smart contract service material discusses development, external-system dependencies and concerns including synchronization and performance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repository observation.&lt;/strong&gt; The inspected service page did not provide an attributable repository sample for this review. Its scope is relevant to an integration-heavy project, while its implementation and release history remain unresolved here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Release evidence to request.&lt;/strong&gt; Request one versioned interface contract covering off-chain inputs, on-chain state transitions and recovery from interrupted synchronization. Then ask for the tests and deployment settings that enforce those assumptions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Limitation.&lt;/strong&gt; Describing integration risks does not prove that a particular delivery team has implemented the required controls. A migration or synchronization demonstration must specify its starting state and the data that cannot be recovered automatically after an interruption.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Antier
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Published scope.&lt;/strong&gt; Its smart contract page lists development, auditing and optimization, with deployment and testing in the described process.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repository observation.&lt;/strong&gt; The service-page review did not establish a project repository, release commit or independently checked audit trail. The scope is a starting point for requesting those artifacts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Release evidence to request.&lt;/strong&gt; For a value-handling contract, ask for the accounting properties, tests of privileged actions and the exact code covered by security review. If optimization is proposed, compare behavior before and after the change under the same compiler and workload assumptions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Limitation.&lt;/strong&gt; An optimization claim is insufficient without its baseline and correctness checks. An audit service description also leaves open who performed a particular review, what it excluded and whether later changes received further assessment.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Vention
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Published scope.&lt;/strong&gt; Its smart contract offering includes design, development, audits, application integration and automated testing. It also describes team augmentation and other delivery models.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repository observation.&lt;/strong&gt; No attributable repository sample was established from the inspected service page. The page describes access controls and multisignature arrangements, without proving the authority configuration of a proposed deployment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Release evidence to request.&lt;/strong&gt; Ask for a release responsibility map alongside the code: who approves changes, controls deployment credentials, resolves findings and accepts residual risk. Require a versioned authority manifest and evidence that the deployed controllers match it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Limitation.&lt;/strong&gt; Staffing arrangements can place these responsibilities on different organizations. A technically capable contributor does not, by that fact alone, own the complete release process or the buyer's production approval.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Cheesecake Labs
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Published scope.&lt;/strong&gt; Its blockchain offering includes smart contracts across Stellar, Solana, Ethereum and Sui, plus tokenization, wallets and DeFi systems.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repository observation.&lt;/strong&gt; Its linked GitHub organization publishes &lt;code&gt;stellar-plus&lt;/code&gt;. The inspected development-branch tree at &lt;code&gt;e3a44fb&lt;/code&gt; includes unit tests and test-coverage and package-publishing workflows. The releases list includes &lt;code&gt;v0.14.4&lt;/code&gt;, published August 7, 2025, targeting &lt;code&gt;main&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Release evidence to request.&lt;/strong&gt; Resolve the selected release tag to its commit, then inspect its build and test records. For a proposed application, separately request the contract deployment manifest and audit scope.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Limitation.&lt;/strong&gt; The inspected branch commit must not be assumed to be the release commit. This SDK offers inspectable engineering artifacts, but it does not establish a customer's audited contract deployment or equivalent expertise across every advertised chain.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Hyperlink InfoSystem
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Published scope.&lt;/strong&gt; Its smart contract material presents requirements, development, testing and deployment within a broader application delivery process.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repository observation.&lt;/strong&gt; The inspected service page did not establish an attributable contract repository or a release packet. Consequently, implementation, test execution and production reconciliation remain unverified in this comparison.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Release evidence to request.&lt;/strong&gt; Ask for a project that connects a wallet-facing application to contract execution. Trace one successful transaction and one rejected transaction through request creation, signing, submission, confirmation and the application's displayed state.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Limitation.&lt;/strong&gt; General application testing does not establish correct handling of chain-specific failure modes. The proposed team must identify which failures its contract tests cover and which require integration or operational checks outside the contract repository.&lt;/p&gt;

&lt;h2&gt;
  
  
  Read the evidence matrix before making a shortlist
&lt;/h2&gt;

&lt;p&gt;The same contract-to-release gap motivates the &lt;a href="https://pharosproduction.com/services/smart-contracts-development/" rel="noopener noreferrer"&gt;smart contract testing and deployment services&lt;/a&gt; described in the publishing company's service scope. Treat that description as a statement of offered work. Apply the artifact requirements below to the publisher and every other candidate before crediting a delivery claim.&lt;/p&gt;

&lt;p&gt;In this matrix, &lt;strong&gt;unverified&lt;/strong&gt; means no complete client audit-to-deployment relation was established during this review. It is a review status, not a verdict on the company's work. The commit prefixes identify snapshots inspected for this article; buyers should retain complete hashes in their own records.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Company&lt;/th&gt;
&lt;th&gt;Inspected public artifact&lt;/th&gt;
&lt;th&gt;Snapshot or release observation&lt;/th&gt;
&lt;th&gt;Client audit-to-deployment relation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Pharos Production&lt;/td&gt;
&lt;td&gt;Upstream smart contract library fork&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;1238d8f&lt;/code&gt;; files present; no repository releases returned&lt;/td&gt;
&lt;td&gt;Unverified&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ScienceSoft&lt;/td&gt;
&lt;td&gt;Service description&lt;/td&gt;
&lt;td&gt;No repository sample established&lt;/td&gt;
&lt;td&gt;Unverified&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PixelPlex&lt;/td&gt;
&lt;td&gt;Service description&lt;/td&gt;
&lt;td&gt;No repository sample established&lt;/td&gt;
&lt;td&gt;Unverified&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SoluLab&lt;/td&gt;
&lt;td&gt;Contract program repository&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;6cef7c7&lt;/code&gt;; tests and migration present; no releases returned&lt;/td&gt;
&lt;td&gt;Unverified&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Boosty Labs&lt;/td&gt;
&lt;td&gt;Solidity contract repository&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;93b1abf&lt;/code&gt;; tests and migrations present; no releases returned&lt;/td&gt;
&lt;td&gt;Unverified&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unicsoft&lt;/td&gt;
&lt;td&gt;Service description&lt;/td&gt;
&lt;td&gt;No repository sample established&lt;/td&gt;
&lt;td&gt;Unverified&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Antier&lt;/td&gt;
&lt;td&gt;Service description&lt;/td&gt;
&lt;td&gt;No repository sample established&lt;/td&gt;
&lt;td&gt;Unverified&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vention&lt;/td&gt;
&lt;td&gt;Service description&lt;/td&gt;
&lt;td&gt;No repository sample established&lt;/td&gt;
&lt;td&gt;Unverified&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cheesecake Labs&lt;/td&gt;
&lt;td&gt;Stellar SDK repository&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;e3a44fb&lt;/code&gt; tree; separate &lt;code&gt;v0.14.4&lt;/code&gt; release observed&lt;/td&gt;
&lt;td&gt;Unverified&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hyperlink InfoSystem&lt;/td&gt;
&lt;td&gt;Service description&lt;/td&gt;
&lt;td&gt;No repository sample established&lt;/td&gt;
&lt;td&gt;Unverified&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These differences change the next review step. An attributable repository lets a reviewer ask about specific files immediately. A service-only candidate first needs to supply a suitable sample. Neither route skips release verification. A private demonstration can be stronger evidence than an unrelated public repository, provided the reviewer can inspect the relevant artifacts and record what was demonstrated.&lt;/p&gt;

&lt;h3&gt;
  
  
  Compare answers without inventing a numerical ranking
&lt;/h3&gt;

&lt;p&gt;Suppose two candidates provide different evidence rooms. One supplies a recent public repository with a polished README but cannot identify the deployed configuration. The other supplies a supervised private demonstration that resolves the build, audit changes and deployment manifest. For the release decision, the second demonstration answers more of the required questions. Public visibility remains useful, but it is not an acceptance criterion by itself.&lt;/p&gt;

&lt;p&gt;Write down the question each artifact answers. A source tree can answer what files were present. A reproducible build can answer how an output was created. A review report can answer what someone examined. A deployment receipt can answer where a transaction executed. None automatically answers the neighboring question. This keeps the comparison tied to evidence rather than presentation quality.&lt;/p&gt;

&lt;p&gt;Use a small set of descriptive outcomes: demonstrated for the agreed scope, partially demonstrated with a named gap, or not demonstrated. Give every gap an owner and a follow-up requirement. Keep a material failure separate from minor documentation cleanup; an unresolved upgrade controller should not disappear inside an average score.&lt;/p&gt;

&lt;p&gt;The proposed delivery team should participate in the review. A sample produced by another team may illustrate an organizational process, but the buyer still needs to know who can maintain it. Ask the assigned lead to explain one design trade-off and locate the corresponding implementation and test. Record the answer's scope without turning the meeting into an unpaid production exercise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Request one release packet from every shortlisted company
&lt;/h2&gt;

&lt;p&gt;Select a system close to the intended chain, asset flow and authority model. Give every candidate the same request and acceptance criteria. Do not let one present a simple token while another must explain a lending protocol, unless the difference is explicitly part of the scope decision.&lt;/p&gt;

&lt;p&gt;The packet should identify the repository and complete commit hash, compiler version and settings, dependency locks, build instructions, test configuration and execution records. It should also contain the security-review scope, finding dispositions, deployment manifest and operational owners. Record unavailable fields as unavailable, rather than accepting a slide deck as an equivalent substitute.&lt;/p&gt;

&lt;p&gt;This is an application of build provenance to the release decision. &lt;a href="https://slsa.dev/spec/v1.2/build-provenance" rel="noopener noreferrer"&gt;SLSA's provenance specification&lt;/a&gt; describes recording how an artifact was produced, including its build definition and execution details. A buyer can use that principle without claiming SLSA compliance: identify the inputs, the builder and the output being approved.&lt;/p&gt;

&lt;p&gt;For confidential work, agree a controlled review format. A sanitized repository or supervised demonstration can protect client material while showing the process. Record whether the sample represents a production engagement, a reference implementation or a training exercise. Each can answer useful questions, but only within its stated scope.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgwlm73voqnenjxkgdbuz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgwlm73voqnenjxkgdbuz.png" alt="A release evidence chain connects a source commit to build and test records, audit scope, deployment, and reconciliation; any changed input requires review of affected evidence." width="800" height="410"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;small&gt;Release evidence must remain connected when code or configuration changes. Diagram by the author.&lt;/small&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Check the repository against the claim being made
&lt;/h2&gt;

&lt;p&gt;Begin with a clean, isolated review environment and the documented build procedure. Record the toolchain and dependency resolution used. The expected output needs an identity, such as a digest and build metadata, that can be compared with the release artifact. A successful local build is only one observation; preserve the associated logs and configuration.&lt;/p&gt;

&lt;p&gt;Next, inspect a small number of important properties. For an escrow, a buyer might require that only the authorized party releases funds and that recorded liabilities remain consistent with held assets under the defined token model. For a minting system, examine issuance authority and supply constraints. The properties must follow the actual design, including fees, rounding and external-token behavior.&lt;/p&gt;

&lt;p&gt;A useful demonstration introduces a reversible defect in an isolated copy and shows the relevant test fail. This checks whether the assertion detects the selected failure. It does not measure the whole team's ability or justify claims about all possible attacks. Record the defect, expected failure and restoration of the original commit.&lt;/p&gt;

&lt;p&gt;Inspect review exceptions as carefully as passing checks. A suppressed finding, excluded directory or accepted risk should identify its scope, rationale and owner. A test result becomes harder to interpret when the reviewer cannot tell which code paths or configuration it omitted. This is why repository structure and green badges are insufficient on their own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reconcile the audit, deployment and authority
&lt;/h2&gt;

&lt;p&gt;An audit report needs a scope commit or an equally precise source boundary. Trace findings to fixes and retest decisions. Then compare that boundary with the candidate release. Changes after the audit require a disposition: reviewed, assessed as outside the relevant scope, or still unresolved. A later branch name cannot substitute for that accounting.&lt;/p&gt;

&lt;p&gt;For an EVM deployment, preserve the chain identifier, transaction receipt, contract address, compiler settings, linked libraries and constructor or initialization inputs. Where proxies are involved, distinguish the proxy from its implementation and identify the authority that can change either relevant configuration or implementation selection.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://ethereum.org/en/developers/docs/smart-contracts/verifying/" rel="noopener noreferrer"&gt;Ethereum's contract verification guidance&lt;/a&gt; explains the role of checking source code against deployed bytecode. That relationship helps establish what is running. It does not prove that the business logic is correct, that the audit covered it or that administrators cannot change its behavior later.&lt;/p&gt;

&lt;p&gt;Consider a hypothetical release: an auditor reviewed commit A, the team fixed a finding in B, and deployment used C after an administrator change. Tests passing on B do not resolve C's new authority behavior. The buyer needs the B-to-C difference, its review disposition and confirmation of the deployed controllers before approving that release.&lt;/p&gt;

&lt;p&gt;The same reasoning applies when source stays unchanged but configuration moves. Replacing an oracle, changing an initializer argument or assigning a different controller can alter the system's behavior. An approval should therefore identify the code and the relevant deployment configuration together. Reconcile actual state against the manifest after deployment, rather than assuming the script's intended inputs became the final state.&lt;/p&gt;

&lt;h3&gt;
  
  
  Preserve the decision when a release changes
&lt;/h3&gt;

&lt;p&gt;Give the acceptance record a stable release identifier and retain the evidence it references. A later code or configuration change should create a new review entry with a link to the previous decision. Describe the affected assumptions and the checks repeated. This makes a small change reviewable without pretending that the earlier approval covers every future state.&lt;/p&gt;

&lt;p&gt;For example, changing an administrator address may leave bytecode untouched while changing who can authorize an upgrade. The follow-up check should inspect the controller's configured authority and the transfer outcome. Re-running an unrelated unit suite would not answer that question. Conversely, a documentation correction need not trigger a complete technical rehearsal if it changes no approved input or assumption.&lt;/p&gt;

&lt;p&gt;Have the receiving operator confirm that the handover is usable. They should be able to identify the deployed release, locate its unresolved risks and find the approved containment procedure. Record where the supplier's responsibility ends and the operator's begins. This final check turns a collection of documents into something the buyer can use after the engagement ends.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hard stops that override an attractive proposal
&lt;/h2&gt;

&lt;p&gt;Pause acceptance when the team cannot identify the reviewed commit, reproduce the agreed build, connect material findings to their dispositions or explain deployed privileged authority. Also pause when the demonstrated artifact differs from the one proposed for release and nobody can account for the difference.&lt;/p&gt;

&lt;p&gt;Missing public code alone is not a hard stop. Refusing every reasonable way to demonstrate the claimed delivery process is. Likewise, an older sample may explain engineering decisions, but it should not silently stand in for evidence that the currently assigned team can operate the current toolchain.&lt;/p&gt;

&lt;p&gt;Vitalik Buterin described the need for layered security in his 2016 essay, &lt;a href="https://blog.ethereum.org/2016/06/19/thinking-smart-contract-security" rel="noopener noreferrer"&gt;Thinking About Smart Contract Security&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;There will be further bugs, and we will learn further lessons; there will not be a single magic technology that solves everything.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The procurement consequence is practical: no audit badge, public repository or named tool should carry the whole decision. Require connected evidence, identify its limits and assign responsibility for what remains unresolved.&lt;/p&gt;

&lt;p&gt;End the review with a short acceptance record: the company and proposed team, demonstrated system, exact release boundary, artifacts inspected, checks performed, unresolved items and decision owner. State what must be supplied before the next stage. Preserve that record when the release changes so the team can see which earlier conclusions still apply.&lt;/p&gt;

&lt;p&gt;This comparison gives ten starting points and a common way to evaluate them. The strongest next step is to ask a shortlisted team to explain one release through its actual artifacts. Choose on the evidence it can demonstrate for the work you need, with every gap visible before deployment approval.&lt;/p&gt;

&lt;h2&gt;
  
  
  More insights to read
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.bulbapp.io/p/e2580ef7-324b-442d-b818-92b8c8442b3d/five-smart-contract-upgrade-patterns-compared" rel="noopener noreferrer"&gt;Five Smart Contract Upgrade Patterns Compared&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dmytronasyrov.medium.com/upgradeable-solidity-smart-contracts-part-1-versioning-7e6e97cafc28" rel="noopener noreferrer"&gt;Upgradeable Solidity Smart Contracts. Part 1 — Versioning&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://medium.com/pharos-production/web3-smart-contracts-oracles-part-1-3905b127c01d" rel="noopener noreferrer"&gt;Web3. Smart Contracts. Oracles. Part 1&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://medium.com/pharos-production/smart-contracts-their-potential-and-real-limitations-part-1-222fe44ee14c" rel="noopener noreferrer"&gt;Smart Contracts. Their Potential and Real Limitations. Part 1&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://medium.com/pharos-production/smart-contracts-their-potential-and-real-limitations-part-2-40942c055d79" rel="noopener noreferrer"&gt;Smart Contracts. Their Potential and Real Limitations. Part 2&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  About the author
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff87rh3rqhcbkjdpamosn.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff87rh3rqhcbkjdpamosn.jpg" width="800" height="800" alt="Portrait of Dmytro Nasyrov wearing a dark suit and light blue shirt against a dark background."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;small&gt;Dmytro Nasyrov. Photo supplied by the author.&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Written by &lt;a href="https://pharosproduction.com/dmytro-nasyrov/" rel="noopener noreferrer"&gt;Dmytro Nasyrov&lt;/a&gt; PhD, software architect with 24 years of production experience. Dmytro is the founder and CTO of Pharos Production. He works on production software architecture for FinTech, AI, Web3 and blockchain systems.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>solidity</category>
      <category>web3</category>
      <category>testing</category>
      <category>security</category>
    </item>
    <item>
      <title>How to Rehearse Smart-Contract Rollback Before Calling a System Upgradeable</title>
      <dc:creator>Dmytro Nasyrov</dc:creator>
      <pubDate>Sat, 12 Sep 2026 07:06:15 +0000</pubDate>
      <link>https://dev.to/pharos_production/how-to-rehearse-smart-contract-rollback-before-calling-a-system-upgradeable-3532</link>
      <guid>https://dev.to/pharos_production/how-to-rehearse-smart-contract-rollback-before-calling-a-system-upgradeable-3532</guid>
      <description>&lt;p&gt;A smart contract can accept a new implementation and still have no safe route back. The upgrade transaction may succeed, the old bytecode may remain available, and an administrator may retain permission to install it. None of those facts proves that the old code can interpret the state users have created since the upgrade.&lt;/p&gt;

&lt;p&gt;Rehearse recovery against those later states before describing a system as operationally upgradeable. The useful result is a manifest that identifies the exact deployment, the checkpoint tested, the recovery action permitted there and the evidence that user rights survive it. A successful pointer change is only one observation in that record.&lt;/p&gt;

&lt;p&gt;This guide develops that manifest for a hypothetical EVM vault. Its scenarios are proposed tests, not results from a deployed protocol. Adapt the accounting, dependencies and authority model to the system under review.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Define the recovery promise before choosing a command
&lt;/h2&gt;

&lt;p&gt;Use three separate terms in the runbook. An implementation rollback reinstalls an earlier implementation through the system's supported upgrade mechanism. A state repair transforms particular stored values under a reviewed procedure. A service recovery restores an acceptable user operation, possibly through a forward fix or a controlled migration. One incident may require all three, but each needs its own success criteria.&lt;/p&gt;

&lt;p&gt;When upgrade acceptance stops at deployment success, the missing deliverable is evidence of recovery. &lt;a href="https://pharosproduction.com" rel="noopener noreferrer"&gt;Pharos Production&lt;/a&gt; documents a blockchain delivery process that includes contract testing, security review and staged deployment. A release review can attach the rehearsal described here to those activities and make the recovery assumptions explicit. That is a proposed acceptance artifact, not a claim that every contract has a reversible migration.&lt;/p&gt;

&lt;p&gt;For the example vault, define the promise as follows: an existing user retains the same valid withdrawal entitlement after recovery, subject only to documented fees and rounding; a pending withdrawal remains identifiable and cannot be paid twice; operators can resume only the functions whose invariants have passed. Specify how each condition will be measured before executing any recovery transaction.&lt;/p&gt;

&lt;p&gt;Also write down what the promise excludes. Restoring one contract's implementation cannot by itself reclaim a payment already received by another party, erase a message already executed on another chain or reverse a decision made by an external service. Those effects require separate authority and a separate reconciliation procedure.&lt;/p&gt;

&lt;p&gt;Give the promise an explicit scope: one vault, its asset contract, the withdrawal queue and any settlement adapter. A statement about the vault alone should not silently become a claim about the entire protocol.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Freeze the deployed system you intend to rehearse
&lt;/h2&gt;

&lt;p&gt;Start from the actual deployment inventory. Record the chain identity, fork block number and block hash, proxy addresses, active implementation addresses and runtime bytecode hashes. Include the source commit, compiler version and settings, dependency lockfile and storage-layout artifacts used to explain that bytecode. A repository branch name is insufficient because it can move.&lt;/p&gt;

&lt;p&gt;Resolve the upgrade topology before selecting the recovery transaction. OpenZeppelin's &lt;a href="https://docs.openzeppelin.com/contracts/5.x/api/proxy" rel="noopener noreferrer"&gt;proxy reference&lt;/a&gt; distinguishes transparent proxies, UUPS implementations and beacon-based deployments. Their upgrade logic and control points differ. In a beacon system, enumerate every proxy that follows the affected beacon; testing one instance does not establish compatibility for instances with different initialization histories.&lt;/p&gt;

&lt;p&gt;For a UUPS deployment, establish that the currently installed implementation still exposes a usable authorized upgrade route. Do not assume that a route present in the previous release remains callable. Record any compatibility restriction that prevents reinstalling a particular historical version. The recovery target must be admissible through the deployed mechanism, not merely available in an artifact directory.&lt;/p&gt;

&lt;p&gt;Build the local fork at a fixed block. &lt;a href="https://www.getfoundry.sh/anvil/index.html" rel="noopener noreferrer"&gt;Anvil's official documentation&lt;/a&gt; describes local forking, controlled mining, state management and account impersonation. These capabilities support a rehearsal, but each convenience changes what the exercise proves. Pin the tool version and relevant chain configuration, verify the starting block hash against the recorded source chain, and keep the transaction destination confined to the local test environment.&lt;/p&gt;

&lt;p&gt;Document injected assumptions alongside the fixture: extra test balances, impersonated actors, mocked oracle responses and altered timestamps. Use dedicated test credentials. Production signing material is unnecessary for testing contract authorization rules, and its presence makes a local exercise harder to keep isolated.&lt;/p&gt;

&lt;p&gt;Finally, choose representative existing positions. Include a long-lived depositor, an account with a pending withdrawal, an empty account and any privileged account with special accounting treatment. Record why each position matters. A fork containing real storage is still a weak fixture if every test touches only a newly created user.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Test the meaning of storage in both directions
&lt;/h2&gt;

&lt;p&gt;A forward storage-layout check asks whether the new implementation can interpret the earlier layout. Recovery introduces another question: can the old implementation interpret every relevant state the new release is allowed to produce?&lt;/p&gt;

&lt;p&gt;OpenZeppelin makes the persistence issue concrete in &lt;a href="https://docs.openzeppelin.com/upgrades-plugins/writing-upgradeable" rel="noopener noreferrer"&gt;Writing Upgradeable Contracts&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;And if you remove a variable from the end of the contract, note that the storage will not be cleared.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The quotation concerns removing a variable from a contract definition. Its relevance to recovery is that changing code does not erase historical storage. The same documentation warns against incompatible changes to variable ordering and types. A layout validation therefore belongs in the release evidence, while the rehearsal must also address the meaning of the values already written.&lt;/p&gt;

&lt;p&gt;Consider a hypothetical vault whose first implementation stores withdrawal requests in asset units. A later version migrates those requests into shares while retaining a similarly shaped numeric field. The old implementation might read a perfectly ordinary integer after reinstallation and interpret it using the wrong unit. Successful reads and unchanged slot locations would not establish correct entitlements.&lt;/p&gt;

&lt;p&gt;Make a state-meaning table for every changed field: previous interpretation, new interpretation, transition that writes the new form and behavior if old code reads it. Include enumerations, sentinel values, rounding conventions, timestamps and identifiers. Mark the first transition that makes a direct return invalid. That boundary can occur during initialization, the first deposit or a later maintenance transaction.&lt;/p&gt;

&lt;p&gt;Use a concrete accounting fixture to expose the difference. Suppose the vault holds 1,000 asset units against 500 shares, with no fees or rounding in this example. A request for ten asset units becomes a request for five shares during migration. If old code later treats the stored five as asset units, the user receives only half the original entitlement. A test that merely confirms the request still exists would pass. A test that settles the request and compares the payment with its expected ten asset units would fail. Preserve both the stored representation and the economic expectation in the fixture so the assertion does not accidentally reuse the faulty conversion logic.&lt;/p&gt;

&lt;p&gt;Treat initializers and migrations as state transitions with their own preconditions. Determine whether a recovery requires additional initialization, whether a version guard prevents it and whether replaying a migration could duplicate an allocation. The answer must come from the specific contract and reviewed payload. Avoid a generic instruction to call the initializer again.&lt;/p&gt;

&lt;p&gt;If the reverse interpretation is undefined, record direct rollback as prohibited at that checkpoint. That finding is useful before release. Hiding it behind a green deployment test would turn a known architectural constraint into an incident-time surprise.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Branch the rehearsal at three checkpoints
&lt;/h2&gt;

&lt;p&gt;Run independent branches from the pinned baseline. In each branch, apply the exact upgrade payload, advance to its named checkpoint and execute the recovery action against that checkpoint's state. Preserve transaction order and the arguments for every intervening operation.&lt;/p&gt;

&lt;p&gt;The first checkpoint is immediately after the upgrade transaction. If installation and migration occur atomically, this checkpoint already includes that migration; there is no accessible production state between the two. Do not manufacture an intermediate recovery window that the actual transaction never exposes.&lt;/p&gt;

&lt;p&gt;The second checkpoint is after any separate migration or initialization work. The third is after representative user activity and external interactions. These checkpoints describe progressively different conditions, not a guarantee that recovery becomes harder in a predictable numerical way.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftm9dtq4cem7pvbdb5zwl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftm9dtq4cem7pvbdb5zwl.png" alt="Three independent rehearsal branches start at the same pinned baseline and test recovery after installation, migration and user activity. Each branch checks invariants before allowing resume or requiring repair." width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Rehearsal branches for the hypothetical vault. A local snapshot resets the test fixture; recovery acts on the state produced within a branch.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Use a small scenario matrix tied to the release's actual changes:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Branch&lt;/th&gt;
&lt;th&gt;State reached&lt;/th&gt;
&lt;th&gt;Recovery attempt&lt;/th&gt;
&lt;th&gt;Required observation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Installation&lt;/td&gt;
&lt;td&gt;Upgrade payload completed&lt;/td&gt;
&lt;td&gt;Supported return to prior code&lt;/td&gt;
&lt;td&gt;Existing positions still behave correctly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Migration&lt;/td&gt;
&lt;td&gt;Changed records converted&lt;/td&gt;
&lt;td&gt;Approved repair or forward fix&lt;/td&gt;
&lt;td&gt;Record meaning and ownership reconcile&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;New activity&lt;/td&gt;
&lt;td&gt;Deposit and withdrawal requested&lt;/td&gt;
&lt;td&gt;Checkpoint-specific recovery&lt;/td&gt;
&lt;td&gt;Claims remain payable exactly once&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;External effect&lt;/td&gt;
&lt;td&gt;Settlement adapter acted&lt;/td&gt;
&lt;td&gt;Containment and reconciliation&lt;/td&gt;
&lt;td&gt;External obligations remain accounted for&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Interrupted operation&lt;/td&gt;
&lt;td&gt;One step pending or reverted&lt;/td&gt;
&lt;td&gt;Resume the recorded procedure&lt;/td&gt;
&lt;td&gt;No duplicated action or lost request&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Use local snapshots to create repeatable starting conditions, then distinguish their resets from the action being tested. A test that upgrades, restores the baseline snapshot and calls an old function demonstrates the old fixture. It says nothing about old code running against the post-upgrade state. Keep the checkpoint evidence before any fixture reset.&lt;/p&gt;

&lt;p&gt;Order matters within a branch. A withdrawal requested before migration can exercise a different path from one requested afterward. A deposit followed by a withdrawal may expose a unit conversion that either operation alone misses. Select sequences from the changed behavior and known invariants, rather than expanding into a large arbitrary matrix.&lt;/p&gt;

&lt;p&gt;Include at least one deliberate incompatibility in the fixture or expectations. The harness should reject a recovery action that violates the declared unit or ownership rule. A suite that passes both the intended case and a clearly invalid case cannot support the release decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Exercise the authority path and the waiting period
&lt;/h2&gt;

&lt;p&gt;Rehearse through the actual control contracts. If the production route requires a multisig proposal followed by a timelock, the local sequence should exercise the corresponding proposal, authorization and execution checks. Calling an implementation directly as an impersonated administrator bypasses the part of the system that determines whether recovery is available.&lt;/p&gt;

&lt;p&gt;Maintain a clear distinction between simulated authority and operational availability. Impersonation can test behavior for a particular caller. It does not prove that enough people can access their signing devices during an incident. Advancing the local clock can test a delay condition. It does not measure detection time, approval time, network congestion or the time needed to inspect the result.&lt;/p&gt;

&lt;p&gt;Record those durations separately. Use measured operational evidence where it exists and label unmeasured estimates. The contract waiting period, signing process and detection path together determine what the system can do while affected operations remain available. Rehearse that exposure interval with realistic allowed actions, including a transaction already queued before a pause.&lt;/p&gt;

&lt;p&gt;Recovery belongs in the same release conversation as audit remediation. The &lt;a href="https://pharosproduction.com/services/how-we-build-blockchain-solutions/" rel="noopener noreferrer"&gt;blockchain delivery process documented by Pharos Production&lt;/a&gt; includes testing, internal review, external audit coordination and staged deployment. Attach the checkpoint manifest to that process so a reviewer can identify which recovery payload was assessed and which subsequent writes invalidate it. This adds an inspectable condition to delivery without turning an audit into a guarantee of reversibility.&lt;/p&gt;

&lt;p&gt;Test the pause boundary precisely. Identify the functions a guardian can stop, functions that remain callable and the authority required to resume them. A pause that prevents deposits but leaves an unsafe settlement path open does not provide the containment assumed by the runbook. A pause that blocks every exit may also change the recovery obligations to users.&lt;/p&gt;

&lt;p&gt;Include a stale operation in the scenario. After choosing a recovery path, verify what happens to an already scheduled upgrade or maintenance transaction. Cancel it where the governance design permits cancellation, or demonstrate that its preconditions prevent execution against the recovered state. Leave no unexplained pending payload capable of undoing the repair.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Verify user outcomes after the recovery transaction
&lt;/h2&gt;

&lt;p&gt;A successful receipt establishes that a transaction executed without reverting. It does not establish that balances, claims and permissions are correct. Capture observations before the upgrade, at the failed checkpoint and after recovery, then reconcile the expected changes between them.&lt;/p&gt;

&lt;p&gt;For the hypothetical vault, start with ownership and accounting. Every sampled withdrawal request should retain its owner, status and entitlement under the declared accounting model. A completed payment must not remain claimable. A pending payment must not disappear merely because the previous implementation ignores a field introduced by the upgrade.&lt;/p&gt;

&lt;p&gt;Define conservation using the system's actual assets and liabilities. Track deposits, withdrawals, fees and any permitted gain or loss in consistent units. Explain rounding tolerances and their maximum aggregate effect. A bare comparison between the vault's token balance and total shares is usually not a complete accounting rule because the quantities may have different meanings.&lt;/p&gt;

&lt;p&gt;Check behavior as well as getters. Have a representative user finish a withdrawal, create a new permitted request and encounter the intended restriction on an invalid request. Verify allowances, role membership, pause settings and request identifiers where the release can affect them. A readable position is not necessarily a usable position.&lt;/p&gt;

&lt;p&gt;Record the population covered by the checks. Sampling representative accounts is useful for scenario design, but it does not prove that every mapping entry survived a migration. If the migration touches a bounded set of records, reconcile that complete set. If the population is large, define the mechanism that establishes coverage, such as an enumerated migration input with totals and per-record checks. State the residual uncertainty rather than presenting a sample as exhaustive evidence.&lt;/p&gt;

&lt;p&gt;Then examine consumers outside the upgraded contract. An indexer may have processed events emitted before recovery. A keeper may hold a pending job. An adapter may have accepted an identifier that the old code no longer understands. Record how these components rebuild, reject stale work or reconcile their records. The chain's current storage is only part of the service state.&lt;/p&gt;

&lt;p&gt;Make every assertion report expected and actual values. When one fails, preserve the smallest reproducible sequence that reaches the discrepancy. This gives a reviewer an explanation of the boundary that broke instead of a screenshot containing a red test name.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Select recovery from the state that actually exists
&lt;/h2&gt;

&lt;p&gt;Choose a direct implementation rollback only when the deployed mechanism permits it and the relevant post-upgrade states remain compatible with the earlier code. Rehearse the exact historical bytecode and the actual return payload. Recompiling an old source tag under different settings creates a different artifact to review.&lt;/p&gt;

&lt;p&gt;If a reversible migration is part of the design, test its inverse as a separate operation. Identify the information needed to restore the earlier representation, the records it will touch and the transaction boundaries. If the forward migration discarded information, the inverse requires another trustworthy source for it; calling the procedure a rollback does not supply the missing data.&lt;/p&gt;

&lt;p&gt;For a migration spread across transactions, rehearse a partially processed population. Record which entries use the old representation and which use the new one. Check that a retry cannot convert the same record twice and that recovery does not assume every batch completed. Run the largest supported batch under the intended gas conditions and preserve the boundary at which another transaction is required. Local success with an artificially generous block gas limit would not establish that the same batch is executable on the target chain.&lt;/p&gt;

&lt;p&gt;A forward repair may preserve valid new state while correcting the faulty behavior. Its acceptance test must cover both the original defect and the recovery obligations. Reusing the upgrade route is convenient, but the repair is still new code with new assumptions. Do not substitute confidence in the previous release for review of the repair.&lt;/p&gt;

&lt;p&gt;Some checkpoints should permit containment only. After an external payment or cross-chain execution, the immediate action may be to stop further affected operations and establish a reconciled claim set. Compensation, migration or administrator-assisted processing can require a separate decision. Record the unresolved obligation and its owner before allowing unrelated activity to obscure it.&lt;/p&gt;

&lt;p&gt;Keep the incident decision narrow. The manifest should identify the permitted action for each observed checkpoint and the evidence needed to choose it. An operator should not need to invent a new storage transformation while deciding whether the system is safe to resume.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Preserve a manifest that another reviewer can reproduce
&lt;/h2&gt;

&lt;p&gt;The manifest is the durable output of the rehearsal. Store it beside the release artifacts and bind its evidence to immutable hashes. Keep confidential endpoint credentials and signing material out of it; a provider label and the required access characteristics are enough to describe the environment.&lt;/p&gt;

&lt;p&gt;The following is a template, not a completed run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;rehearsal_id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;vault-release-candidate-01&lt;/span&gt;
&lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;NOT_RUN&lt;/span&gt;
&lt;span class="na"&gt;baseline&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;chain_id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;REQUIRED&lt;/span&gt;
  &lt;span class="na"&gt;block_number&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;REQUIRED&lt;/span&gt;
  &lt;span class="na"&gt;block_hash&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;REQUIRED&lt;/span&gt;
  &lt;span class="na"&gt;deployment_inventory_hash&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;REQUIRED&lt;/span&gt;
&lt;span class="na"&gt;release&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;source_commit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;REQUIRED&lt;/span&gt;
  &lt;span class="na"&gt;build_and_dependency_manifest_hash&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;REQUIRED&lt;/span&gt;
  &lt;span class="na"&gt;implementation_code_hashes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;REQUIRED&lt;/span&gt;
  &lt;span class="na"&gt;upgrade_payload_hash&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;REQUIRED&lt;/span&gt;
&lt;span class="na"&gt;scenario&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;checkpoint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;REQUIRED&lt;/span&gt;
  &lt;span class="na"&gt;ordered_transactions_hash&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;REQUIRED&lt;/span&gt;
  &lt;span class="na"&gt;simulated_privileges_and_time_changes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;REQUIRED&lt;/span&gt;
&lt;span class="na"&gt;recovery&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;permitted_action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;REQUIRED&lt;/span&gt;
  &lt;span class="na"&gt;payload_hash&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;REQUIRED&lt;/span&gt;
  &lt;span class="na"&gt;authority_and_delay_evidence&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;REQUIRED&lt;/span&gt;
&lt;span class="na"&gt;verification&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;invariant_definitions_hash&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;REQUIRED&lt;/span&gt;
  &lt;span class="na"&gt;expected_and_actual_results_hash&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;REQUIRED&lt;/span&gt;
  &lt;span class="na"&gt;covered_records_and_exclusions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;REQUIRED&lt;/span&gt;
  &lt;span class="na"&gt;receipts_traces_and_state_diff_hash&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;REQUIRED&lt;/span&gt;
&lt;span class="na"&gt;decision&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;reviewer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;REQUIRED&lt;/span&gt;
  &lt;span class="na"&gt;outcome&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;UNDECIDED&lt;/span&gt;
  &lt;span class="na"&gt;unresolved_obligations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;REQUIRED&lt;/span&gt;
  &lt;span class="na"&gt;invalidation_conditions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;REQUIRED&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Give every scenario its own record or unambiguous entry. Do not overwrite a failed rehearsal with a successful rerun. Preserve the failed payload and observations, then link the revised run to the change that resolved them. The release decision should identify the exact accepted record rather than whichever file currently has the newest timestamp.&lt;/p&gt;

&lt;p&gt;Retain enough environment information to reproduce the observation: tool version, chain configuration, dependency versions and any mock behavior. An archived result without a usable fixture can help incident analysis, but it is weaker evidence for a future release than a procedure another engineer can execute.&lt;/p&gt;

&lt;p&gt;Keep local transaction receipts clearly labeled as rehearsal evidence. A local receipt is not a production receipt, and a local block height is not evidence that a live operation occurred. Store production approvals and eventual deployment observations in separately identified records linked to the same release.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Make the release claim expire when its assumptions change
&lt;/h2&gt;

&lt;p&gt;Approve recovery per checkpoint. A release can legitimately support direct rollback before user activity while requiring forward repair afterward. Put that distinction in the operational runbook and the release decision so the team knows when the simpler route stops being valid.&lt;/p&gt;

&lt;p&gt;Define invalidation conditions before deployment: different implementation bytecode, changed migration input, altered authority, a new dependency configuration or a newly enabled operation outside the rehearsed states. A recent rehearsal is not automatically current when the conditions it tested have changed.&lt;/p&gt;

&lt;p&gt;Specify abort conditions as executable preflight checks wherever possible. An unexpected current implementation hash, an unknown migration version or an unresolved settlement record should stop the selected procedure before its first write. Test those rejection paths as well as the accepted path. Record who investigates an abort and how the incident remains contained while the assumption is unresolved. This prevents a prepared recovery payload from becoming an instruction to proceed regardless of the state operators actually find.&lt;/p&gt;

&lt;p&gt;Before the real upgrade, compare the intended payload and deployment inventory with the accepted manifest. Review material state drift since the pinned block and rerun the affected scenarios when it changes their preconditions. After deployment, inspect the actual implementation and resulting state before resuming affected operations. Resolve an uncertain transaction outcome from on-chain evidence before submitting another potentially duplicative action.&lt;/p&gt;

&lt;p&gt;The defensible claim is specific: this release has a demonstrated recovery procedure for these states, with these remaining limitations. That gives operators a usable decision under pressure and gives reviewers evidence they can challenge. Upgradeability becomes an operational property only when the system can reach an acceptable state through an authorized, understood and rehearsed procedure.&lt;/p&gt;

&lt;h2&gt;
  
  
  More insights to read
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.bulbapp.io/p/e2580ef7-324b-442d-b818-92b8c8442b3d/five-smart-contract-upgrade-patterns-compared" rel="noopener noreferrer"&gt;Five Smart Contract Upgrade Patterns Compared&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dmytronasyrov.medium.com/upgradeable-solidity-smart-contracts-part-1-versioning-7e6e97cafc28" rel="noopener noreferrer"&gt;Upgradeable Solidity Smart Contracts. Part 1 — Versioning&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://medium.com/pharos-production/smart-contracts-their-potential-and-real-limitations-part-2-40942c055d79" rel="noopener noreferrer"&gt;Smart Contracts. Their Potential and Real Limitations. Part 2&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://medium.com/pharos-production/smart-contracts-their-potential-and-real-limitations-part-1-222fe44ee14c" rel="noopener noreferrer"&gt;Smart Contracts. Their Potential and Real Limitations. Part 1&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://medium.com/pharos-production/web3-smart-contracts-oracles-part-1-3905b127c01d" rel="noopener noreferrer"&gt;Web3. Smart Contracts. Oracles. Part 1&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  About the author
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F81i2l07e00u2ekr237vw.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F81i2l07e00u2ekr237vw.jpg" width="800" height="800" alt="Portrait of Dmytro Nasyrov wearing a dark suit and light blue shirt against a dark background."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;small&gt;Dmytro Nasyrov. Photo supplied by the author.&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Written by &lt;a href="https://pharosproduction.com/dmytro-nasyrov/" rel="noopener noreferrer"&gt;Dmytro Nasyrov&lt;/a&gt; PhD, software architect with 24 years of production experience. Dmytro is the founder and CTO of Pharos Production. He works on production software architecture for FinTech, AI, Web3 and blockchain systems.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>solidity</category>
      <category>web3</category>
      <category>testing</category>
      <category>security</category>
    </item>
    <item>
      <title>Multi-Agent Architecture Adds Coordination Faster Than Capability</title>
      <dc:creator>Dmytro Nasyrov</dc:creator>
      <pubDate>Thu, 10 Sep 2026 06:46:32 +0000</pubDate>
      <link>https://dev.to/dmytronasyrov/multi-agent-architecture-adds-coordination-faster-than-capability-16b5</link>
      <guid>https://dev.to/dmytronasyrov/multi-agent-architecture-adds-coordination-faster-than-capability-16b5</guid>
      <description>&lt;p&gt;Adding a second agent creates a coordination problem before it creates a capability gain. Someone must define the assignment, preserve the relevant context, reconcile the result and decide whether another attempt is allowed. Those obligations exist even when the second agent produces nothing useful. The title describes that architectural asymmetry, not a universal measured growth rate: extra capability is possible, but it has to earn the machinery introduced to obtain it.&lt;/p&gt;

&lt;p&gt;Start with a working single-agent baseline. Split one part only when you can name the missing capability, state the boundary of the delegated task and verify the returned artifact. This article develops a coordination-budget table and a handoff contract for that decision. Its examples and numbers are hypothetical; they are not results from a benchmark or a client deployment. AI assisted the drafting and the conceptual cover.&lt;/p&gt;

&lt;h2&gt;
  
  
  Define the result before multiplying the workers
&lt;/h2&gt;

&lt;p&gt;A system becomes more capable when it completes a useful job that the baseline cannot complete reliably, or achieves an agreed outcome within a better operating constraint. More messages, more tool calls and a longer final answer do not establish that improvement. The unit of value belongs to the user: a resolved support case, an accepted code change or a decision supported by the required evidence.&lt;/p&gt;

&lt;p&gt;Consider a support workflow that proposes an account adjustment. It reads a customer's request, retrieves the applicable policy and prepares a recommendation for an authorized operator. Success means the recommendation matches the relevant account facts and policy version, contains the required evidence and leaves the adjustment untouched until the authorized action. A persuasive explanation attached to the wrong account is a failure.&lt;/p&gt;

&lt;p&gt;The baseline could be one agent with retrieval and a narrowly defined set of read-only tools. It might already have several model calls, retries and deterministic checks. Single-agent does not mean one prompt, and several calls do not automatically constitute useful specialization. Keep the baseline reasonably competent before comparing it with a more elaborate arrangement.&lt;/p&gt;

&lt;p&gt;Now identify the failure you want to remove. Does the baseline overlook a separate source collection? Does irrelevant context crowd out a decisive policy exception? Does a check require a tool that the main worker should not access? Each answer suggests a different intervention. Better retrieval, a deterministic validator or a smaller context may address the problem without introducing another autonomous decision-maker.&lt;/p&gt;

&lt;p&gt;Anthropic expressed the underlying design principle in its December 19, 2024 article:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;When building applications with LLMs, we recommend finding the simplest solution possible, and only increasing complexity when needed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Attribution: Anthropic, &lt;a href="https://www.anthropic.com/engineering/building-effective-agents" rel="noopener noreferrer"&gt;Building effective agents&lt;/a&gt;. The page now notes that its tooling landscape has changed. The quoted principle is useful here as a decision criterion, not as current framework-selection advice.&lt;/p&gt;

&lt;p&gt;Write the proposed gain as an observable change. For the support example, a policy specialist should identify the relevant exception and return the source passage that makes it applicable. Its job is not to offer another general opinion about the customer. That difference determines what the coordinator must send and what it can accept.&lt;/p&gt;

&lt;h2&gt;
  
  
  Put coordination on the same decision table as capability
&lt;/h2&gt;

&lt;p&gt;Before adding a role, fill in the row that describes its intended contribution. Each row needs both a benefit and the recurring work introduced to obtain that benefit. A blank cost cell does not mean the cost is zero; it means the architecture decision is unfinished.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Proposed split&lt;/th&gt;
&lt;th&gt;Capability to establish&lt;/th&gt;
&lt;th&gt;Coordination to budget&lt;/th&gt;
&lt;th&gt;Acceptance evidence&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Parallel source researchers&lt;/td&gt;
&lt;td&gt;Find distinct required evidence sooner&lt;/td&gt;
&lt;td&gt;Scope division, duplicate removal and source reconciliation&lt;/td&gt;
&lt;td&gt;Required facts linked to inspected sources&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Policy specialist&lt;/td&gt;
&lt;td&gt;Apply an exception missed by the baseline&lt;/td&gt;
&lt;td&gt;Versioned input, exception ownership and conflicting advice&lt;/td&gt;
&lt;td&gt;Applicable rule and supporting account facts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Separate reviewer&lt;/td&gt;
&lt;td&gt;Catch a specified class of error&lt;/td&gt;
&lt;td&gt;Review inputs, disagreement handling and revision limits&lt;/td&gt;
&lt;td&gt;Reproducible defect or passed external check&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Restricted action executor&lt;/td&gt;
&lt;td&gt;Enforce a distinct authority boundary&lt;/td&gt;
&lt;td&gt;Exact payload binding, approval expiry and outcome tracking&lt;/td&gt;
&lt;td&gt;Authorized action receipt and resulting state&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dynamic coordinator&lt;/td&gt;
&lt;td&gt;Discover subtasks that cannot be fixed beforehand&lt;/td&gt;
&lt;td&gt;Planning, worker limits, cancellation and merge ownership&lt;/td&gt;
&lt;td&gt;Accepted result within the complete run budget&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The table is a proposal template, not a ranking of architectures. A restricted executor, for example, may be ordinary application code rather than another model. Keeping the authority boundary can be useful even when the proposed agent role is removed. Likewise, fixed source partitions can use a predefined parallel workflow instead of a coordinator that reasons about staffing.&lt;/p&gt;

&lt;p&gt;For teams deciding whether orchestration complexity is justified, &lt;a href="https://pharosproduction.com" rel="noopener noreferrer"&gt;Pharos Production&lt;/a&gt; describes an AI delivery process that defines the goal, tool surface and evaluation set before production readiness. That process connects the architecture decision to an inspectable outcome. It does not prove that adding an agent will improve this hypothetical support workflow.&lt;/p&gt;

&lt;p&gt;Separate recurring run costs from the engineering work needed to maintain the arrangement. A worker may consume little inference while requiring a difficult incident procedure, a new permission boundary and another prompt to version. Conversely, an expensive research branch may be acceptable when it produces evidence that materially changes a valuable decision. Record those judgments separately rather than compressing them into an unsupported universal return estimate.&lt;/p&gt;

&lt;p&gt;Every added role should have a removal condition. If the specialist returns the same material as the baseline, if the coordinator repeatedly repairs its output or if its benefit disappears after retrieval improves, revisit the split. An architecture diagram should not become the reason a worker stays in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measure the critical path, including the merge
&lt;/h2&gt;

&lt;p&gt;Parallel workers shorten a run only when the work they perform can proceed independently and their outputs can be combined without recreating the original task. Drawing branches on a diagram does not establish either property. List the inputs each branch needs and identify which of them are produced by another branch.&lt;/p&gt;

&lt;p&gt;In the support example, account-history retrieval and policy-document retrieval can start together if both have the identifiers they need. Applying an exception usually waits for both results. Writing the final recommendation then waits for that interpretation. Those dependencies remain even if each box is labeled as a specialist agent.&lt;/p&gt;

&lt;p&gt;Use an explicit timing model for a proposed parallel section:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Elapsed time = preparation + longest required branch + merge + verification + recovery&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This is a planning decomposition, not a universal performance equation. Real stages can overlap, queue or repeat. Its purpose is to prevent the fastest worker's completion time from being presented as the speed of the full service.&lt;/p&gt;

&lt;p&gt;Suppose, hypothetically, two independent retrieval branches take eight and twelve seconds. Preparation takes three, merging takes seven and verification takes six. Ignoring queuing and retries, the complete parallel path takes twenty-eight seconds. If a competent sequential baseline completes the same accepted job in twenty-four seconds, the visible parallelism has not created a latency improvement. It may still provide better coverage, but that would be a separate claim to verify.&lt;/p&gt;

&lt;p&gt;Anthropic's &lt;a href="https://www.anthropic.com/engineering/multi-agent-research-system" rel="noopener noreferrer"&gt;multi-agent research system report&lt;/a&gt;, published June 13, 2025, describes a successful use of parallel exploration across separate context windows. It also reports substantial token consumption and explains why highly dependent work can be a poor fit. That is evidence for a conditional design opportunity, not proof that every workload should use the same topology.&lt;/p&gt;

&lt;p&gt;Treat merge ownership as an architectural responsibility. If two researchers disagree about the policy effective date, the coordinator must resolve the conflict against an authoritative source. Concatenating both answers only moves the unresolved problem into the final response. Assign a deadline and an explicit incomplete-result state so the merge cannot continue indefinitely while appearing to make progress.&lt;/p&gt;

&lt;h2&gt;
  
  
  Give every handoff an input version and a stopping condition
&lt;/h2&gt;

&lt;p&gt;A useful task description says what the worker owns, what it receives and what it must return. It also describes the result that should cause the worker to stop. Without those boundaries, a specialist can expand a narrow assignment into another complete attempt at the original job.&lt;/p&gt;

&lt;p&gt;For the support workflow, the policy worker receives a redacted account snapshot, the relevant jurisdiction or product category and a fixed policy version. It returns the applicable rule, the supporting passage and any unresolved eligibility fact. It has no authority to change the account. If the snapshot lacks a required fact, the correct output identifies that absence rather than filling it with an assumption.&lt;/p&gt;

&lt;p&gt;A compact handoff might contain these fields:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Task identifier, parent identifier and the exact decision being supported.&lt;/li&gt;
&lt;li&gt;Input artifact references, versions and the facts that may change during the run.&lt;/li&gt;
&lt;li&gt;Allowed data and tools, plus actions outside the worker's authority.&lt;/li&gt;
&lt;li&gt;Required result fields, source references and the acceptance check.&lt;/li&gt;
&lt;li&gt;Time, tool and token limits, together with the allowed revision count.&lt;/li&gt;
&lt;li&gt;Terminal outcomes: completed, insufficient evidence, invalid input or stopped by budget.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not force every failure into an empty success-shaped response. A missing policy document and an inapplicable exception are different findings. The coordinator needs that distinction to decide whether to retrieve another input, reject the request or ask a person to resolve the uncertainty.&lt;/p&gt;

&lt;p&gt;A separate context window can reduce distraction, but it can also hide the one fact that matters. Give the worker the minimum sufficient context for its assigned decision, then preserve a route to request a specific missing input. Sending the entire conversation to every worker pays for duplication and weakens the point of the split. Sending only a cheerful one-line assignment can remove the boundary conditions.&lt;/p&gt;

&lt;p&gt;When inputs change, identify which results become stale. A policy worker's conclusion for one account snapshot should not silently authorize an adjustment against a later balance. Tie the returned artifact to its input version and let the coordinator invalidate it when a relevant fact changes. This is especially important when a delayed worker returns after another part of the workflow has already completed.&lt;/p&gt;

&lt;p&gt;Validate the result at two levels. A response can contain every required field and still cite a passage that does not support its conclusion. First check the structure, identifiers and input version. Then check the relationship between the evidence and the decision. A schema violation should not reach the interpretation step, while a valid schema should not be treated as proof of factual correctness. Keep both outcomes in the task record so a later failure can be attributed to the right boundary.&lt;/p&gt;

&lt;p&gt;Keep the handoff as an artifact that an operator can inspect. A transcript may contain the necessary facts somewhere, but an incident responder should not need to reconstruct the task contract from a long exchange of acknowledgments and revisions.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7ftjoc1hmlj8g9443cdv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7ftjoc1hmlj8g9443cdv.png" alt="A versioned task flows to a bounded worker and an acceptance gate. Missing evidence, stale versions or an exhausted budget stop the task. Accepted evidence goes to a separate action owner that requires an exact approved payload and records the result." width="800" height="410"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;small&gt;AI-assisted diagram rendered from code for the hypothetical support workflow. Evidence acceptance and action authorization are separate checks.&lt;/small&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Budget accepted outcomes, not impressive activity
&lt;/h2&gt;

&lt;p&gt;A coordination budget needs a boundary around the whole run. Worker-level limits alone leave room for a coordinator to create replacements, restart failed branches or request repeated reviews. Track the parent run's total consumption and make child allocations reduce the remaining allowance.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://pharosproduction.com/services/ai-agent-development/" rel="noopener noreferrer"&gt;AI agent development process&lt;/a&gt; described by Pharos Production includes evaluation sets, shadow-mode checks, structured output validation and audit logging. Applied to this decision, those practices create places to compare accepted results and inspect failures. The service description is process evidence; the budget example below is an independent illustration.&lt;/p&gt;

&lt;p&gt;Count inference across the coordinator, workers and reviewers. Keep tool charges and human review time visible as separate quantities. A cheap output that requires substantial operator repair may have poor economics, while a costlier output that resolves a difficult case can be worthwhile. Avoid converting that judgment into currency unless the assumptions behind the conversion are available.&lt;/p&gt;

&lt;p&gt;For a hypothetical set of ten previously retained cases, suppose one architecture spends one hundred cost units and produces eight accepted results. Its observed cost per accepted result is twelve and a half units. Another spends one hundred and fifty units and produces the same eight accepted results. Its corresponding value is eighteen and three-quarter units. These invented numbers illustrate the denominator; they do not establish a performance result for either architecture.&lt;/p&gt;

&lt;p&gt;Include unsuccessful and budget-exhausted runs in the numerator. Otherwise, repeatedly abandoning difficult cases can make the surviving outputs look deceptively efficient. Record how many cases required a person to finish the job, and distinguish that result from autonomous completion under the stated acceptance contract.&lt;/p&gt;

&lt;p&gt;A hard allowance can create a difficult final decision. The coordinator may have enough budget to summarize existing evidence but not enough to reopen research. Define whether a partial answer is acceptable for that task and what must be disclosed with it. A support recommendation with a missing eligibility fact should not become actionable merely because the token counter is almost exhausted.&lt;/p&gt;

&lt;p&gt;Cancellation also needs ownership. Stop unnecessary workers when the parent task ends, prevent late results from reopening it and retain the costs already incurred. If a tool call is in flight, record its uncertain outcome until the application can establish what happened. A canceled reasoning task and a canceled external operation are not the same event.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make review independent in the way that matters
&lt;/h2&gt;

&lt;p&gt;Two agents agreeing is weak evidence when both saw the same incomplete source, used the same assumption and were rewarded for producing a tidy answer. Assigning one of them a critic persona does not create new information. The review design should specify what the reviewer can inspect that makes its judgment useful.&lt;/p&gt;

&lt;p&gt;For the support recommendation, a reviewer could check that the cited policy passage exists, that the account snapshot supports the stated eligibility conditions and that no action was attempted. Some of those checks are better implemented deterministically. Use a model for interpretation where needed, but retain direct checks for identifiers, required fields and exact payload boundaries.&lt;/p&gt;

&lt;p&gt;Decide whether the reviewer sees the proposed answer before forming its own assessment. Hiding the answer can reduce one route to agreement by imitation, but it may also duplicate expensive research. Showing it allows a targeted critique, provided the reviewer has access to the underlying evidence. Choose the arrangement for the error you are trying to detect, not because independent review sounds reassuring.&lt;/p&gt;

&lt;p&gt;Anthropic's August 13, 2026 &lt;a href="https://www.anthropic.com/research/multiagent-systems" rel="noopener noreferrer"&gt;research on emerging multiagent systems&lt;/a&gt; describes both improving coordination and persistent failures involving shared assumptions, interdependence and incomplete information exchange. Its experiments cover particular models and environments. They support inspecting those failure modes; they do not supply a universal failure rate for a production workflow.&lt;/p&gt;

&lt;p&gt;Define how disagreement ends. A reviewer should return a reproducible defect, a source conflict or a clearly bounded uncertainty. The coordinator can then repair the relevant artifact, request the missing fact or escalate the decision. Repeatedly asking another reviewer until one approves is approval shopping, even when every individual call fits within its local budget.&lt;/p&gt;

&lt;p&gt;Preserve the original objection after a correction. An operator should be able to see which input changed and why the revised result is now acceptable. If the correction cannot answer the objection, a more confident final paragraph should not close the case.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep shared state and action authority explicit
&lt;/h2&gt;

&lt;p&gt;Splitting reasoning does not require distributing write authority. In the support example, multiple workers can prepare evidence while a single controlled component owns the account adjustment. That component should receive an exact action request tied to the current account state and the relevant approval, rather than interpreting a worker's free-form suggestion as permission.&lt;/p&gt;

&lt;p&gt;An action boundary still needs concurrency control. Two branches might independently conclude that an adjustment is necessary. If each can execute it, both can be locally correct while the combined outcome is wrong. Choose one owner for the consequential transition and make duplicate requests converge on a known operation identity where the underlying system supports that behavior.&lt;/p&gt;

&lt;p&gt;A timeout is not proof that an action failed. If a worker loses the response after submitting a write, starting another worker with the same broad goal can repeat the side effect. The recovery path must first distinguish an unsubmitted request from an operation whose result is uncertain. Retrying safely depends on the external system's semantics, not on the coordinator's confidence.&lt;/p&gt;

&lt;p&gt;Keep proposed state separate from committed state. A worker can suggest a policy classification or a code patch without declaring it accepted. The coordinator should publish a new accepted artifact only after the required checks succeed. Downstream workers then consume the accepted version instead of racing against partially written shared notes.&lt;/p&gt;

&lt;p&gt;Access boundaries should follow data and action needs. A policy researcher may need the product category but not the customer's full history. A reviewer may need a redacted transaction record but no execution credential. Restricting those surfaces can justify a split even when latency stays unchanged, although the permission controls must be enforced outside the model's prose instructions.&lt;/p&gt;

&lt;p&gt;Treat returned source material as data, even when another agent selected it. A retrieved document may contain instructions addressed to its reader; those instructions do not acquire authority by passing through a specialist. The coordinator should preserve the source reference and relevant evidence without accepting an embedded demand to change tools, widen access or bypass an approval. A second model can relay an unsafe instruction as fluently as the first, so delegation does not remove the application's trust boundary.&lt;/p&gt;

&lt;p&gt;Logs should make the decision reconstructable without copying every sensitive input into every transcript. Retain artifact identifiers, relevant versions, authorization decisions and tool outcomes according to the application's data policy. The coordinator needs evidence that a check happened, not unrestricted access to every secret held by a specialist.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use a bounded decision process and keep the simpler fallback
&lt;/h2&gt;

&lt;p&gt;Begin with retained failures and existing traces. They can show whether the apparent problem is missing information, a poor tool interface or a task that genuinely benefits from separate reasoning. Do not launch a large comparison grid merely because the architecture has several possible roles. A proposal that cannot identify its target failure is not ready to consume an evaluation budget.&lt;/p&gt;

&lt;p&gt;If new evidence is necessary, predeclare one candidate split, a small relevant case set and a fixed allowance. Keep the task inputs, acceptance rule and operating limits comparable with the baseline. Record any deliberate difference, such as an additional source collection or a restricted tool surface, because that difference may explain the result more directly than the number of agents.&lt;/p&gt;

&lt;p&gt;A small comparison is a decision aid, not proof of a broad capability law. Inspect the cases individually and preserve uncertainty when the outputs are mixed. Stop when the allowance is consumed. If the result does not justify the extra coordination, keep the baseline and record what evidence would warrant revisiting the decision.&lt;/p&gt;

&lt;p&gt;During a limited rollout, retain a way to route eligible work through the simpler path. Make the operator responsible for that choice explicit, and check that reverting the routing does not abandon external operations already in progress. The useful fallback is a working service behavior, not an old diagram stored beside the new one.&lt;/p&gt;

&lt;p&gt;Which boundary in your workflow could become a separately verifiable artifact, and which would merely split a shared uncertainty between two agents?&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Keep one agent when the task shares most of its context and the proposed split adds no separately verifiable result.&lt;/li&gt;
&lt;li&gt;Split a task when independent work, a necessary capability or an enforceable permission boundary justifies the handoff and merge work.&lt;/li&gt;
&lt;li&gt;Retain a second reviewer when it detects the specified error through evidence or checks that the baseline does not already provide.&lt;/li&gt;
&lt;li&gt;Stop expanding when the accepted-outcome gain fails to justify the complete coordination budget, and preserve the simpler working route.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  More insights to read
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dmytronasyrov.medium.com/we-let-the-ai-agent-draft-the-article-we-wouldnt-let-it-publish-b9e6f1a19e46" rel="noopener noreferrer"&gt;We Let the AI Agent Draft the Article. We Wouldn’t Let It Publish.&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://huyenchip.com/2025/01/07/agents.html" rel="noopener noreferrer"&gt;Agents&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.anthropic.com/engineering/building-effective-agents" rel="noopener noreferrer"&gt;Building effective agents&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents" rel="noopener noreferrer"&gt;Effective context engineering for AI agents&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.anthropic.com/engineering/demystifying-evals-for-ai-agents" rel="noopener noreferrer"&gt;Demystifying evals for AI agents&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  About the author
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F07kzatok96bidmma7099.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F07kzatok96bidmma7099.jpg" alt="Portrait of Dmytro Nasyrov wearing a dark suit and light blue shirt against a dark background." width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;small&gt;Dmytro Nasyrov. Photo supplied by the author.&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Written by &lt;a href="https://pharosproduction.com/dmytro-nasyrov/" rel="noopener noreferrer"&gt;Dmytro Nasyrov&lt;/a&gt; PhD, software architect with 24 years of production experience. Dmytro is the founder and CTO of Pharos Production. He works on production software architecture for FinTech, AI, Web3 and blockchain systems.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>architecture</category>
      <category>programming</category>
      <category>discuss</category>
    </item>
  </channel>
</rss>
