You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(auditor): attribute empty-EID inputs to their own token owner
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: #2242 (multi-sender representation), #2249 (action-scoped
output filter), #2250 (cache entry lifetime), #2251 (audit-info
preference order).
Fixes#2198
Signed-off-by: Evan <evanyan@sign.global>
Copy file name to clipboardExpand all lines: docs/services/auditor.md
+17Lines changed: 17 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -52,6 +52,23 @@ Auditors use specialized wallets (Auditor Wallets) managed by the **Identity Ser
52
52
53
53
The service provides the `AuditApproveView`, which auditors use to respond to incoming audit requests. This view automates the verification and signing process, ensuring that the auditor only approves transactions that are fully compliant with the system's public parameters.
54
54
55
+
## Input Attribution
56
+
57
+
An audit record pairs every input and output with an enrollment ID. Inputs do not always carry one, so those left empty are resolved before the record is stored; the rest are untouched.
58
+
59
+
Each such input is resolved from its own spent token:
60
+
61
+
1. The spent token is read from the vault, yielding its owner, type, and quantity.
62
+
2. The owner is resolved to an enrollment ID and revocation handle through `WalletManager.GetEIDAndRH`, using the audit info attached to the input: the locally stored audit info of the spent token's owner where present, the one carried by the request metadata otherwise — a counterparty's audit info is not necessarily present locally. An owner the identity layer cannot decode counts as resolving to nothing; any other resolution failure — a storage error, a canceled context — fails the audit.
63
+
64
+
Across the built-in drivers, a **token upgrade** is the action whose metadata describes no sender for its inputs, and whose pre-upgrade owner often resolves to nothing: that identity predates the current driver and the request carries no audit info for it. Since an upgrade re-issues the spent tokens to the same party under a fresh identity, 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. The revocation handle comes from the same output, and is dropped when the outputs carry more than one. Without this the upgraded amount would be credited to the owner without ever being debited, doubling the holding. A composite owner issues one output row per member, all under one output index: members resolving to one enrollment ID attribute the input to it, while members spanning enrollment IDs fail the audit — a record keeps a single sender per action, and an unattributed input would credit the members without debiting anyone. Issued outputs that only partly resolve fail the audit for the same reason; when none resolve, nothing is credited and the input stays unattributed.
65
+
66
+
An owner that maps to no single enrollment ID — a composite owner such as a multisig, or one whose audit info is not available to, or not decodable by, this auditor — leaves its input **unattributed**, with an empty enrollment ID. Amount aggregations skip empty enrollment IDs, so an unattributed input is counted for nobody. Guessing instead, for instance from the first output, would in a payment attribute the payer's spending to the recipient and silently corrupt both balances.
67
+
68
+
An action whose inputs attribute to **more than one** enrollment ID — reachable when the tokens to spend are passed explicitly, since they are not constrained to a single wallet — fails the audit with an error naming the cause: a transaction record keeps a single sender per action, so the store cannot represent it. Representing multi-sender actions is tracked separately.
69
+
70
+
Attribution runs in `Audit`, before the enrollment IDs are collected, so the EID locking described below covers the enrollment ID each input is finally booked under. `Append` reuses the record `Audit` attributed and stores it as it stands; it attributes the record itself only when called without a preceding `Audit`. An input left unattributed by `Audit` therefore stays unattributed, and no enrollment ID outside the locked set can reach the store.
71
+
55
72
## Distributed EID Locking
56
73
57
74
When multiple auditor replicas share the same AuditDB (PostgreSQL), concurrent processing of the same enrollment IDs (EIDs) must be serialized. The **auditor locker** (`token/services/storage/auditdb/locker`) coordinates exclusive access to EIDs during audit record writes.
returnerrors.Errorf("action [%d] of request [%s] spends tokens of multiple enrollment IDs ([%s] and [%s]): a transaction record keeps a single sender per action", in.ActionIndex, record.Anchor, eID, in.EnrollmentID)
464
+
}
465
+
}
466
+
467
+
returnnil
468
+
}
469
+
416
470
// completeInputsWithEmptyEID fills in missing enrollment ID information for inputs in the audit record
417
471
// by querying the token vault. This is necessary when inputs don't have enrollment IDs explicitly set.
418
-
// It uses the first output's enrollment ID as the target and retrieves token details from the vault.
472
+
// Each input is attributed to the enrollment ID resolved from its own token
473
+
// owner and the audit info the input carries — the locally stored audit info
474
+
// of the owner where present, the one carried by the request otherwise (see
475
+
// Request.AuditRecord). An owner the identity layer cannot decode counts as
476
+
// resolving to nothing; any other resolution failure — a storage error, a
477
+
// canceled context — fails the audit. An input the request describes no sender for is an
478
+
// upgrade input, and falls back to the enrollment ID the request issues to;
479
+
// an upgrade issued to a composite owner spanning enrollment IDs fails the
480
+
// audit (see issuedToEIDAndRH). An owner that maps to no single enrollment ID
481
+
// leaves its input unattributed rather than booked under a guessed enrollment
482
+
// ID, so a record keeping such an input is not fully attributed on return.
"output [%d] of action [%d] is issued to a composite owner whose members span enrollment IDs ([%s] and [%s]): no single enrollment ID to attribute its input to",
0 commit comments