The Redistribution contract implements a Schelling coordination game for forming consensus around the Reserve Commitment (RC) hash. This is the core incentive mechanism that rewards nodes for storing data honestly.
The contract:
- Coordinates a three-phase game (Commit, Reveal, Claim)
- Form consensus on what chunks nodes are storing
- Randomly select winners who receive the PostageStamp pot
- Penalize nodes that reveal dishonest data
- Automatically adjust prices based on participation
The game works because:
- Nodes that store data honestly will have similar reserve commitments
- This shared value becomes a "focal point" (Schelling point)
- Nodes are incentivized to reveal the true value to maximize chances of winning
- Nodes that lie can be caught and penalized
Each round consists of three consecutive phases:
-
Commit Phase (25% = 38 blocks ≈ 3 minutes)
- Nodes commit to hashed values
- Cannot be decoded until reveal
-
Reveal Phase (25% = 38 blocks ≈ 3 minutes)
- Nodes reveal their actual values
- Randomness updates after each reveal
- Only nodes in proximity to anchor can participate
-
Claim Phase (50% = 76 blocks ≈ 6 minutes)
- Truth is determined from reveals
- Winner is randomly selected from truth-tellers
- Winner verifies their reserve
- Pot is transferred to winner
Anchor: A random seed that determines which nodes are "in proximity"
Proximity: Two overlays are in proximity if their XOR is less than 2^(256-depth)
function inProximity(bytes32 A, bytes32 B, uint8 minimum) pure returns (bool) {
return uint256(A ^ B) < uint256(2 ** (256 - minimum))
}Higher depth = smaller neighborhood = more specific group
uint256 private constant ROUND_LENGTH = 152 blocks; // ~12.7 minutes at 5s/block
// Phase checks
function currentPhaseCommit() {
return block.number % ROUND_LENGTH < ROUND_LENGTH / 4;
}
function currentPhaseReveal() {
uint256 n = block.number % ROUND_LENGTH;
return n >= ROUND_LENGTH / 4 && n < ROUND_LENGTH / 2;
}
function currentPhaseClaim() {
return block.number % ROUND_LENGTH >= ROUND_LENGTH / 2;
}Commits to an obfuscated hash for the current round.
Parameters:
_obfuscatedHash: Hash of (overlay, depth, hash, nonce)_roundNumber: Round number for this commit
Requirements:
- Must be in commit phase
- Node must be staked for 2+ rounds
- Node must not have already committed
- Not in last block of commit phase (prevents front-running)
Logic:
bytes32 overlay = get from StakeRegistry
uint256 stake = get effective stake from StakeRegistry
uint8 height = get from StakeRegistry
// Check 2-round staking requirement
// Store commit with obfuscated hashCommit Structure:
struct Commit {
bytes32 overlay;
address owner;
bool revealed;
uint8 height;
uint256 stake;
bytes32 obfuscatedHash;
uint256 revealIndex;
}Checks if node is eligible for NEXT round's commit phase.
Parameters:
_owner: Node address_depth: Intended storage depth
Returns: True if node's overlay is in proximity to NEXT round's anchor
Use: Called during reveal/claim phases to check next round eligibility
Reveals the actual values used to create a commit.
Parameters:
_depth: Reported storage depth_hash: Reserve commitment hash_revealNonce: Nonce used in commit
Requirements:
- Must be in reveal phase
- Anchor must be in range of reported depth
- Commit must exist and match
Logic:
// Calculate obfuscated hash from inputs
bytes32 obfuscatedHash = wrapCommit(overlay, _depth, _hash, _revealNonce)
// Find matching commit
// Check proximity to anchor
// Store revealFirst Reveal Special Handling:
- Sets
currentRevealRoundAnchorfrom seed - Initializes reveal array
- Updates randomness
Reveal Structure:
struct Reveal {
bytes32 overlay;
address owner;
uint8 depth;
uint256 stake;
uint256 stakeDensity; // stake * 2^(depth - height)
bytes32 hash;
}Stake Density: Weighted stake based on reported depth
Higher depth → Higher density → Better chance of being selected as truth
Winner claims the pot by proving they have the chunks.
Parameters:
entryProof1: Chunk inclusion proof for random index 1entryProof2: Chunk inclusion proof for random index 2entryProofLast: Chunk inclusion proof for last index
Requirements:
- Only winner can call
- Must be in claim phase
- Must provide valid proofs
Logic:
- Select winner (if not already done)
- Calculate random chunk indices from seed
- Verify proximity for all three chunks
- Verify inclusion proofs for all three chunks
- Verify stamp proofs for all chunks
- Verify SOC proofs (if applicable)
- Check ordering of chunks
- Estimate reserve size
- Withdraw pot from PostageStamp
- Transfer to winner
Determines if caller is the winner for the current round.
Returns: True if caller's overlay matches the selected winner
Logic: Same winner selection as claim() but without doing actions
Sets the penalty multipliers.
Parameters:
_penaltyMultiplierDisagreement: Freeze duration multiplier for disagreeing_penaltyMultiplierNonRevealed: Freeze duration multiplier for not revealing_penaltyRandomFactor: Random factor for disagreement penalty (0-100)
Requirements:
- Only
DEFAULT_ADMIN_ROLEcan call
Changes the maximum value for reserve size estimation.
Parameters:
_sampleMaxValue: New maximum value
Requirements:
- Only
DEFAULT_ADMIN_ROLEcan call
Pauses or unpauses the contract.
Returns current round number: block.number / ROUND_LENGTH
Returns true if in respective phase
Checks eligibility for next round
Returns the anchor for the current phase (proximity calculation)
Checks if two overlays are within proximity
The anchor set during first reveal
Current random seed (updated after each reveal)
Verifies that a chunk is included in a Merkle tree (BMT - Binary Merkle Tree).
Structure:
struct ChunkInclusionProof {
bytes32[] proofSegments; // Merkle proof segments
bytes32 proveSegment; // Chunk data
bytes32[] proofSegments2; // Proof for transformed address
bytes32 proveSegment2; // Transformed chunk
uint64 chunkSpan; // Size of chunk span
bytes32[] proofSegments3; // Proof for transformed chunk
PostageProof postageProof; // Postage stamp proof
SOCProof[] socProof; // Single-owner chunk proof
}Verifies postage stamp validity for a chunk.
Structure:
struct PostageProof {
bytes signature; // Batch owner signature
bytes32 postageId; // Batch ID
uint64 index; // Stamp index
uint64 timeStamp; // Timestamp
}Verifies single-owner chunk ownership.
Structure:
struct SOCProof {
address signer; // Ethereum address of signer
bytes signature; // Signature
bytes32 identifier; // Content identifier
bytes32 chunkAddr; // Chunk address
}function getCurrentTruth() {
currentSum = 0
for (each revealed commit in order) {
currentSum += reveal.stakeDensity
if (random < reveal.stakeDensity / currentSum) {
truthHash = reveal.hash
truthDepth = reveal.depth
}
}
return (truthHash, truthDepth)
}The median reveal (by stake density) is selected as truth.
function winnerSelection() {
(truthHash, truthDepth) = getCurrentTruth()
currentSum = 0
redundancyCount = 0
for (each reveal matching truth) {
currentSum += reveal.stakeDensity
if (random < reveal.stakeDensity / currentSum) {
winner = reveal
}
redundancyCount++
}
adjustPrice(redundancyCount)
return winner
}A single winner is randomly selected from truth-tellers, weighted by stake density.
Nodes that commit but don't reveal are penalized:
freezeDeposit(committer, penaltyMultiplierNonRevealed * ROUND_LENGTH * 2^truthDepth)Nodes that reveal wrong truth are penalized (randomly):
if (revealed but wrong truth && random(100) < penaltyRandomFactor) {
freezeDeposit(revealer, penaltyMultiplierDisagreement * ROUND_LENGTH * 2^truthDepth)
}Penalties scale exponentially with reported depth:
- Depth 20: 1x freeze duration
- Depth 21: 2x freeze duration
- Depth 22: 4x freeze duration
- etc.
After each claim phase, the contract calls:
OracleContract.adjustPrice(uint16(redundancyCount))The redundancyCount is the number of nodes that revealed the correct truth, which becomes the input for price adjustment.
event Committed(uint256 roundNumber, bytes32 overlay, uint8 height);
event Revealed(uint256 roundNumber, bytes32 overlay, uint256 stake,
uint256 stakeDensity, bytes32 reserveCommitment, uint8 depth);
event WinnerSelected(Reveal winner);
event TruthSelected(bytes32 hash, uint8 depth);
event ChunkCount(uint256 validChunkCount);
event CurrentRevealAnchor(uint256 roundNumber, bytes32 anchor);
event PriceAdjustmentSkipped(uint16 redundancyCount);
event WithdrawFailed(address owner);constructor(
address staking,
address postageContract,
address oracleContract
)staking: StakeRegistry addresspostageContract: PostageStamp addressoracleContract: PriceOracle address
Commit Phase (152000-152037):
Block 152000: Node A commits hash_1
Block 152001: Node B commits hash_2
Block 152037: Commit phase ends
Reveal Phase (152038-152075):
Block 152038: First node reveals
→ currentRevealRoundAnchor = currentSeed()
→ updateRandomness()
Block 152039: Node B reveals
→ updateRandomness()
Block 152075: Reveal phase ends
Claim Phase (152076-152151):
Block 152076: Node A checks isWinner()
Block 152100: Winner claims pot
→ truth = getCurrentTruth()
→ winner = winnerSelection()
→ verify proofs
→ withdraw pot
→ adjustPrice()
Commit Phase (152152-152189):
- Uses anchor from seed at block 152152
- Different nodes participate (based on proximity)
For each chunk in the claim:
- Verify chunk is in proximity to anchor
- Verify chunk address matches reserve commitment hash
- Verify chunk is in transformed address tree
- Verify chunks are ordered correctly (first < second < last)
- Verify reserve size estimation
- Check batch exists and is alive
- Verify stamp index is valid for batch depth
- Verify stamp bucket matches chunk bucket
- Verify batch owner signature on chunk
- Verify signature matches signer
- Verify SOC address calculation matches chunk address
- Handle transformed addresses for SOCs
error NotCommitPhase(); // Wrong phase
error NoCommitsReceived(); // No commits in round
error AlreadyCommitted(); // Already committed this round
error MustStake2Rounds(); // Need to stake 2 rounds first
error NotStaked(); // Not staked
error NotRevealPhase(); // Wrong phase
error OutOfDepthReveal(bytes32); // Anchor out of depth
error AlreadyRevealed(); // Already revealed
error NotClaimPhase(); // Wrong phase
error AlreadyClaimed(); // Round already claimed
error SocVerificationFailed(bytes32); // SOC verification failed
error IndexOutsideSet(bytes32); // Stamp index invalid
error SigRecoveryFailed(bytes32); // Signature recovery failed
error BatchDoesNotExist(bytes32); // Batch not found
error BucketDiffers(bytes32); // Bucket mismatch
error InclusionProofFailed(uint8, bytes32); // Inclusion proof failed
error RandomElementCheckFailed(); // Chunk order wrong
error LastElementCheckFailed(); // Last element order wrong
error ReserveCheckFailed(bytes32); // Reserve size too largebytes32 overlay = StakeRegistry(stakes).overlayOfAddress(myAddress);
bytes32 hash = calculateReserveCommitment(); // From stored chunks
bytes32 nonce = randomNonce();
bytes32 obfuscatedHash = Redistribution(redis).wrapCommit(
overlay,
depth,
hash,
nonce
);
Redistribution(redis).commit(obfuscatedHash, currentRound());Redistribution(redis).reveal(
reportedDepth,
reserveCommitment,
revealNonce
);bool winner = Redistribution(redis).isWinner(overlay);
if (winner) {
// Generate proofs and claim
claim(proof1, proof2, proofLast);
}bool eligible = Redistribution(redis).isParticipatingInUpcomingRound(
myAddress,
intendedDepth
);- Random Nonce: Must be truly random and never reused
- Proximity Calculations: Proper depth responsibility
- Freeze Protection: Prevents stake manipulation during freeze
- Proof Verification: Comprehensive validation prevents fake claims
- Random Selection: Weighted fairly by stake density
- Truth Selection: Deters dishonest behavior
- StakeRegistry: Provides stake and overlay info
- PostageStamp: Source of pot, valid chunk count
- PriceOracle: Receives redundancy data for price adjustment