Skip to content

Commit 6010e67

Browse files
committed
feat: wire alignment scoring into training reward calculation
Coordinators now compute charter↔constitution alignment (via AlignmentPricing) alongside correctness when verifying solutions. Both scores flow through Yuma consensus, and solver rewards are scaled by correctness × alignment. Changes: - AutonetLib: add alignmentScore to CoordinatorVote, consensusAlignmentScore to YumaConsensusResult, charterCid to SolverSubmission - ResultsRewards: submitVote accepts alignment score, _processRewardsYuma applies alignment factor to solver reward calculation - TaskContract: commitSolution accepts charterCid so coordinators can fetch solver charter text from blob store - Coordinator node: computes alignment after correctness verification, submits both scores in vote - Solver node: uploads charter to blob store and commits CID with solution - Python contract bindings: updated for new parameters - Tests: updated all submitVote/commitSolution calls
1 parent 995c376 commit 6010e67

8 files changed

Lines changed: 195 additions & 50 deletions

File tree

contracts/core/ResultsRewards.sol

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ contract ResultsRewards {
129129
address _solver,
130130
bool _isCorrect,
131131
uint256 _score,
132+
uint256 _alignmentScore,
132133
string calldata _reportCid
133134
) external onlyStakedCoordinator {
134135
AutonetLib.TaskProposal memory proposal = taskContract.getTaskProposal(_taskId);
@@ -159,6 +160,7 @@ contract ResultsRewards {
159160
coordinator: msg.sender,
160161
isCorrect: _isCorrect,
161162
score: _score,
163+
alignmentScore: _alignmentScore,
162164
stake: stakeInfo.amount,
163165
voteBlock: block.number,
164166
reportHash: keccak256(abi.encodePacked(_reportCid))
@@ -187,14 +189,15 @@ contract ResultsRewards {
187189
require(taskVotes.length >= MIN_COORDINATORS, "Insufficient coordinator votes");
188190

189191
// Compute Yuma consensus
190-
(bool consensusCorrect, uint256 consensusScore, uint256 totalStake, uint256 correctStake, uint256 clippedCount) =
192+
(bool consensusCorrect, uint256 consensusScore, uint256 consensusAlignmentScore, uint256 totalStake, uint256 correctStake, uint256 clippedCount) =
191193
_computeYumaConsensus(_taskId, taskVotes);
192194

193195
// Store result
194196
result.taskId = _taskId;
195197
result.solver = _solver;
196198
result.consensusCorrect = consensusCorrect;
197199
result.consensusScore = consensusScore;
200+
result.consensusAlignmentScore = consensusAlignmentScore;
198201
result.totalStake = totalStake;
199202
result.correctStake = correctStake;
200203
result.clippedVotes = clippedCount;
@@ -212,35 +215,40 @@ contract ResultsRewards {
212215
emit YumaConsensusReached(_taskId, _solver, consensusCorrect, consensusScore);
213216

214217
// Process rewards
215-
_processRewardsYuma(_taskId, _solver, taskVotes, consensusCorrect, consensusScore);
218+
_processRewardsYuma(_taskId, _solver, taskVotes, consensusCorrect, consensusScore, consensusAlignmentScore);
216219
}
217220

218221
/**
219222
* @dev Compute Yuma consensus with stake-weighted voting and clipping.
220223
*/
221224
function _computeYumaConsensus(uint256 _taskId, AutonetLib.CoordinatorVote[] storage taskVotes)
222225
internal
223-
returns (bool consensusCorrect, uint256 consensusScore, uint256 totalStake, uint256 correctStake, uint256 clippedCount)
226+
returns (bool consensusCorrect, uint256 consensusScore, uint256 consensusAlignmentScore, uint256 totalStake, uint256 correctStake, uint256 clippedCount)
224227
{
225228
uint256 numVotes = taskVotes.length;
226229

227-
// Calculate total stake and stake-weighted score
230+
// Calculate total stake, stake-weighted correctness score, and alignment score
228231
uint256 weightedScoreSum = 0;
232+
uint256 weightedAlignmentSum = 0;
229233
for (uint256 i = 0; i < numVotes; i++) {
230234
totalStake += taskVotes[i].stake;
231235
if (taskVotes[i].isCorrect) {
232236
correctStake += taskVotes[i].stake;
233237
}
234238
weightedScoreSum += taskVotes[i].score * taskVotes[i].stake;
239+
weightedAlignmentSum += taskVotes[i].alignmentScore * taskVotes[i].stake;
235240
}
236241

237242
// Consensus correct if majority stake agrees
238243
consensusCorrect = correctStake > totalStake / 2;
239244

240-
// Calculate stake-weighted average score (before clipping)
245+
// Stake-weighted average alignment score (no clipping — alignment is subjective)
246+
consensusAlignmentScore = totalStake > 0 ? weightedAlignmentSum / totalStake : 0;
247+
248+
// Calculate stake-weighted average correctness score (before clipping)
241249
uint256 avgScore = totalStake > 0 ? weightedScoreSum / totalStake : 0;
242250

243-
// Apply clipping: scores that deviate too much from average are clipped
251+
// Apply clipping: correctness scores that deviate too much from average are clipped
244252
uint256 clippedWeightedSum = 0;
245253
uint256 clippedTotalStake = 0;
246254

@@ -306,7 +314,8 @@ contract ResultsRewards {
306314
address _solver,
307315
AutonetLib.CoordinatorVote[] storage taskVotes,
308316
bool _isCorrect,
309-
uint256 _consensusScore
317+
uint256 _consensusScore,
318+
uint256 _consensusAlignmentScore
310319
) internal {
311320
require(!rewardsProcessed[_taskId], "Already processed");
312321

@@ -340,8 +349,11 @@ contract ResultsRewards {
340349
}
341350

342351
if (_isCorrect) {
343-
// Solver reward scaled by consensus score
344-
uint256 scaledSolverReward = (proposal.proposedSolverReward * _consensusScore) / 100;
352+
// Solver reward scaled by correctness AND alignment
353+
// correctness: 0-100, alignment: 0-10000 (basis points)
354+
// Formula: base * (correctness/100) * (alignment/10000)
355+
// = base * correctness * alignment / 1_000_000
356+
uint256 scaledSolverReward = (proposal.proposedSolverReward * _consensusScore * _consensusAlignmentScore) / 1000000;
345357
try rpbContract.disburseFromBudget(_solver, scaledSolverReward) returns (bool success) {
346358
if (success) {
347359
emit RewardsDistributed(_taskId, _solver, scaledSolverReward, "SolverReward");
@@ -350,7 +362,7 @@ contract ResultsRewards {
350362
// Continue if disbursement fails
351363
}
352364

353-
// Proposer reward
365+
// Proposer reward (not alignment-gated — proposers create tasks, not charters)
354366
try rpbContract.disburseFromBudget(proposal.proposer, proposal.proposedLearnabilityReward) returns (bool success) {
355367
if (success) {
356368
emit RewardsDistributed(_taskId, proposal.proposer, proposal.proposedLearnabilityReward, "ProposerReward");
@@ -539,6 +551,7 @@ contract ResultsRewards {
539551
address _solver,
540552
bool _isCorrect,
541553
uint256 _score,
554+
uint256 _alignmentScore,
542555
string calldata _reportCid
543556
) external onlyStakedCoordinator {
544557
AutonetLib.TaskProposal memory proposal = taskContract.getTaskProposal(_taskId);

contracts/core/TaskContract.sol

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ contract TaskContract {
115115
_activateTask(_taskId);
116116
}
117117

118-
function commitSolution(uint256 _taskId, bytes32 _solutionHash)
118+
function commitSolution(uint256 _taskId, bytes32 _solutionHash, bytes32 _charterCid)
119119
external taskExists(_taskId) onlyStakedSolver
120120
{
121121
Task storage t = _tasks[_taskId];
@@ -127,6 +127,7 @@ contract TaskContract {
127127

128128
submissions[_taskId][msg.sender] = AutonetLib.SolverSubmission({
129129
solutionHash: _solutionHash,
130+
charterCid: _charterCid,
130131
solver: msg.sender,
131132
submissionBlock: block.number,
132133
score: 0

contracts/utils/AutonetLib.sol

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ library AutonetLib {
9393

9494
struct SolverSubmission {
9595
bytes32 solutionHash; // Hash of solution (content hash)
96+
bytes32 charterCid; // Content hash of solver's charter (for alignment scoring)
9697
address solver;
9798
uint256 submissionBlock;
9899
uint256 score; // Quality score if verified
@@ -166,7 +167,8 @@ library AutonetLib {
166167
struct CoordinatorVote {
167168
address coordinator;
168169
bool isCorrect;
169-
uint256 score; // 0-100
170+
uint256 score; // 0-100 correctness score
171+
uint256 alignmentScore; // 0-10000 charter↔constitution alignment (basis points)
170172
uint256 stake; // Coordinator's stake at vote time
171173
uint256 voteBlock;
172174
bytes32 reportHash;
@@ -176,7 +178,8 @@ library AutonetLib {
176178
uint256 taskId;
177179
address solver;
178180
bool consensusCorrect; // Final consensus decision
179-
uint256 consensusScore; // Stake-weighted score
181+
uint256 consensusScore; // Stake-weighted correctness score
182+
uint256 consensusAlignmentScore; // Stake-weighted alignment score (basis points)
180183
uint256 totalStake; // Total stake that voted
181184
uint256 correctStake; // Stake that voted correct
182185
uint256 clippedVotes; // Number of votes that were clipped

nodes/common/contracts.py

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -245,9 +245,23 @@ def propose_task(
245245
gas_limit=800000,
246246
)
247247

248-
def commit_solution(self, task_id: int, solution_hash: bytes) -> TransactionResult:
249-
"""Commit a solution hash for a task."""
250-
return self.send("TaskContract", "commitSolution", task_id, solution_hash)
248+
def commit_solution(self, task_id: int, solution_hash: bytes, charter_cid: bytes = b'\x00' * 32) -> TransactionResult:
249+
"""Commit a solution hash and charter CID for a task."""
250+
return self.send("TaskContract", "commitSolution", task_id, solution_hash, charter_cid)
251+
252+
def get_submission(self, task_id: int, solver: str) -> dict:
253+
"""Get a solver's submission for a task."""
254+
try:
255+
result = self.call("TaskContract", "getSubmission", task_id, solver)
256+
return {
257+
"solutionHash": result[0],
258+
"charterCid": result[1],
259+
"solver": result[2],
260+
"submissionBlock": result[3],
261+
"score": result[4],
262+
}
263+
except Exception:
264+
return {}
251265

252266
def submit_checkpoint(
253267
self,
@@ -295,12 +309,13 @@ def submit_vote(
295309
solver: str,
296310
is_correct: bool,
297311
score: int,
312+
alignment_score: int,
298313
report_cid: str,
299314
) -> TransactionResult:
300-
"""Submit a coordinator vote."""
315+
"""Submit a coordinator vote with correctness and alignment scores."""
301316
return self.send(
302317
"ResultsRewards", "submitVote",
303-
task_id, solver, is_correct, score, report_cid,
318+
task_id, solver, is_correct, score, alignment_score, report_cid,
304319
)
305320

306321
def finalize_voting(self, task_id: int, solver: str) -> TransactionResult:

nodes/coordinator/main.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from ..common.contracts import ContractRegistry
1919
from ..common.blob_store import BlobStore
2020
from ..common.governance import GovernanceBridge
21+
from ..common.alignment_pricing import AlignmentPricing
2122

2223
logging.basicConfig(level=logging.INFO)
2324
logger = logging.getLogger(__name__)
@@ -97,6 +98,10 @@ def __init__(
9798
# Governance bridge for attestation and heartbeat
9899
self.governance = GovernanceBridge(registry, node_id)
99100

101+
# Alignment scoring
102+
self._alignment_pricing = AlignmentPricing(embedding_mode=False)
103+
self._constitution_text: Optional[str] = None
104+
100105
logger.info(
101106
f"CoordinatorNode initialized: {node_id}"
102107
)
@@ -262,6 +267,9 @@ def _process_solution_reveal(self, event):
262267
ground_truth_cid
263268
)
264269

270+
# Compute alignment score (charter ↔ constitution)
271+
alignment_score = self._compute_solver_alignment(task_id, solver)
272+
265273
# Create verification report
266274
report = {
267275
"task_id": task_id,
@@ -270,6 +278,7 @@ def _process_solution_reveal(self, event):
270278
"ground_truth_cid": ground_truth_cid,
271279
"is_correct": is_correct,
272280
"score": score,
281+
"alignment_score": alignment_score,
273282
"coordinator": self.registry.blockchain.account.address,
274283
"timestamp": int(time.time()),
275284
"node_id": self.node_id,
@@ -285,6 +294,7 @@ def _process_solution_reveal(self, event):
285294
solver,
286295
is_correct,
287296
score,
297+
alignment_score,
288298
report_cid
289299
)
290300

@@ -673,3 +683,82 @@ def _update_bond_strength(self, success: Optional[bool] = None):
673683
self.EMA_ALPHA * target +
674684
(1 - self.EMA_ALPHA) * self.metrics.ema_bond_strength
675685
)
686+
687+
# ============ Alignment Scoring ============
688+
689+
def _get_constitution_text(self) -> str:
690+
"""Load and cache the jurisdiction constitution text."""
691+
if self._constitution_text is not None:
692+
return self._constitution_text
693+
694+
try:
695+
from ..core.constitution import DEFAULT_CONSTITUTION
696+
self._constitution_text = DEFAULT_CONSTITUTION.get_principle_summary()
697+
except Exception as e:
698+
logger.warning(f"Failed to load constitution: {e}")
699+
self._constitution_text = ""
700+
701+
return self._constitution_text
702+
703+
def _compute_solver_alignment(self, task_id: int, solver: str) -> int:
704+
"""
705+
Compute a solver's charter↔constitution alignment score.
706+
707+
Retrieves the solver's charter text from the blob store (via the
708+
charterCid committed with their solution), then computes similarity
709+
against the jurisdiction constitution.
710+
711+
Returns:
712+
Alignment score in basis points (0-10000).
713+
"""
714+
constitution = self._get_constitution_text()
715+
if not constitution:
716+
return 5000 # Neutral default if constitution unavailable
717+
718+
# Get solver's charter from their submission
719+
charter_text = self._get_solver_charter(task_id, solver)
720+
if not charter_text:
721+
return 5000 # Neutral default if charter unavailable
722+
723+
try:
724+
alignment, breakdown = self._alignment_pricing.compute_alignment(
725+
task_description=charter_text,
726+
user_standards=charter_text,
727+
jurisdiction_standards=constitution,
728+
)
729+
score_bps = int(alignment * 10000)
730+
logger.info(
731+
f"Alignment score for solver {solver[:10]}...: "
732+
f"{score_bps} bps (breakdown: {breakdown})"
733+
)
734+
return score_bps
735+
except Exception as e:
736+
logger.warning(f"Alignment computation failed: {e}")
737+
return 5000
738+
739+
def _get_solver_charter(self, task_id: int, solver: str) -> Optional[str]:
740+
"""
741+
Retrieve the solver's charter text from the blob store.
742+
743+
The solver commits a charterCid alongside their solution hash.
744+
We read it from the on-chain submission and fetch the text.
745+
"""
746+
try:
747+
submission = self.registry.get_submission(task_id, solver)
748+
charter_cid_bytes = submission.get("charterCid", b'\x00' * 32)
749+
750+
# Zero hash means no charter submitted
751+
if charter_cid_bytes == b'\x00' * 32 or not charter_cid_bytes:
752+
return None
753+
754+
# charterCid is a content hash — fetch from blob store
755+
charter_cid = charter_cid_bytes.hex() if isinstance(charter_cid_bytes, bytes) else str(charter_cid_bytes)
756+
charter_data = self.store.get_json(charter_cid)
757+
if charter_data and isinstance(charter_data, dict):
758+
return charter_data.get("charter", "")
759+
elif isinstance(charter_data, str):
760+
return charter_data
761+
return None
762+
except Exception as e:
763+
logger.warning(f"Failed to get solver charter: {e}")
764+
return None

nodes/solver/main.py

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -309,8 +309,11 @@ def _train_and_commit(self, task_id: int, task_info: TaskInfo):
309309

310310
logger.info(f"[{self.node_id}] Solution uploaded: {solution_cid}, hash: {solution_hash.hex()[:16]}...")
311311

312-
# Commit solution hash to blockchain
313-
commit_result = self.registry.commit_solution(task_id, solution_hash)
312+
# Upload charter for alignment scoring by coordinators
313+
charter_cid_bytes = self._upload_charter()
314+
315+
# Commit solution hash and charter CID to blockchain
316+
commit_result = self.registry.commit_solution(task_id, solution_hash, charter_cid_bytes)
314317

315318
if commit_result.success:
316319
logger.info(f"[{self.node_id}] Solution committed for task {task_id}: {commit_result.tx_hash}")
@@ -327,6 +330,27 @@ def _train_and_commit(self, task_id: int, task_info: TaskInfo):
327330
else:
328331
logger.error(f"[{self.node_id}] Failed to commit solution: {commit_result.error}")
329332

333+
def _upload_charter(self) -> bytes:
334+
"""Upload this solver's charter to the blob store for alignment scoring.
335+
336+
Returns:
337+
The charter CID as bytes32 (zero bytes if upload fails).
338+
"""
339+
try:
340+
from ..core.constitution import DEFAULT_CONSTITUTION
341+
charter = {
342+
"charter": self.config.charter if hasattr(self.config, 'charter') and self.config.charter else "",
343+
"node_id": self.node_id,
344+
"constitution_hash": DEFAULT_CONSTITUTION.get_principle_summary()[:64],
345+
}
346+
cid = self.store.add_json(charter)
347+
if cid:
348+
from web3 import Web3
349+
return Web3.keccak(text=cid)
350+
except Exception as e:
351+
logger.warning(f"[{self.node_id}] Failed to upload charter: {e}")
352+
return b'\x00' * 32
353+
330354
def _train(self, task_id: int, task_spec: Optional[dict] = None) -> tuple:
331355
"""
332356
Perform real ML training driven by task spec.

0 commit comments

Comments
 (0)