Merkle-System¶
Datei:
merkle/merkleBuilder.ts(65 Zeilen) Zweck: Merkle-Tree-Aufbau für trustless Winner- und Affiliate-Claims
Übersicht¶
Das Merkle-System ermöglicht gaseffiziente Auszahlungen: Statt jede Auszahlung einzeln on-chain zu speichern, wird nur ein Merkle-Root committed. Spieler können dann mit einem Merkle-Proof ihren Gewinn beanspruchen.
Merkle Root (32 Bytes on-chain)
/ \
Hash(A+B) Hash(C+D)
/ \ / \
Leaf A Leaf B Leaf C Leaf D
(Player1) (Player2) (Player3) (Player4)
Leaf-Konstruktion¶
// merkleBuilder.ts:7-11
function makeLeaf(dayId: number, wallet: string, amount: bigint): string {
return keccak256(
abi.encode(
["uint256", "address", "uint256"],
[dayId, wallet, amount]
)
);
}
Leaf = keccak256(dayId + wallet + amount) - identisch zur Solidity-Verifikation im PrizeVault.
Tree-Aufbau¶
// merkleBuilder.ts:21-39
function buildMerkleLayersFromLeavesSorted(leavesSorted: string[]): string[][] {
const layers: string[][] = [leavesSorted];
let current = leavesSorted;
while (current.length > 1) {
const next: string[] = [];
for (let i = 0; i < current.length; i += 2) {
const left = current[i];
const right = current[i + 1] ?? current[i]; // Duplizieren wenn ungerade
next.push(hashPairSorted(left, right));
}
layers.push(next);
current = next;
}
return layers;
}
Sortierung¶
Leaves werden lexikographisch sortiert (kleinerer Hash links). Dies ist kompatibel mit OpenZeppelin's MerkleProof-Bibliothek.
function hashPairSorted(a: string, b: string): string {
return a < b
? keccak256(a + b.slice(2))
: keccak256(b + a.slice(2));
}
Proof-Generierung¶
// merkleBuilder.ts:41-53
function getProofFromLayers(layers: string[][], leafIndex: number): string[] {
const proof: string[] = [];
let idx = leafIndex;
for (let i = 0; i < layers.length - 1; i++) {
const siblingIdx = idx % 2 === 0 ? idx + 1 : idx - 1;
if (siblingIdx < layers[i].length) {
proof.push(layers[i][siblingIdx]);
}
idx = Math.floor(idx / 2);
}
return proof;
}
Zwei Merkle-Trees pro Settlement¶
1. Winners Merkle¶
- Leaves: Pro Spieler ein Leaf (aggregierte Auszahlung)
- Root: Committed on-chain via
commitMerkleSettlement() - Claim: Spieler ruft
PrizeVault.claimMerkle(dayId, amount, proof)auf
2. Affiliate Merkle¶
- Leaves: Pro Affiliate ein Leaf (aggregierte Provision)
- Root: Committed on-chain via
commitMerkleSettlement() - Claim: Affiliate ruft
AffiliateVault.claimMerkle(dayId, amount, proof)auf
Invarianten¶
// core/invariants.ts
// Winners-Root darf NIEMALS 0x0 sein
// Bei 0 Gewinnern: Dummy-Leaf einfügen
assertMerkleRootNonZero(merkleRoot);
// Affiliate-Root:
// affiliateTotal == 0 → Root MUSS 0x0 sein
// affiliateTotal > 0 → Root MUSS non-zero sein
assertAffiliateRootRule(affiliateRoot, affiliateTotal);
Warum Dummy-Leaf? Der Smart Contract prüft merkleRoot != 0x0. Ein leerer Tree hätte Root 0x0 und würde fehlschlagen.
Artefakt-Speicherung¶
// artifacts/dayId=X/winners_merkle.json
{
"merkleRoot": "0xabc123...",
"totalPayout": "15000000000",
"leaves": [
{
"index": 0,
"wallet": "0x1234...",
"amount": "5000000000",
"leaf": "0xdef456...",
"proof": ["0x...", "0x...", "0x..."]
}
]
}
// artifacts/dayId=X/affiliate_merkle.json
{
"affiliateRoot": "0x789abc...",
"affiliateTotal": "500000000",
"leaves": [
{
"index": 0,
"affiliate": "0x5678...",
"amount": "250000000",
"leaf": "0xghi789...",
"proof": ["0x...", "0x..."]
}
]
}
Gasvergleich¶
| Methode | Gas pro Auszahlung | Gas für 100 Gewinner |
|---|---|---|
| Einzelne Transfers | ~50.000 | 5.000.000 |
| Merkle Claims | ~80.000 pro Claim | 80.000 (1x Commit) |
Vorteil: Settlement-Commit ist O(1), Claim-Kosten werden auf Spieler verteilt.
Datenbank-Speicherung¶
settlement_proofs (
id SERIAL PRIMARY KEY,
settlement_run_id INT REFERENCES settlement_runs(id),
proof_type VARCHAR, -- 'winners_merkle' / 'affiliate_merkle'
proof_json JSONB, -- Root, Leaves, Proofs
checksum VARCHAR -- SHA256
)