agro.yaya.cash › Protocol

Data model & spec v0.1 draft

Wire formats. Unstable — every structure on this page may change.
Draft

This is a design sketch, not an implemented specification. Field names, canonicalisation and the token-family construction are all provisional. Do not build against it expecting stability.

1. Key hierarchy

All keys are EdDSA (Ed25519), matching Taler's signature scheme.

Registro de Atestadores          // signed mirror of SENASA's public registry
  ├─ issuer key: sgp-arequipa             // SGP council
  ├─ issuer key: oc-controlunion-pe        // registered certification body
  ├─ issuer key: senasa-bpa                // state, for Sello BPA
  └─ …
        ├─ attestation → subject_pub          // the producer's key
        ├─ token issue keys (per family, per season, per denomination)
        └─ revocation signing key

Producer
  └─ merchant instance key = subject_pub    // signs contract terms

Distributor / retailer
  └─ distributor key                       // countersigns, never re-signs

The root is deliberately not ours. It is a signed mirror of a list the Peruvian state already publishes[1]. We do not decide who may certify; we transcribe a decision SENASA already made and sign the transcription so it can travel offline. If SENASA ever publishes the registry in a signed machine-readable form, our mirror should be deleted, not kept.

2. Attestation object

FieldTypeReq.Definition
vintegeryesFormat version. 1.
standardstringyes Registered standard identifier, e.g. pe.rtpo.organico.sgp. See mapping table.
issuerstringyes Slug of the issuing body as it appears in the registry mirror.
issuer_pubEdDSA pubyes Key used for issuer_sig. Must match the registry entry.
subject_pubEdDSA pubyes The producer's merchant-instance key. The binding that makes the claim non-transferable.
subject_labelstringyes Human-readable producer or association name, for display only. Never trusted for verification.
scope.crops[]string[]yes Crop identifiers the attestation covers. ["*"] permitted for whole-operation certification.
scope.parcels[]string[]no Parcel or cadastral references. Omitted where publishing them would expose a producer to risk.
scope.marketenumyes domestic | export | any. An SGP attestation is domestic, per Ley 30983.
scope.regionstringno INEI region/province code, for display and for regional-market rules.
valid_fromdateyesISO 8601.
valid_untildateyes ISO 8601. Short windows are preferred over long ones plus revocation.
families[]string[]no Token family slugs opened under this attestation. Absent for attestations that carry no quantity ceiling (e.g. BPA).
audit_refstringno Opaque reference to the inspection or council minute. Resolvable by the issuer, meaningless to anyone else.
issuer_sigEdDSA sigyes Signature over the canonical form of all other fields.

Canonicalisation

Signing is over a deterministic serialisation: keys sorted lexicographically, no insignificant whitespace, UTF-8, no floating-point values anywhere. We intend to follow whatever Taler already uses for contract-term hashing rather than introducing a second canonicalisation — two canonical forms in one system is a bug generator. Confirming that is a task, not a decision we have made.

3. Token family slug grammar

<standard> "/" <crop> "/" <season> "/" <issuer>

pe.rtpo.organico.sgp/papa-nativa/2026-A/sgp-arequipa
pe.senasa.bpa/palta-hass/2026/senasa-bpa
pe.libre-pesticidas/lechuga/2026-W31/sgp-lima-norte

Slugs are opaque to Taler, which only uses them as map keys in token_families.[2] We impose structure so that a wallet can cross-check a family against an attestation without a lookup table, and so that a human reading a log can tell what a family is. Rules:

  • Lowercase, ASCII, hyphen-separated segments; / separates the four components; no empty components.
  • season is a year, a year plus campaign letter (2026-A), or a year plus ISO week for short-cycle crops (2026-W31).
  • A family is append-only within a season: supplementary issuance adds to the same slug rather than creating a parallel one, so the ceiling stays legible as a single number.
  • One unit = one kilogram unless the family metadata declares otherwise. Crate- and bunch-denominated families are permitted and must say so in the description.

4. The contract extra block

Taler's contract terms carry an extra field for merchant-defined data that needs to live on the contract without separate storage.[3] Because it is inside the signed and hashed contract, anything we put there is covered by merchant_sig for free.

{
  "order_id": "AQP-2026-A-00417",
  "summary":  "Papa nativa 3 kg — orgánico certificado",
  "summary_i18n": {
    "es": "Papa nativa 3 kg — orgánico certificado (SGP Arequipa)",
    "qu": "Papa 3 kg — allin puquy"
  },
  "merchant_pub": "J7QK…",          // = attestation.subject_pub

  "choices": [{
    "amount": "PEN:25.00",
    "inputs": [{
      "type": "token",
      "token_family_slug": "pe.rtpo.organico.sgp/papa-nativa/2026-A/sgp-arequipa",
      "number": 3                    // 3 kg burned from the season ceiling
    }],
    "outputs": []
  }],

  "extra": {
    "agro_v": 1,
    "attestations": [ /* attestation objects, §2 */ ],
    "lot": {
      "crop": "papa-nativa",
      "variety": "huayro",
      "net_kg": 3.0,
      "harvest_date": "2026-07-11",
      "parcel": "UC-04812-A"
    },
    "split": [
      { "role": "producer",    "label": "Chacra Alta",      "amount": "PEN:18.60" },
      { "role": "transport",   "label": "Corredor Sur",    "amount": "PEN:2.10"  },
      { "role": "distributor", "label": "Mercado #4 p.112", "amount": "PEN:4.30"  }
    ],
    "countersigs": [ /* §5 */ ]
  }
}
Contract terms for one certified sale, abbreviated. The inputs entry is what burns the kilo-tokens; extra carries everything a wallet needs to explain the claim to a human.

