forked from Qortal/q-fund-v2
153 lines
7.3 KiB
Markdown
153 lines
7.3 KiB
Markdown
# 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 big‑endian 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 app’s 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).
|
||
|
||
### Variant‑specific 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 32‑byte 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, 32‑byte 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.
|