Files
q-fund-v2/docs/qfund_v1_v2_spec.md
2025-08-23 19:47:40 -04:00

7.3 KiB
Raw Permalink Blame History

Q-Fund Crowdfund ATs — Technical Specification (v1 refund, v2 non-refund)

Scope: This document specifies two Qortal Automated Transactions (ATs) that implement a crowdfund with two settlement behaviors: v1 (refund-if-goal-missed) and v2 (pay-awardee-even-if-missed). It describes their immutable code bytes, codeHash calculation, data bytes ABI, state machine, creation & deploy flow, and verification rules so future developers can reproduce identical ATs (same codeHash) and build compatible apps.


1) Invariants & Golden Rules

  • Code identity = codeHash = Base58(SHA-256(codeBytes)). Never include user parameters in codeBytes.
  • ABI uses bigendian u64 unless noted. Address bytes are 32 bytes.
  • Creation bytes = header + codeBytes + dataBytes. Only dataBytes vary per deployment.
  • Determinism: avoid compiling time-dependent constants into codeBytes; compute cutoffs from GET_BLOCK_TIMESTAMP at runtime.

2) Code Hashes (Pinned)

  • v1 (refund)
    • codeHash = 9gS2L74FdaG3zuEeYv815xVyHkhvLguq7ZGD6pf24i8F
    • codeBytes.length = 167 bytes
  • v2 (pay-anyway)
    • codeHash = HaqJBVVr9gZqgARZ5UZd7EU9ybyvVK2fCo9sx3gMMFsr
    • codeBytes.length = 104 bytes

These hashes were recomputed by Base58(SHA-256(codeBytes)) from deployed creation bytes and from the apps embedded Base64 (v2).


2a) Premade codeBytes (reference)

These are the canonical, premade codeBytes for each variant. They are immutable; apps should embed these exact bytes (Base64) to reproduce the same codeHash.

v1 — refund-if-goal-missed

  • codeHash: 9gS2L74FdaG3zuEeYv815xVyHkhvLguq7ZGD6pf24i8F
  • codeBytes.length: 167
  • SHA-256(codeBytes) hex: 80f770c2b6e6475208da0e9daa6b632b1b18293a606c88a213142929e2e7e9f0

Base64 (exact):

NQMBAAAABTUDAAAAAAI3BAYAAAACAAAAAgAAAAACAAAAAwAAAAJLAAAAAwAAAAAAAAAgJQAAAAM1BAAAAAAEIAAAAAQAAAABGTgBHwAAAAAAAAAKMgQDKDAzAwQAAAAFNQElAAAABhsAAAAGByg1AwcAAAAFIAAAAAUAAAACCyg1AwUAAAAHJAAAAAcAAAAI0jUDBgAAAAkyAwozBAIAAAAJGgAAAFk=

v2 — pay-awardee-even-if-missed

  • codeHash: HaqJBVVr9gZqgARZ5UZd7EU9ybyvVK2fCo9sx3gMMFsr
  • codeBytes.length: 104
  • SHA-256(codeBytes) hex: f665c427e2a59187aa4ee1c6325c226cd57814b02d007071439fb103f0badf3d

Base64 (exact):

NQMBAAAABTUDAAAAAAI3BAYAAAACAAAAAgAAAAACAAAAAwAAAAJLAAAAAwAAAAAAAAAgJQAAAAM1BAAAAAAEIAAAAAQAAAABGTgBHwAAAAAAAAAGMgQDKDA4AR8AAAAAAAAABjIEAyg=

3) Data Bytes ABI (public interface)

Common fields

  • [0..7] (u64)sleepMinutes/blocks window. Number of minutes/blocks until the campaign decision point. Stored as u64 (big-endian). Used to compute a timestamp cutoff and a sleep-until-height.
  • [8..15] (u64)goalAmountAtoms. Goal amount in QORT atoms (QORT×1e8).

Variantspecific fields

  • v1 (refund) — extended ABI to support refund scanning

    • [16..23] (u64)lastTxnTimestamp (internal; initialized 0). Tracks scan cursor for inbound tx iteration.
    • [24..31] (u64)cutoffTimestamp (internal; computed).
    • [32..39] (u64)finalAmount (internal; computed at decision time).
    • [40..47] (u64)tmpAmount (scratch).
    • [48..55] (u64)tmpType (scratch; holds tx type).
    • [56..63] (u64)tmpHeight (scratch; holds height from timestamp).
    • [64..71] (u64)paymentTypeId (= PAYMENT = 2). Constant used to filter only payment transactions.
    • [72..79] (u64)reserved.
    • [80..111] (32 bytes)awardeeAddress.
  • v2 (pay-anyway) — minimal ABI

    • [16..47] (4×u64) — reserved/scratch (implementation-specific).
    • [48..79] (32 bytes)awardeeAddress.

