Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
253 changes: 253 additions & 0 deletions docs/STELLAR_MEMO_GROUP_ID.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
# Stellar Memo Group ID Feature

## Overview

This feature leverages Stellar's native memo field to store and reference group IDs during on-chain interactions. This enables lightweight group identification without requiring additional custom fields or contracts.

## Architecture

### Memo Format

- **Format**: `grp_<12-char-slug>` (17 bytes total, well within Stellar's 28-byte limit)
- **Derivation**: Deterministic derivation from room ID using the existing format `room_{timestamp}_{random9chars}`
- **Fallback**: SHA-256 hash of room ID (first 24 hex chars) for non-standard room IDs

### Database Schema

#### `rooms` table
- **`memo_group_id`**: Compact group identifier (≤28 bytes) embedded in the Stellar transaction memo field
- **Index**: Unique index on `memo_group_id` for fast lookups

#### `group_tx_memos` table
Lookup table for groupId ↔ transactionId mapping:
- **`group_id`**: References `rooms(id)` with CASCADE delete
- **`memo_group_id`**: The exact text stored in the Stellar transaction memo
- **`tx_hash`**: Stellar transaction hash that contains this memo
- **Indexes**: Fast lookups in both directions (group_id, memo_group_id, tx_hash)
- **RLS**: Public read access, authenticated insert only

## Implementation Details

### Core Components

#### 1. Memo Utilities (`lib/blockchain/memo.ts`)

```typescript
// Derive a deterministic memo from room ID
deriveMemoGroupId(roomId: string): string

// Validate memo format and constraints
validateMemoGroupId(memo: unknown): { valid: true } | { valid: false; reason: string }

// Check if memo matches expected value for a group
memoMatchesGroup(roomId: string, onChainMemo: string): boolean
```

**Validation Rules**:
- Must be a non-empty string
- Must be ≤28 bytes when encoded as UTF-8
- Must start with "grp_" prefix
- Must contain only printable ASCII characters

#### 2. Memo Middleware (`lib/blockchain/memo-middleware.ts`)

```typescript
// Validate memo from request
validateRequestMemo(memo: unknown, groupId?: string): MemoValidationResult

// Return error response for invalid memo
memoValidationError(memo: unknown, groupId?: string): NextResponse | null
```

#### 3. Stellar Service Integration (`lib/blockchain/stellar-service.ts`)

The `submitMetadataHash()` function:
1. Derives memo from group ID using `deriveMemoGroupId()`
2. Validates memo before building transaction
3. Embeds memo in Stellar transaction using `StellarSdk.Memo.text()`
4. Returns `memoGroupId` in the result for database storage

#### 4. API Endpoints

##### `GET /api/stellar/memo?memo=grp_abc123xyz`
Looks up a group by its Stellar memo identifier:
- Validates memo format
- Queries `group_tx_memos` lookup table first (fast path)
- Falls back to `rooms` table if lookup table entry missing
- Returns room record and associated transaction hash

##### `POST /api/rooms`
Creates a room and:
- Derives memo from room ID
- Submits transaction with embedded memo
- Stores `memo_group_id` in `rooms` table
- Persists mapping in `group_tx_memos` lookup table

##### `GET /api/rooms/[roomId]/verify`
Verifies room metadata including:
- Retrieves transaction from blockchain
- Extracts memo from on-chain transaction
- Validates memo matches expected value for group
- Returns verification status with memo validation result

## Acceptance Criteria

✅ **Each transaction memo is linked to a specific group ID**
- Memo is deterministically derived from room ID
- Stored in both `rooms.memo_group_id` and `group_tx_memos` table

✅ **Memo is stored and retrievable during on-chain interaction**
- Memo embedded in Stellar transaction via `StellarSdk.Memo.text()`
- Can be retrieved via `/api/stellar/memo` endpoint
- Transaction hash stored for blockchain verification

✅ **Group ID can be validated against existing records**
- `memoMatchesGroup()` validates on-chain memo against expected value
- Middleware validates memo format and cross-checks with group ID
- Verification endpoint includes memo validation status

✅ **Ensure memo usage does not conflict with other transaction metadata**
- Memo carries group ID reference only
- Metadata hash stored separately in database
- Clear separation of concerns: memo = group ID, hash = metadata integrity

## Migration

Apply the database migration:

```bash
# Using psql
psql "$DATABASE_URL" -f scripts/011_stellar_memo_group_id.sql

# Or via Supabase SQL Editor
# Run the contents of scripts/011_stellar_memo_group_id.sql
```

## Usage Examples

### Deriving a Memo

```typescript
import { deriveMemoGroupId } from "@/lib/blockchain/memo";

const roomId = "room_1714000000000_abc123xyz";
const memo = deriveMemoGroupId(roomId);
// Returns: "grp_abc123xyz" (17 bytes)
```

### Validating a Memo

```typescript
import { validateMemoGroupId } from "@/lib/blockchain/memo";

const result = validateMemoGroupId("grp_abc123xyz");
if (!result.valid) {
console.error(result.reason);
}
```

### Looking Up a Group by Memo

```bash
curl "https://your-app.com/api/stellar/memo?memo=grp_abc123xyz"
```

Response:
```json
{
"groupId": "room_1714000000000_abc123xyz",
"memoGroupId": "grp_abc123xyz",
"transactionHash": "abc123...",
"explorerUrl": "https://stellar.expert/tx/abc123...",
"room": {
"id": "room_1714000000000_abc123xyz",
"name": "My Group",
"description": "A private group",
"is_private": false,
"created_at": "2024-01-01T00:00:00Z"
}
}
```

### Verifying Memo Integrity

```typescript
import { memoMatchesGroup } from "@/lib/blockchain/memo";

const roomId = "room_1714000000000_abc123xyz";
const onChainMemo = "grp_abc123xyz";

if (memoMatchesGroup(roomId, onChainMemo)) {
console.log("Memo is valid for this group");
}
```

## Error Handling

### Missing Memo
- API returns 400 with error: "memo query parameter is required"

### Invalid Memo Format
- API returns 400 with detailed reason (e.g., exceeds byte limit, wrong prefix, non-ASCII characters)

### Memo Not Found
- API returns 404 with error: "No group found for the provided memo"

### Memo Mismatch
- Verification endpoint returns `memoVerified: false` with expected vs actual memo values

## Technical Notes

### Memo Length Limits
- Stellar TEXT memo limit: 28 bytes
- Our format: `grp_<12-char-slug>` = 17 bytes (safe margin)
- Fallback format: `grp_<24-hex-chars>` = 28 bytes (at limit)

### Deterministic Derivation
- Same room ID always produces same memo
- Enables reliable on-chain group identification
- No need to store memo separately for derivation

### Lookup Table Benefits
- Fast O(1) lookups by memo or transaction hash
- Avoids re-querying blockchain for common operations
- Enables reverse lookup (tx_hash → group_id)

### Security Considerations
- Memo is public on-chain (by design)
- Group IDs are pseudonymous, not sensitive
- RLS policies ensure authenticated inserts only
- Memo validation prevents injection attacks

## Testing

### Manual Testing

1. Create a room via POST `/api/rooms`
2. Verify `memo_group_id` is returned in response
3. Check database for `memo_group_id` in `rooms` table
4. Check database for entry in `group_tx_memos` table
5. Lookup group via GET `/api/stellar/memo?memo=grp_xxx`
6. Verify room via GET `/api/rooms/[roomId]/verify`
7. Confirm `memoVerified: true` in verification response

### API Smoke Test

```bash
# Test memo lookup with valid memo
curl "http://localhost:3000/api/stellar/memo?memo=grp_test123"

# Test memo lookup with invalid memo
curl "http://localhost:3000/api/stellar/memo?memo=invalid"

# Test memo lookup with missing parameter
curl "http://localhost:3000/api/stellar/memo"
```

## Future Enhancements

- [ ] Add memo-based group search in frontend
- [ ] Implement memo-based group discovery
- [ ] Add memo validation to all group-related endpoints
- [ ] Consider using HASH memo for additional privacy
- [ ] Add memo rotation support for group renames
3 changes: 3 additions & 0 deletions scripts/MIGRATIONS_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Included migrations (apply in numeric order):
- `006_unread_view.sql` (new)
- `007_create_group_membership.sql` (new)
- `011_enhance_invites_for_group_join.sql` (new)
- `011_stellar_memo_group_id.sql` (new)

## How to apply (psql)

Expand All @@ -30,6 +31,7 @@ psql "$DATABASE_URL" -f scripts/005_add_last_read_to_room_members.sql
psql "$DATABASE_URL" -f scripts/006_unread_view.sql
psql "$DATABASE_URL" -f scripts/007_create_group_membership.sql
psql "$DATABASE_URL" -f scripts/011_enhance_invites_for_group_join.sql
psql "$DATABASE_URL" -f scripts/011_stellar_memo_group_id.sql
```

## How to apply (Supabase)
Expand All @@ -43,6 +45,7 @@ If the project uses Supabase, maintainers can run the same `psql` commands again
- `scripts/006_unread_view.sql` creates `public.user_room_unreads` view and grants `SELECT` to `public` for convenience; adjust privileges as needed.
- `scripts/007_create_group_membership.sql` creates `public.group_membership` table for wallet-based group membership tracking.
- `scripts/011_enhance_invites_for_group_join.sql` adds `max_uses` and `use_count` columns to `public.invites`, adds RLS policies for authenticated read/update, and creates the `increment_invite_use_count` SQL function for atomic usage tracking.
- `scripts/011_stellar_memo_group_id.sql` adds `memo_group_id` column to `public.rooms` and creates `public.group_tx_memos` lookup table for groupId ↔ transactionId mapping via Stellar memo field. This enables lightweight group identification from on-chain transactions.
- A development-only endpoint (`/api/rooms/seed-test`) was added that seeds a room for an authenticated user. It requires a valid Supabase session; do not enable any service-role or unauthenticated behavior in production without review.

## Including migrations in PRs
Expand Down
Loading