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

153 lines
7.3 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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_TIMESTAMP``ADD_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_B``FIN_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 `>= cutoffTimestamp``FIN_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_B``PAY_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_B``FIN_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:
```json
{
"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)
```text
codeHash = Base58( SHA-256( codeBytes ) )
```
---
## 10) Appendix — Validation Artifacts
- v1 `codeBytes.length` = 167; v2 `codeBytes.length` = 104.