Note: All unspecified fields are zero-initialized. The layouts above reflect the offsets used by the app (v2) and by the v1 Java program. Keep exact sizes to preserve codeHash parity across builds.


4) State Machine (high level)

v1 — Refund if goal missed

  1. Init: Read sleepMinutes & goalAtoms; awardee set in dataBytes.
  2. Compute cutoff: GET_BLOCK_TIMESTAMPADD_MINUTES_TO_TIMESTAMP(sleepMinutes) → store cutoffTimestamp.
  3. Sleep: Convert cutoff timestamp to height; SLP_DAT(height) to pause until decision block.
  4. Decision:
    • GET_CURRENT_BALANCE → store finalAmount.
    • If finalAmount >= goalAtoms: SET_B_DAT(awardee)PAY_ALL_TO_ADDRESS_IN_BFIN_IMD.
    • Else: enter Refund Loop.
  5. Refund Loop:
    • PUT_TX_AFTER_TIMESTAMP_INTO_A(lastTxnTimestamp); if A is zero → FIN_IMD.
    • GET_TIMESTAMP_FROM_TX_IN_A → update lastTxnTimestamp; if >= cutoffTimestampFIN_IMD.
    • GET_TYPE_FROM_TX_IN_A → if != paymentTypeId → loop (skip non-payments).
    • GET_AMOUNT_FROM_TX_IN_A & PUT_ADDRESS_FROM_TX_IN_A_INTO_BPAY_TO_ADDRESS_IN_B(amount) → loop.

v2 — Pay awardee even if goal missed

  1. Init: Read sleepMinutes & goalAtoms; awardee set in dataBytes.
  2. Compute cutoff as in v1; Sleep until decision block.
  3. Decision: Regardless of whether GET_CURRENT_BALANCE >= goalAtoms, pay all: SET_B_DAT(awardee)PAY_ALL_TO_ADDRESS_IN_BFIN_IMD.

5) Build → Create → Deploy

  1. Build dataBytes (big-endian):
    • Write u64 at offset 0: sleepMinutes (or block window).
    • Write u64 at offset 8: goalAtoms.
    • Write 32byte awardee at offset v1: 80 / v2: 48.
    • (v1 only) Write u64 at offset 64: paymentTypeId = 2.
  2. Use fixed codeBytesBase64 per variant (see §2).
  3. POST /at/create with:
    {
      "ciyamAtVersion": 2,
      "codeBytesBase64": "...",
      "dataBytesBase64": "...",
      "numCallStackPages": 0,
      "numUserStackPages": 0,
      "minActivationAmount": 0
    }
    
  4. Verify the response by decoding creationBytes and re-hashing codeBytes.
  5. Deploy via Hub: qortalRequest({ action: 'DEPLOY_AT', creationBytes, name, description, tags:'q-fund', amount:0.2, assetId:0, type:'crowdfund' }).

6) Creation Bytes Decoder (sanity tool)

Given creationBytes (Base58):

  • Decode → parse header → extract codeLen → slice codeBytes → slice dataBytes.
  • Compute codeHash = Base58(SHA-256(codeBytes)).
  • Assert expected lengths and non-absurd values (e.g., data ≥ 112 for v1; ≥ 80 for v2).

7) Parity Tests (Definition of Done)

  • Code parity: The provided codeBytesBase64 must hash to the pinned codeHash for each variant.
  • Creation parity: Changing only dataBytes must not alter codeHash.
  • Behavior parity:
    • v1: when goal is missed, donors are refunded; when met, awardee receives all.
    • v2: awardee receives all regardless of goal.
  • ABI parity: Offsets and sizes match §3 (BE u64, 32byte address).

8) Known Offsets & Helpers (app reference)

  • v2 (repo) uses
    • setLongValue(data, 0, blocks)
    • setLongValue(data, 8, goalAtoms)
    • replaceArraySlice(data, 48, bs58.decode(awardee))
  • v1 must use
    • setLongValue(data, 0, blocks)
    • setLongValue(data, 8, goalAtoms)
    • setLongValue(data, 64, 2) // PAYMENT constant
    • replaceArraySlice(data, 80, bs58.decode(awardee))

9) Hash Recipe (exact)

codeHash = Base58( SHA-256( codeBytes ) )

10) Appendix — Validation Artifacts

  • v1 codeBytes.length = 167; v2 codeBytes.length = 104.