Invariants a verifier must enforce

  1. extra.attestations[*].subject_pub == merchant_pub.
  2. extra.lot.cropscope.crops (or scope is ["*"]).
  3. Every token input's family slug is listed in the matching attestation's families[], and its standard and issuer components agree with that attestation.
  4. input.number == ceil(extra.lot.net_kg) for kilogram-denominated families. Rounding is up: the system may burn more certified quantity than sold, never less.
  5. Σ extra.split[*].amount == choices[i].amount, same currency throughout.
  6. Contract timestamp ∈ [valid_from, valid_until].

5. Distributor countersignature

A distributor must be able to add its margin and its identity without touching what the producer signed. The countersignature covers the producer's contract hash plus the distributor's own additions:

{
  "role":            "distributor",
  "pub":             "R2MW…",
  "label":           "Mercado #4, puesto 112",
  "h_producer_terms": "…SHA-512 of the producer's contract terms…",
  "adds":            [{ "amount": "PEN:4.30", "reason": "comision-puesto" }],
  "sig":             "…EdDSA…"
}

Because h_producer_terms is a hash of the producer's already-signed object, a distributor can append but never rewrite. Altering the farmgate figure breaks the hash, which breaks the attestation chain, which removes the certified claim — the exact thing the distributor is selling. The incentive and the cryptography point the same way, which is the only kind of rule that holds up in a market.

Multiple countersignatures chain: transport countersigns the producer, the stall countersigns transport. Each link is verifiable independently.

6. Verification algorithm

verify(contract, registry_mirror, now) → Result

 1. h ← SHA-512(canonical(contract))
 2. require verify_eddsa(contract.merchant_sig, h, contract.merchant_pub)
 3. att ← contract.extra.attestations
    require att non-empty and att[*].v == 1
 4. for each a in att:
      require verify_eddsa(a.issuer_sig, canonical(a minus sig), a.issuer_pub)
      require a.subject_pub == contract.merchant_pub
      entry ← registry_mirror.lookup(a.issuer)
      require entry exists and entry.pub == a.issuer_pub
      require entry.status == ACTIVE                // see §7
      require a.valid_from ≤ now ≤ a.valid_until
      require contract.extra.lot.crop in a.scope.crops
 5. for each input of type "token" in the selected choice:
      require slug in (union of a.families for matching a)
      require input.number ≥ ceil(lot.net_kg)
 6. require sum(extra.split[*].amount) == choice.amount
 7. for each cs in extra.countersigs:
      require verify_eddsa(cs.sig, canonical(cs minus sig), cs.pub)
      require cs.h_producer_terms == h
 8. return Verified {
      standard, issuer_label, scope, valid_until,
      producer_share = split[role=producer] / total,
      mirror_age = now - registry_mirror.fetched_at   // surface this
    }

Steps 1–7 are pure local computation. mirror_age is returned rather than hidden because a wallet whose registry is four months stale should say so, and a UI that renders a confident tick over stale trust data is worse than one that shows nothing.

7. Revocation

Three independent mechanisms, in order of how much we rely on them:

  1. Short validity windows. Attestations expire at the end of a season. This is the primary mechanism because it needs no infrastructure and fails closed.
  2. Token exhaustion. A ceiling reached is a claim ended, with no revocation step at all.
  3. Explicit revocation list, signed by the issuer and by the registry mirror, fetched opportunistically and cached. Necessary for the case that matters most — a certifier suspended by SENASA mid-season — and weakest exactly there, because it depends on the wallet having refreshed.

We deliberately do not require an online status check per sale. An OCSP-style round trip would leak purchase patterns to the certifier and would fail in the markets we care about. The cost is a revocation window bounded by mirror staleness; the mitigation is short validity and honest display of mirror_age.

8. Key custody open

The hardest unsolved problem in the design, and the one most likely to quietly undo it.

The entire premise is that the producer holds the signing key. In practice that key lives on a phone that may be shared, sold, lost, or held by whichever family member is most comfortable with technology. Constraints:

  • Losing a key must not lose the certification. Rotation has to be a routine, cheap operation at the issuer — a re-attestation to a new subject_pub, not a re-audit.
  • Custody by an aggregator recreates the problem. A cooperative that holds members' keys "for convenience" is the retailer relationship in new clothing. Threshold or co-signature schemes are a possible answer, and they add operational complexity to a system that already asks a lot of its users.
  • Shared phones are the norm, not the exception. Designing for one-person-one-device is designing for someone else's countryside.

No decision. Tracked as OQ-5.

9. Privacy properties

PartyLearnsDoes not learn
Certifier / SGP council How much of its issued quota was consumed, over time Which retailer sold it, which shopper bought it, at what price — blind signatures make the tokens unlinkable to issuance
ProducerIts own salesShopper identity
DistributorIts own sales and the producer's declared farmgate price Shopper identity
ExchangeThat coins and tokens were spent; settlement to merchants Which withdrawal a coin came from — payer anonymity is Taler's core property
Shopper's walletEverything on the contract — and it reports none of it anywhere
agro.yaya.cash operators Nothing. We publish a registry mirror and a spec. We are not in the transaction path and must never be Everything
Design rule: no transaction data product

A dataset of who buys certified produce, where, at what premium, is commercially valuable and would compromise the reason for the design. The protocol is built so that no party is in a position to assemble it — not because we promise not to, but because the blind signatures make it unavailable. Any future proposal to aggregate and sell transaction data has to answer to Ley 29733 on personal data protection and, before that, to this paragraph.

See also: Protocol overview · GNU Taler primer · Open questions

References

  1. SENASA registry of certification bodies and SGPs. gob.pe
  2. GNU Taler, DD 46: Contract Format v1token_families, inputs and outputs. docs.taler.net
  3. GNU Taler, Merchant Backend RESTful APIextra, h_contract_terms, merchant_sig. docs.taler.net