Cover photo

ブロック報酬の分配ルール

報酬配分の概要

当選者には、応募したポストのインプレッションに応じたブロック報酬が与えられます。

正確には、以下の計算式によってポイントを計算し、ブロック報酬をそのポイントで按分して配分します。

Point={0ifimp<50, impif50imp<1,000,000 1000ifimp1,000,000  imp:応募したポストのインプレッション\begin{gather*} Point = \begin{cases} 0 & \text{if} \quad imp < 50, \ \sqrt{imp} & \text{if} \quad 50 \leq imp < 1,000,000 \ 1000 & \text{if} \quad imp \geq 1,000,000 \end{cases}\ \quad\ \quad\quad\quad{\small imp : {\scriptsize\text{応募したポストのインプレッション}}} \end{gather*}

まず、大量のBotアカウントによるマイニングを除くために、50未満のインプレッションについてはポイント0とします。

つぎに、インフルエンサーへの過度な集中を防止し、分散性を高めるためにインプレッションの平方根をポイントとします。つまり、インプレッションが100倍になっても、もらえる報酬は10倍にしかなりません。

post image

さらに、100万を超えるとポイントは1000で固定されます。これにより例えば、一般的なアカウントのインプレッションが100だとするとそのPointは10ですので、最大でも一般的なポストの100倍程度の報酬しか生じません。これによって一つのバズったポストにブロック報酬のほとんどを取られるというような事態を防止します。

post image

補足

  • 認証ユーザーと非認証ユーザーの当選者でポイント計算に違いはありません。

  • 按分計算は1satoshi以下は切り捨てられます。それによって生じた余りは一番インプレッションが大きい当選者に配分されます。(もしインプレッションが同数で並んだ場合は認証ユーザーで応募が早い当選者が優先されます。)

プログラムコード

/**
 * impressionからポイントを計算する関数
 * @param {number} impression - インプレッション数
 * @return {number} - 計算されたポイント
 */
function calculatePoints(impression) {
  // 50以下の場合は0ポイント
  if (impression <= 50) {
    return 0;
  }
  
  // 100万以上の場合は100万として計算
  const cappedImpression = Math.min(impression, 1000000);
  
  // インプレッションの平方根をポイントとする
  return Math.sqrt(cappedImpression);
}

/**
 * 当選者リストにポイントを追加する関数
 * @param {Array<Object>} winnersList - 当選者リスト
 * @return {Array<Object>} - ポイントが追加された当選者リスト
 */
function addPointsToWinners(winnersList) {
  return winnersList.map(winner => {
    // 既存のオブジェクトを変更せず、新しいオブジェクトを作成する
    return {
      ...winner,
      point: calculatePoints(winner.impression)
    };
  });
}

// Node.jsの場合のエクスポート
if (typeof module !== 'undefined' && module.exports) {
  module.exports = {
    calculatePoints,
    addPointsToWinners
  };
}

/**
 * Bitcoinu報酬を当選者にポイントに比例して分配する関数
 * @param {Array<Object>} winnersWithPoints - ポイントが追加された当選者リスト
 * @param {number} blockReward - 分配する報酬総額(satoshi単位)
 * @return {Array<Object>} - 報酬が追加された当選者リスト
 */
function distributeReward(winnersWithPoints, blockReward) {
  // 総ポイント数を計算
  const totalPoints = winnersWithPoints.reduce((sum, winner) => sum + winner.point, 0);
  
  // 総ポイントがゼロの場合は報酬を分配しない
  if (totalPoints === 0) {
    return winnersWithPoints.map(winner => ({
      ...winner,
      reward: 0
    }));
  }
  
  // 初期配分を計算(切り捨て)
  const winnersWithReward = winnersWithPoints.map(winner => {
    const rewardAmount = Math.floor((winner.point / totalPoints) * blockReward);
    return {
      ...winner,
      reward: rewardAmount
    };
  });
  
  // 初期配分の合計を計算
  const distributedReward = winnersWithReward.reduce((sum, winner) => sum + winner.reward, 0);
  
  // 残りの報酬(未分配の端数)
  const remainingReward = blockReward - distributedReward;
  
  // 残りの報酬がある場合、最大インプレッションの当選者に付与
  if (remainingReward > 0) {
    // 最大インプレッションを持つ当選者のインデックスを見つける
    let maxImpressionIndex = 0;
    let maxImpression = winnersWithReward[0].impression;
    
    for (let i = 1; i < winnersWithReward.length; i++) {
      if (winnersWithReward[i].impression > maxImpression) {
        maxImpression = winnersWithReward[i].impression;
        maxImpressionIndex = i;
      }
    }
    
    // 最大インプレッションの当選者に残りの報酬を全て付与
    winnersWithReward[maxImpressionIndex].reward += remainingReward;
  }
  
  return winnersWithReward;
}

/**
 * 認証済みユーザーと未認証ユーザーの当選者リストをマージして報酬を分配する関数
 * @param {Array<Object>} verifiedWinners - 認証済みユーザーの当選者リスト
 * @param {Array<Object>} unverifiedWinners - 未認証ユーザーの当選者リスト
 * @param {number} blockReward - 分配する報酬総額(satoshi単位)
 * @return {Array<Object>} - 報酬が分配された当選者リスト
 */
function mergeAndDistributeReward(verifiedWinners, unverifiedWinners, blockReward) {
  // 両方のリストをマージ
  const allWinners = [...verifiedWinners, ...unverifiedWinners];
  
  // ポイントを計算
  const winnersWithPoints = addPointsToWinners(allWinners);
  
  // 報酬を分配
  const winnersWithReward = distributeReward(winnersWithPoints, blockReward);
  
  return winnersWithReward;
}

// Node.jsの場合のエクスポート
if (typeof module !== 'undefined' && module.exports) {
  module.exports = {
    calculatePoints,
    addPointsToWinners,
    distributeReward,
    mergeAndDistributeReward
  };
}

// Cloudflare Workersでも利用できるようにグローバルスコープにエクスポート
if (typeof self !== 'undefined') {
  self.calculatePoints = calculatePoints;
  self.addPointsToWinners = addPointsToWinners;
  self.distributeReward = distributeReward;
  self.mergeAndDistributeReward = mergeAndDistributeReward;
}