Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -220,41 +220,30 @@ describe('PendingRcaConsumer', () => {
expect(proposals[1].tokensPerSecond).toBe(200n)
})

test('rejects rows with non-empty signature (producer regression)', async () => {
const errorSpy = jest.fn()
const testLogger = {
...logger,
error: errorSpy,
info: jest.fn(),
warn: jest.fn(),
child: () => testLogger,
} as unknown as Logger

const badPayload = encodeTestPayload({ signature: '0xdeadbeef' })
test('ignores a non-empty signer signature and decodes the proposal', async () => {
const signedPayload = encodeTestPayload({
signature: '0xdeadbeef',
tokensPerSecond: 150n,
})

const model = createMockModel([
{
id: 'bad-sig-uuid',
signed_payload: badPayload,
id: 'signed-uuid',
signed_payload: signedPayload,
version: 2,
status: 'pending',
created_at: new Date(),
updated_at: new Date(),
},
])

const consumer = new PendingRcaConsumer(testLogger, model)
const consumer = new PendingRcaConsumer(logger, model)
const proposals = await consumer.getPendingProposals()

expect(proposals).toHaveLength(0)
expect(model.update).toHaveBeenCalledWith(
{ status: 'rejected' },
{ where: { id: 'bad-sig-uuid' } },
)
expect(errorSpy).toHaveBeenCalledWith(
expect.stringContaining('non-empty signature'),
expect.any(Object),
)
expect(proposals).toHaveLength(1)
expect(proposals[0].id).toBe('signed-uuid')
expect(proposals[0].tokensPerSecond).toBe(150n)
expect(model.update).not.toHaveBeenCalled()
})
})

Expand Down
29 changes: 4 additions & 25 deletions packages/indexer-common/src/indexing-fees/pending-rca-consumer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,34 +73,13 @@ export class PendingRcaConsumer {
}
}

// Returns the decoded proposal, or null if the row was rejected here
// (non-empty signature — producer regression, marked rejected in the DB
// before returning).
//
// Decode failures from toolshed (malformed payload, bad metadata, etc.)
// propagate as throws; the caller skip-logs them, leaving the row pending
// for the next cycle.
// Returns the decoded proposal, or throws on a malformed payload (the caller
// skip-logs those, leaving the row pending). An embedded signer signature is
// ignored — acceptance is offer-based, so the agent has no use for it.
private async decodeRow(row: PendingRcaProposal): Promise<DecodedRcaProposal | null> {
const signedPayload = new Uint8Array(row.signed_payload)
const signedRca = decodeSignedRCA(signedPayload)
const { rca, signature } = signedRca

if (signature && signature !== '0x') {
const sigByteLength = Math.max(0, (signature.length - 2) / 2)
this.logger.error(
`Pending RCA proposal ${row.id} has non-empty signature (producer regression); rejecting`,
{ id: row.id, signatureLength: sigByteLength },
)
try {
await this.markRejected(row.id, 'non_empty_signature')
} catch (err) {
this.logger.error('Failed to mark non-empty-signature proposal as rejected', {
id: row.id,
error: err instanceof Error ? err.message : String(err),
})
}
return null
}
const { rca } = signedRca

const metadata = decodeAcceptIndexingAgreementMetadata(rca.metadata)
const terms = decodeIndexingAgreementTermsV1(metadata.terms)
Expand Down
Loading