-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathproof_bundle_service.go
More file actions
196 lines (172 loc) · 6.56 KB
/
Copy pathproof_bundle_service.go
File metadata and controls
196 lines (172 loc) · 6.56 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
package main
import (
"fmt"
"strings"
"github.com/deroproject/derohe/globals"
"github.com/deroproject/derohe/proof"
)
// Offline proof receipts.
//
// ValidateProofFull (explorer_service.go) can only check a proof by calling
// DERO.GetTransaction on a daemon first, because proof.Prove needs the raw
// transaction and the completed ring and neither is recoverable from the proof
// string alone. That puts a third party in the verification path: whoever
// serves that RPC learns which transaction the user cares about, and that they
// are a party to it.
//
// A sender holds every input Prove needs at the moment they send. So the sender
// can package them once and hand them over, and the receiver can verify with no
// daemon, no explorer and no network at all.
//
// This does NOT make anything on chain more private -- the transaction is still
// public and still has a visible txid. It removes the lookup, not the record.
// ExportProofBundle builds a self-contained, shareable receipt for a
// transaction the caller already knows about.
//
// This half still needs the daemon, because it is assembling the receipt from
// chain data. That is fine: it runs on the SENDER's machine, about the sender's
// own transaction. The privacy win is on the receiving side.
func (a *App) ExportProofBundle(proofString string, txid string) map[string]interface{} {
if strings.TrimSpace(proofString) == "" {
return map[string]interface{}{"success": false, "error": "No proof string provided"}
}
if strings.TrimSpace(txid) == "" {
return map[string]interface{}{"success": false, "error": "Transaction ID required to build a receipt"}
}
a.logToConsole(fmt.Sprintf("[WALLET] Building offline proof receipt for %s", txid))
txHex, ringData, err := a.fetchTxAndRing(txid)
if err != nil {
return ErrorResponse(err)
}
bundle := proof.NewBundle(a.proofNetworkName(), txHex, ringData, proofString)
// Verify before handing it out. A receipt that does not verify on the
// sender's own machine will not verify on the receiver's, and finding that
// out now is much cheaper than after it has been sent.
if _, err := bundle.Verify(); err != nil {
a.logToConsole(fmt.Sprintf("[ERR] Refusing to export a receipt that does not verify: %v", err))
return map[string]interface{}{
"success": false,
"error": fmt.Sprintf("Receipt does not verify locally, refusing to export: %v", err),
}
}
data, err := bundle.Marshal()
if err != nil {
return ErrorResponse(err)
}
a.logToConsole("[OK] Offline proof receipt built and self-verified")
return map[string]interface{}{
"success": true,
"bundle": string(data),
"txid": bundle.TXID,
"network": bundle.Network,
}
}
// VerifyProofBundle checks a receipt with NO network access whatsoever.
//
// This is the point of the feature. No daemon, no explorer, no RPC -- it works
// on an air-gapped machine, and nobody learns which transaction was checked.
func (a *App) VerifyProofBundle(bundleJSON string) map[string]interface{} {
if strings.TrimSpace(bundleJSON) == "" {
return map[string]interface{}{"success": false, "error": "No receipt provided"}
}
a.logToConsole("[WALLET] Verifying proof receipt offline")
bundle, err := proof.UnmarshalBundle([]byte(bundleJSON))
if err != nil {
return map[string]interface{}{"success": false, "error": err.Error()}
}
// Bundle.Verify recovers internally -- a receipt is untrusted input by
// construction, and derohe's transaction parser panics on some malformed
// bytes rather than returning an error.
result, err := bundle.Verify()
if err != nil {
a.logToConsole(fmt.Sprintf("[ERR] Receipt did not verify: %v", err))
return map[string]interface{}{
"success": true,
"valid": false,
"error": err.Error(),
}
}
// Same hard-cap screen ValidateProofFull applies, so a fabricated amount
// cannot be displayed as real through this path either.
for i, amount := range result.Amounts {
if verr := proof.ValidatePayloadProofAmount(amount); verr != nil {
a.logToConsole(fmt.Sprintf("[SECURITY] Fake receipt rejected for receiver %d: %s", i+1, verr))
return map[string]interface{}{
"success": true,
"valid": false,
"error": verr.Error(),
"securityNote": "This receipt claims an amount that exceeds the DERO hard cap (21M) and is therefore fabricated.",
}
}
}
receivers := make([]map[string]interface{}, 0, len(result.Receivers))
for i := range result.Receivers {
entry := map[string]interface{}{"address": result.Receivers[i]}
if i < len(result.Amounts) {
entry["amount"] = result.Amounts[i]
entry["amountFormatted"] = globals.FormatMoney(result.Amounts[i])
}
if i < len(result.Payloads) {
entry["payload"] = result.Payloads[i]
}
receivers = append(receivers, entry)
}
a.logToConsole(fmt.Sprintf("[OK] Receipt verified offline, %d receiver(s)", len(receivers)))
return map[string]interface{}{
"success": true,
"valid": true,
"txid": result.TXID,
"network": bundle.Network,
"receivers": receivers,
"offline": true,
}
}
// proofNetworkName reports the network name the proof package expects.
func (a *App) proofNetworkName() string {
if network, ok := a.settings["network"].(string); ok && network == "simulator" {
return "testnet"
}
return "mainnet"
}
// fetchTxAndRing pulls the raw transaction hex and completed ring for a txid.
// Extracted from ValidateProofFull so both paths share one implementation.
func (a *App) fetchTxAndRing(txid string) (txHex string, ringData [][]string, err error) {
params := map[string]interface{}{
"txs_hashes": []string{txid},
"decode_as_json": 1,
}
result, err := a.daemonClient.Call("DERO.GetTransaction", params)
if err != nil {
return "", nil, err
}
resultMap, ok := result.(map[string]interface{})
if !ok {
return "", nil, fmt.Errorf("unexpected daemon response for %s", txid)
}
if txsHex, ok := resultMap["txs_as_hex"].([]interface{}); ok && len(txsHex) > 0 {
if hexStr, ok := txsHex[0].(string); ok {
txHex = hexStr
}
}
if txs, ok := resultMap["txs"].([]interface{}); ok && len(txs) > 0 {
if tx, ok := txs[0].(map[string]interface{}); ok {
if ring, ok := tx["ring"].([]interface{}); ok {
for _, payload := range ring {
var payloadRing []string
if addresses, ok := payload.([]interface{}); ok {
for _, addr := range addresses {
if addrStr, ok := addr.(string); ok {
payloadRing = append(payloadRing, addrStr)
}
}
}
ringData = append(ringData, payloadRing)
}
}
}
}
if txHex == "" {
return "", nil, fmt.Errorf("could not extract transaction hex from daemon response")
}
return txHex, ringData, nil
}