Zum Inhalt

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
)

Integration mit Smart Contracts

BO-Engine                          Blockchain
────────                          ──────────
buildMerkle(winners)      ────►   merkleRoot stored in SettlementV5
Spieler-App               ────►   PrizeVault.claimMerkle(
  (holt Proof aus DB)                dayId, amount, proof
                                  )
                                  MerkleProof.verify(
                                    proof, merkleRoot, leaf
                                  )
                                  ✅ USDT Transfer