fix(auditor): attribute empty-EID inputs to their own token owner - #2199
Conversation
5e052bf to
856fbd4
Compare
856fbd4 to
0148b19
Compare
0148b19 to
6f7cbab
Compare
|
Thanks a lot for submitting this PR. According to my understanding (please correct me if I am wrong) Problem The auditor's ledger recorded the wrong person as the spender. When a spend arrived without an owner attached, the code guessed by picking the first name on the receiving side — which is the recipient, not the payer. Fix Look up each spent coin and record its actual previous owner as the spender. If ownership can't be pinned to a single person (e.g. a joint wallet), leave it blank rather than guess. Scenario
I think the direction makes sense to me; attributing the spend to the actual token owner rather than the first output is clearly the right model. A few concerns before it lands:
Regards, |
6f7cbab to
c21333e
Compare
|
@AkramBitar Thanks — your summary is exactly right. (2), (3) and (5) are fixed in the latest push; on (1) you were right to ask and my first answer was wrong; (4) I would like your opinion on. 1. Multi-owner actions. Correcting an earlier version of this reply, in which I claimed this was unreachable: it is reachable.
One detail of the consequence is milder than feared: That leaves a real question: reject such an action earlier with a clear error, or teach the store to represent a multi-sender action. I would rather not settle it inside this PR — happy to open a separate issue unless you prefer it handled here. 2. Godoc vs. behaviour. Fixed — a leftover from an earlier revision that did fail closed. The Godoc and the doc page now both state that such an input is left unattributed. 3. Bounds/nil check. Fixed, mirroring 4. Two TMS sources. Narrowed by the push below: 5. The "returns immediately" note. Correct, and now moot: Also pushed. Since then: |
62c3f2c to
ecb71f9
Compare
There was a problem hiding this comment.
Thanks a lot for all the efforts that you put on that PR.
I did additional review on what we have.
Regarding your question: "That leaves a real question: reject such an action earlier with a clear error, or teach the store to represent a multi-sender action. I would rather not settle it inside this PR — happy to open a separate issue unless you prefer it handled here."
I preferer to handle it in different issue, however I leave this to you.
Thanks a million,
Akram
Optional / follow-up:
token/services/storage/ttxdb/store.go:376— happy to defer the multi-owner case as agreed. Two small asks: open the follow-up issue as part of this PR, and reject atAudittime with a message that names the multi-owner cause. Today it surfaces asexpected at most 1 input enrollment idfromAppend, which is easy to misdiagnose. Optional for this PR.- Documented preference order is inverted.
docs/services/auditor.md:62andWalletManager.GetEIDAndRH's godoc (token/wallet.go:169) both say the record-carried audit info is preferred over the locally stored one, butRequest.AuditRecordhas already overwrittenin.OwnerAuditInfowith the local lookup oftoks[i].OwnerbeforecompleteInputsWithEmptyEIDruns — so in practice the local one wins wherever it exists.TestRequestWrapper_CompleteInputsWithEmptyEID_UsesRecordAuditInfohand-builds the record, so it doesn't catch this. Optional. auditor.go:437re-reads audit tokens thatRequest.AuditRecordalready fetched, only to addOwner. HavingAuditRecordsetin.Owner = toks[i].Ownerwould remove a vault query per unattributed input. Optional, nit.
|
@AkramBitar Done:
On the optional points:
|
|
Thanks for #2242 and I re-checked the remaining points against 1. Composite-owner upgrade is still credited without being debited ( 2. Owner-resolution errors still fail the audit ( 3. Documented preference order is inverted 4. Action-scoped output filter ( 5. Cache entry lifetime ( 6. TMS source ( Thanks again — I think 1 and 2 are the main blockers, while I’d like your call on 3–6. What do you think? |
|
@AkramBitar Thanks — all six addressed. 1, 2 and 6 are in the pushed commits; 3's docs are fixed here; 3's behavior, 4 and 5 are now #2251, #2249 and #2250. 1. Composite-owner upgrade. Working through the cases, deduplicating by (Index, EnrollmentID) as 2. Owner-resolution errors. Done, with the classification made at the layer that knows: 3. Preference order. Docs fixed here: the locally stored audit info wins, since 4. Action-scoped filter → #2249. 5. Cache entry lifetime → #2250. 6. TMS source. Done, one step further than resolving through the provider: the record computation itself runs through |
The auditor attributed an input with an unresolved enrollment ID to the first output's enrollment ID — in a payment, the recipient — charging the counterparty and never booking the payer. Each such input is now resolved from its own spent token's owner, and the gap filling runs in Audit so the EID locks cover what the record is finally booked under; Append reuses the record Audit attributed. An owner that maps to no single enrollment ID — a composite owner such as a multisig — leaves its input unattributed rather than booked under a guess. Only decoding failures count as unresolvable: the identity layer marks them with identity.ErrUnresolvableIdentity, keeping the cause in the chain; storage failures and context cancellation fail the audit. A token upgrade describes no sender for its inputs and its pre-upgrade owner often resolves to nothing, so such an input takes the enrollment ID of the outputs issued by its own action when every one of them resolves to the same party. Shapes that would credit the members while debiting nobody fail the audit instead: an action spending tokens of multiple enrollment IDs, a composite owner spanning enrollment IDs, issued outputs that only partly resolve. Audit and Append rebind the request to the provider-resolved TMS before the record is computed, so the request cannot influence which TMS attributes the record. Follow-ups: LFDT-Panurus#2242 (multi-sender representation), LFDT-Panurus#2249 (action-scoped output filter), LFDT-Panurus#2250 (cache entry lifetime), LFDT-Panurus#2251 (audit-info preference order). Fixes LFDT-Panurus#2198 Signed-off-by: Evan <evanyan@sign.global>
f127045 to
6c77de6
Compare
|
@AkramBitar Done — rebased on latest main and squashed to a single commit ( |
Fixes #2198
What
WalletManager.GetEIDAndRH(ctx, identity, auditInfo), which resolves through the audit info attached to the input — the locally stored audit info of the spent token's owner where present (Request.AuditRecordfills it in), the request-carried one otherwise; when neither is available it returns empty values rather than an error.Audit, before the enrollment IDs are collected, so the locks cover the enrollment ID each input is finally booked under, and return a record cached byAuditfromAppendas it stands.Appendstill attributes the record when called without a precedingAudit.Request.AuditRecorddoes — a length check plus a per-token nil check — since this code now runs on the audit approval path.Why
In a payment the first output is the recipient, so the old
targetEID := record.Outputs.EnrollmentIDs()[0]recorded the payer's spend against the counterparty: the recipient is charged a spend it never made, and the payer's spend is never booked. Neither side errors or warns. TheTODOabove that line marked it as a placeholder.Leaving an unattributable input empty rather than guessing is the point of the change:
EnrollmentIDs()skips empty enrollment IDs, so such an input is counted for nobody, which is the honest outcome when the owner genuinely maps to no single enrollment ID. A multisig input is exactly that — several members, no single one to charge — so it is a normal state to represent, not a failure to raise.The upgrade case is the one where the same guess was right, and dropping it showed up as a red
TokensUpgradeintegration test (alice's holding 220 instead of 110).extractIssueInputsfills only the token id, and the pre-upgrade owner predates the current driver, so nothing in the record resolves it. But an upgrade re-issues the spent tokens to their owner under a fresh identity, so what the request issues to is the input's enrollment ID — leaving it empty credits the upgraded amount without ever debiting it. The fallback is confined to inputs the request describes no sender for, and scoped to the outputs of the input's own action: a second issue action neither suppresses the attribution nor lends its enrollment ID to it. Every issued output of that action must resolve to the same party — one resolving to none cannot be shown to belong to the others — and the handle is kept only while it stays paired with that enrollment ID.The lifecycle matters as much as the attribution.
Auditcollects the enrollment IDs it locks from the record andAppendwrites that record, so filling the gaps only inAppendwould leave the final record booked against a payer that was never locked — an exact spend has no change output, so the payer's enrollment ID appears nowhere in whatAuditsaw.The same reasoning applies in reverse to the cached record: re-running the gap filling in
Appendcould attribute an input thatAuditdeliberately left empty, storing it under an enrollment ID that was never locked.Appendtherefore returns the cached record as it stands. Together these keep the locked set and the stored record in agreement, and remove the second vault read for records carrying an unattributed input.Resolving from the input's own owner also drops the
record.Outputs.EnrollmentIDs()[0]index-out-of-range on a record with no outputs.Testing
token/services/auditor/auditor_internal_test.go: attribution from the token owner; record-carried audit info preferred over a local lookup (asserting no local lookup happens); a composite owner and a missing-audit-info owner both staying unattributed without an error, with the remaining fields still filled from the spent token; a genuine resolution error still propagating; a short vault answer and a nil vault token each erroring instead of panicking.TestRequestWrapper_AuditRecord_UpgradeInputAttributedToReceiverruns an upgrade-shaped request through the realRequest.AuditRecordpipeline rather than a hand-written record, so whatextractIssueInputsandextractIssueOutputsactually produce — the input arriving without an owner, the issued output carrying issuer, owner and enrollment ID under the same action index — is exercised instead of assumed. It also asserts the record's inputs and outputs sum equal, which is the holding that went to 220 inupdate-t1.TestRequestWrapper_AuditRecord_CachedKeepsUnattributedInput: a record cached byAuditwith an unattributed input is returned untouched even though the wallet service would now resolve it, with no further vault or identity lookup.TestService_Audit_AttributesAndLocksEmptyEIDInputpins the lifecycle over an exact spend whose sender does not resolve locally:Auditreturns the resolved payer enrollment ID and revocation handle, the acquired locks contain that payer and no empty ID, andAppendreuses the record without a second vault read or identity lookup. Moving the gap filling back after lock acquisition fails this test.gofmt,go build,go vet,go test -race, and golangci-lint v2.12.2 with the repo config.Docs
docs/services/auditor.mdgains an "Input Attribution" section: the per-input resolution order, the upgrade fallback, what happens to an owner with no single enrollment ID, and where in theAudit/Appendlifecycle it runs relative to the EID locking documented just below it.Review rounds
identity.ErrUnresolvableIdentity, keeping the cause in the chain; the gap filling treats only that class as unresolvable, while storage failures and context cancellation fail the audit.AuditandAppendrebind the request to the provider-resolved TMS before the record is computed, so the request cannot influence which TMS attributes the record.docs/services/auditor.mdgained an Input Attribution section describing the behavior, including the audit-info preference order (the local store wins; flipping it is auditor: decide which audit info wins during input attribution #2251).