Skip to repository content585 lines · 22.9 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T03:04:12.503Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
community_verification.rs
1//! The verifier-signed independent verification event (omega#48).
2//!
3//! Contract: `SARAH-CW-00` §8.4, amendment `SARAH-CW-00-A1` (2026-07-25), in
4//! `docs/omega/2026-07-24-community-workroom-contract.md` in the openagents
5//! repository.
6//!
7//! ## Why this exists on the Rust side at all
8//!
9//! The contract's §4 writable-authority table already reserved a row for
10//! independent verification with its authority named and its wire form left
11//! unspecified. Every client therefore read independence off Sarah's
12//! *arbitration decision* — the deciding key asserting, on the verifier's
13//! behalf, that a verification happened. Step 6 of the §8.1 lifecycle was the
14//! one step nobody signed.
15//!
16//! The amendment specifies the wire form. A second implementation must be able
17//! to reach the same admission from the contract and the fixtures alone, so
18//! this module decodes the **same fixture bytes** as
19//! `packages/sarah/src/community-arbitration/verification.ts`. The digests in
20//! [`SHARED_FIXTURE_DIGESTS`] are what makes that enforceable rather than
21//! conventional: edit a fixture in either repository and that repository's test
22//! goes red until the other agrees.
23//!
24//! ## What this module does not do
25//!
26//! It admits a verification. It never accepts a unit — acceptance is Sarah's,
27//! per the §4 acceptance row — and it holds no state.
28
29use std::collections::{BTreeMap, BTreeSet};
30
31use serde::Deserialize;
32
33/// Schema id of the record the verifying agent signs.
34pub const COMMUNITY_INDEPENDENT_VERIFICATION_SCHEMA: &str =
35 "openagents.sarah.community_independent_verification.v1";
36
37/// Amendment packet that specified this wire form.
38pub const COMMUNITY_INDEPENDENT_VERIFICATION_PACKET: &str = "SARAH-CW-00-A1";
39
40/// NIP-90 feedback kind. The carrier the contract already used for the quote,
41/// the acceptance, the decision, the appeal and the ruling. No new kind.
42pub const COMMUNITY_FEEDBACK_KIND: u32 = 7000;
43
44/// The `cw_feedback_type` value. A fourth value in an existing discriminator.
45pub const INDEPENDENT_VERIFICATION_FEEDBACK_TYPE: &str = "independent_verification";
46
47/// The verifier's verdict.
48///
49/// Deliberately not `accepted` / `rejected`: those are Sarah's words and this
50/// event must not be mistakable for the decision.
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub enum VerificationVerdict {
53 Reproduced,
54 NotReproduced,
55 Inconclusive,
56}
57
58impl VerificationVerdict {
59 #[must_use]
60 pub const fn token(self) -> &'static str {
61 match self {
62 Self::Reproduced => "reproduced",
63 Self::NotReproduced => "not_reproduced",
64 Self::Inconclusive => "inconclusive",
65 }
66 }
67
68 #[must_use]
69 pub fn parse_token(token: &str) -> Option<Self> {
70 match token {
71 "reproduced" => Some(Self::Reproduced),
72 "not_reproduced" => Some(Self::NotReproduced),
73 "inconclusive" => Some(Self::Inconclusive),
74 _ => None,
75 }
76 }
77}
78
79/// Why a verification event did not become an admitted verification.
80///
81/// Every one of these is a refusal a reader can render. A verification that
82/// vanishes silently is indistinguishable from one that was never published.
83#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84pub enum VerificationRefusal {
85 NotAVerificationEvent,
86 Malformed,
87 /// Somebody other than the verifier signed the verifier's claim. The state
88 /// this amendment exists to end.
89 VerifierNotAuthor,
90 VerifierKeyBurned,
91 VerifierBindingUnconfirmed,
92 ProducerBindingUnconfirmed,
93 SelfDealingOperators,
94 DecidesPaymentForbidden,
95}
96
97impl VerificationRefusal {
98 #[must_use]
99 pub const fn token(self) -> &'static str {
100 match self {
101 Self::NotAVerificationEvent => "not_a_verification_event",
102 Self::Malformed => "malformed",
103 Self::VerifierNotAuthor => "verifier_not_author",
104 Self::VerifierKeyBurned => "verifier_key_burned",
105 Self::VerifierBindingUnconfirmed => "verifier_binding_unconfirmed",
106 Self::ProducerBindingUnconfirmed => "producer_binding_unconfirmed",
107 Self::SelfDealingOperators => "self_dealing_operators",
108 Self::DecidesPaymentForbidden => "decides_payment_forbidden",
109 }
110 }
111}
112
113/// An admitted verification, with both operators read from the folded record.
114#[derive(Debug, Clone, PartialEq, Eq)]
115pub struct AdmittedVerification {
116 pub source_event_id: String,
117 pub result_event_id: String,
118 pub verifier_agent_pubkey: String,
119 /// From the binding. Never from a tag.
120 pub verifier_operator_pubkey: String,
121 pub producer_agent_pubkey: String,
122 /// From the binding. Never from a tag.
123 pub producer_operator_pubkey: String,
124 pub verdict: VerificationVerdict,
125}
126
127/// A signed event as it arrives from a relay.
128#[derive(Debug, Clone, Deserialize)]
129pub struct VerificationEvent {
130 pub id: String,
131 pub pubkey: String,
132 pub created_at: i64,
133 pub kind: u32,
134 pub tags: Vec<Vec<String>>,
135 pub content: String,
136}
137
138/// The agent → operator binding a caller resolves from its folded record.
139///
140/// A map plus a burn set rather than the whole ledger: this module has no
141/// business folding membership, and taking only what it reads makes it obvious
142/// that nothing here can be talked into a binding by an event.
143#[derive(Debug, Clone, Default)]
144pub struct CommunityBinding {
145 operator_by_agent: BTreeMap<String, String>,
146 burned_agent_keys: BTreeSet<String>,
147}
148
149impl CommunityBinding {
150 #[must_use]
151 pub fn new() -> Self {
152 Self::default()
153 }
154
155 #[must_use]
156 pub fn bind(mut self, agent_pubkey: &str, operator_pubkey: &str) -> Self {
157 self.operator_by_agent
158 .insert(agent_pubkey.trim().to_ascii_lowercase(), operator_pubkey.trim().to_ascii_lowercase());
159 self
160 }
161
162 #[must_use]
163 pub fn burn(mut self, agent_pubkey: &str) -> Self {
164 self.burned_agent_keys
165 .insert(agent_pubkey.trim().to_ascii_lowercase());
166 self
167 }
168
169 #[must_use]
170 pub fn operator_for_agent(&self, agent_pubkey: &str) -> Option<&str> {
171 self.operator_by_agent
172 .get(&agent_pubkey.trim().to_ascii_lowercase())
173 .map(String::as_str)
174 }
175
176 #[must_use]
177 pub fn is_agent_key_burned(&self, agent_pubkey: &str) -> bool {
178 self.burned_agent_keys
179 .contains(&agent_pubkey.trim().to_ascii_lowercase())
180 }
181}
182
183fn tag_value<'a>(event: &'a VerificationEvent, name: &str) -> Option<&'a str> {
184 event
185 .tags
186 .iter()
187 .find(|tag| tag.first().map(String::as_str) == Some(name))
188 .and_then(|tag| tag.get(1))
189 .map(String::as_str)
190}
191
192fn tagged_event_id<'a>(event: &'a VerificationEvent, marker: &str) -> Option<&'a str> {
193 event
194 .tags
195 .iter()
196 .find(|tag| {
197 tag.first().map(String::as_str) == Some("e")
198 && tag.get(3).map(String::as_str) == Some(marker)
199 })
200 .and_then(|tag| tag.get(1))
201 .map(String::as_str)
202}
203
204fn is_hex_64(value: &str) -> bool {
205 value.len() == 64 && value.bytes().all(|byte| byte.is_ascii_hexdigit() && !byte.is_ascii_uppercase())
206}
207
208/// Admit one signed verification event against the folded record.
209///
210/// Both operators come from `binding`, never from a tag. `cw_verifier_operator_ref`
211/// is what the *verifier* asserts about itself, and an agent key asserting its
212/// own operator is exactly the self-dealing shape refused on the decision path.
213///
214/// # Errors
215///
216/// Returns the typed refusal. There is no silent drop.
217pub fn admit_independent_verification(
218 event: &VerificationEvent,
219 binding: &CommunityBinding,
220) -> Result<AdmittedVerification, VerificationRefusal> {
221 let verifier_key = event.pubkey.trim().to_ascii_lowercase();
222
223 let feedback_type = tag_value(event, "cw_feedback_type")
224 .or_else(|| tag_value(event, "lbr_feedback_type"));
225 if event.kind != COMMUNITY_FEEDBACK_KIND
226 || feedback_type != Some(INDEPENDENT_VERIFICATION_FEEDBACK_TYPE)
227 {
228 return Err(VerificationRefusal::NotAVerificationEvent);
229 }
230
231 let result_event_id = tagged_event_id(event, "result").unwrap_or_default().to_owned();
232 let producer_key = tag_value(event, "cw_producer_agent_pubkey")
233 .or_else(|| tag_value(event, "p"))
234 .unwrap_or_default()
235 .trim()
236 .to_ascii_lowercase();
237 let verdict = tag_value(event, "status").and_then(VerificationVerdict::parse_token);
238
239 let Some(verdict) = verdict else {
240 return Err(VerificationRefusal::Malformed);
241 };
242 if !is_hex_64(&result_event_id) || !is_hex_64(&producer_key) {
243 return Err(VerificationRefusal::Malformed);
244 }
245
246 if tag_value(event, "cw_decides_payment") != Some("false") {
247 return Err(VerificationRefusal::DecidesPaymentForbidden);
248 }
249
250 // The whole point of the amendment.
251 if let Some(claimed) = tag_value(event, "cw_verifier_agent_pubkey")
252 && claimed.trim().to_ascii_lowercase() != verifier_key
253 {
254 return Err(VerificationRefusal::VerifierNotAuthor);
255 }
256 if verifier_key == producer_key {
257 return Err(VerificationRefusal::SelfDealingOperators);
258 }
259
260 // Revocation binds the subject whatever it signs and whenever it arrives.
261 // Checked before the binding lookup so a burned key with a live binding row
262 // cannot pass as merely "unconfirmed".
263 if binding.is_agent_key_burned(&verifier_key) {
264 return Err(VerificationRefusal::VerifierKeyBurned);
265 }
266
267 let Some(verifier_operator) = binding.operator_for_agent(&verifier_key) else {
268 return Err(VerificationRefusal::VerifierBindingUnconfirmed);
269 };
270 if let Some(claimed_operator) = tag_value(event, "cw_verifier_operator_ref")
271 && claimed_operator.trim().to_ascii_lowercase() != verifier_operator
272 {
273 return Err(VerificationRefusal::VerifierBindingUnconfirmed);
274 }
275
276 let Some(producer_operator) = binding.operator_for_agent(&producer_key) else {
277 return Err(VerificationRefusal::ProducerBindingUnconfirmed);
278 };
279
280 // Distinct *operators*, not merely distinct keys. One operator holding two
281 // agent keys passes every key comparison above.
282 if producer_operator == verifier_operator {
283 return Err(VerificationRefusal::SelfDealingOperators);
284 }
285
286 Ok(AdmittedVerification {
287 source_event_id: event.id.clone(),
288 result_event_id,
289 verifier_agent_pubkey: verifier_key,
290 verifier_operator_pubkey: verifier_operator.to_owned(),
291 producer_agent_pubkey: producer_key,
292 producer_operator_pubkey: producer_operator.to_owned(),
293 verdict,
294 })
295}
296
297/// The exact bytes both repositories hold, by SHA-256 of the file content.
298///
299/// Not a convenience. A fixture is only "byte-shared" if drift is detectable
300/// from inside one repository without reading the other.
301pub const SHARED_FIXTURE_DIGESTS: &[(&str, &str)] = &[
302 (
303 "openagents.sarah.community_independent_verification.v1.canonical.json",
304 "992085428cf36e74ee0f1b6bdb6b38c494e7b7cc1b68d668e82bef74fcfa928f",
305 ),
306 (
307 "openagents.sarah.community_independent_verification.v1.negative-operators-not-independent.json",
308 "35061fb7846a3fa961ac7335e96dd9cbd21b080ba5bf2fe0389402ce22d3333b",
309 ),
310 (
311 "openagents.sarah.community_independent_verification.v1.negative-verifier-key-burned.json",
312 "0a01c8665d52cf0542084897bc79eac52a5cca3b3ae0dfb725ead4f5a8dbcc36",
313 ),
314 (
315 "openagents.sarah.community_independent_verification.v1.negative-verifier-not-author.json",
316 "c2f91af69fe979acdcbe8550060d782c72d963c7346d8d6febb5705f87338271",
317 ),
318];
319
320#[cfg(test)]
321mod tests {
322 use super::*;
323
324 const CANONICAL: &str = include_str!(
325 "../fixtures/openagents.sarah.community_independent_verification.v1.canonical.json"
326 );
327 const NOT_AUTHOR: &str = include_str!(
328 "../fixtures/openagents.sarah.community_independent_verification.v1.negative-verifier-not-author.json"
329 );
330 const NOT_INDEPENDENT: &str = include_str!(
331 "../fixtures/openagents.sarah.community_independent_verification.v1.negative-operators-not-independent.json"
332 );
333 const BURNED: &str = include_str!(
334 "../fixtures/openagents.sarah.community_independent_verification.v1.negative-verifier-key-burned.json"
335 );
336
337 #[derive(Debug, Deserialize)]
338 struct BoundAgent {
339 #[serde(rename = "agentPubkey")]
340 agent_pubkey: String,
341 #[serde(rename = "operatorPubkey")]
342 operator_pubkey: String,
343 }
344
345 #[derive(Debug, Deserialize)]
346 struct FixtureBinding {
347 agents: Vec<BoundAgent>,
348 #[serde(rename = "burnedAgentKeys")]
349 burned_agent_keys: Vec<String>,
350 }
351
352 #[derive(Debug, Deserialize)]
353 struct Fixture {
354 fixture: String,
355 packet: String,
356 expect: String,
357 binding: FixtureBinding,
358 event: VerificationEvent,
359 expected: serde_json::Value,
360 }
361
362 fn load(raw: &str) -> (Fixture, CommunityBinding) {
363 let fixture: Fixture = serde_json::from_str(raw).expect("fixture decodes");
364 let mut binding = CommunityBinding::new();
365 for agent in &fixture.binding.agents {
366 binding = binding.bind(&agent.agent_pubkey, &agent.operator_pubkey);
367 }
368 for key in &fixture.binding.burned_agent_keys {
369 binding = binding.burn(key);
370 }
371 (fixture, binding)
372 }
373
374 /// The cross-repository check. These bytes are the same bytes
375 /// `packages/sarah` decodes, and this test is what notices if they stop
376 /// being.
377 #[test]
378 fn the_shared_fixtures_are_the_exact_bytes_the_typescript_side_holds() {
379 use std::fmt::Write as _;
380 for (name, expected) in SHARED_FIXTURE_DIGESTS {
381 let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
382 .join("fixtures")
383 .join(name);
384 let bytes = std::fs::read(&path).expect("shared fixture is readable");
385 let digest = sha256_hex(&bytes, &mut String::new());
386 let mut rendered = String::new();
387 write!(&mut rendered, "{digest}").expect("write");
388 assert_eq!(
389 &rendered, expected,
390 "{name} drifted from the byte-shared copy in packages/sarah. \
391 Re-share the file rather than re-pinning one side."
392 );
393 }
394 }
395
396 /// Minimal SHA-256 so this crate keeps its two-dependency footprint.
397 fn sha256_hex(bytes: &[u8], scratch: &mut String) -> String {
398 const K: [u32; 64] = [
399 0x428a_2f98, 0x7137_4491, 0xb5c0_fbcf, 0xe9b5_dba5, 0x3956_c25b, 0x59f1_11f1,
400 0x923f_82a4, 0xab1c_5ed5, 0xd807_aa98, 0x1283_5b01, 0x2431_85be, 0x550c_7dc3,
401 0x72be_5d74, 0x80de_b1fe, 0x9bdc_06a7, 0xc19b_f174, 0xe49b_69c1, 0xefbe_4786,
402 0x0fc1_9dc6, 0x240c_a1cc, 0x2de9_2c6f, 0x4a74_84aa, 0x5cb0_a9dc, 0x76f9_88da,
403 0x983e_5152, 0xa831_c66d, 0xb003_27c8, 0xbf59_7fc7, 0xc6e0_0bf3, 0xd5a7_9147,
404 0x06ca_6351, 0x1429_2967, 0x27b7_0a85, 0x2e1b_2138, 0x4d2c_6dfc, 0x5338_0d13,
405 0x650a_7354, 0x766a_0abb, 0x81c2_c92e, 0x9272_2c85, 0xa2bf_e8a1, 0xa81a_664b,
406 0xc24b_8b70, 0xc76c_51a3, 0xd192_e819, 0xd699_0624, 0xf40e_3585, 0x106a_a070,
407 0x19a4_c116, 0x1e37_6c08, 0x2748_774c, 0x34b0_bcb5, 0x391c_0cb3, 0x4ed8_aa4a,
408 0x5b9c_ca4f, 0x682e_6ff3, 0x748f_82ee, 0x78a5_636f, 0x84c8_7814, 0x8cc7_0208,
409 0x90be_fffa, 0xa450_6ceb, 0xbef9_a3f7, 0xc671_78f2,
410 ];
411 let mut h: [u32; 8] = [
412 0x6a09_e667, 0xbb67_ae85, 0x3c6e_f372, 0xa54f_f53a, 0x510e_527f, 0x9b05_688c,
413 0x1f83_d9ab, 0x5be0_cd19,
414 ];
415 let mut message = bytes.to_vec();
416 let bit_len = (bytes.len() as u64) * 8;
417 message.push(0x80);
418 while message.len() % 64 != 56 {
419 message.push(0);
420 }
421 message.extend_from_slice(&bit_len.to_be_bytes());
422
423 for chunk in message.chunks_exact(64) {
424 let mut w = [0u32; 64];
425 for (index, word) in w.iter_mut().enumerate().take(16) {
426 let base = index * 4;
427 *word = u32::from_be_bytes([
428 chunk[base],
429 chunk[base + 1],
430 chunk[base + 2],
431 chunk[base + 3],
432 ]);
433 }
434 for index in 16..64 {
435 let s0 = w[index - 15].rotate_right(7)
436 ^ w[index - 15].rotate_right(18)
437 ^ (w[index - 15] >> 3);
438 let s1 = w[index - 2].rotate_right(17)
439 ^ w[index - 2].rotate_right(19)
440 ^ (w[index - 2] >> 10);
441 w[index] = w[index - 16]
442 .wrapping_add(s0)
443 .wrapping_add(w[index - 7])
444 .wrapping_add(s1);
445 }
446 let mut v = h;
447 for index in 0..64 {
448 let s1 = v[4].rotate_right(6) ^ v[4].rotate_right(11) ^ v[4].rotate_right(25);
449 let ch = (v[4] & v[5]) ^ ((!v[4]) & v[6]);
450 let temp1 = v[7]
451 .wrapping_add(s1)
452 .wrapping_add(ch)
453 .wrapping_add(K[index])
454 .wrapping_add(w[index]);
455 let s0 = v[0].rotate_right(2) ^ v[0].rotate_right(13) ^ v[0].rotate_right(22);
456 let maj = (v[0] & v[1]) ^ (v[0] & v[2]) ^ (v[1] & v[2]);
457 let temp2 = s0.wrapping_add(maj);
458 v[7] = v[6];
459 v[6] = v[5];
460 v[5] = v[4];
461 v[4] = v[3].wrapping_add(temp1);
462 v[3] = v[2];
463 v[2] = v[1];
464 v[1] = v[0];
465 v[0] = temp1.wrapping_add(temp2);
466 }
467 for (slot, value) in h.iter_mut().zip(v) {
468 *slot = slot.wrapping_add(value);
469 }
470 }
471 scratch.clear();
472 let mut out = String::with_capacity(64);
473 for word in h {
474 out.push_str(&format!("{word:08x}"));
475 }
476 out
477 }
478
479 #[test]
480 fn the_canonical_fixture_admits_with_both_operators_from_the_record() {
481 let (fixture, binding) = load(CANONICAL);
482 assert_eq!(fixture.fixture, COMMUNITY_INDEPENDENT_VERIFICATION_SCHEMA);
483 assert_eq!(fixture.packet, COMMUNITY_INDEPENDENT_VERIFICATION_PACKET);
484 assert_eq!(fixture.expect, "admit");
485
486 let admitted = admit_independent_verification(&fixture.event, &binding)
487 .expect("the canonical verification is admitted");
488 assert_eq!(admitted.verifier_agent_pubkey, fixture.event.pubkey);
489 assert_ne!(
490 admitted.verifier_operator_pubkey,
491 admitted.producer_operator_pubkey
492 );
493 assert_eq!(admitted.verdict, VerificationVerdict::Reproduced);
494 assert_eq!(
495 admitted.verifier_operator_pubkey,
496 fixture.expected["verifierOperatorPubkey"].as_str().unwrap()
497 );
498 assert_eq!(
499 admitted.producer_operator_pubkey,
500 fixture.expected["producerOperatorPubkey"].as_str().unwrap()
501 );
502 }
503
504 /// The exact reason the amendment exists: the verifier signs, or nobody
505 /// did.
506 #[test]
507 fn a_verification_signed_by_somebody_other_than_the_verifier_is_refused() {
508 let (fixture, binding) = load(NOT_AUTHOR);
509 let refusal = admit_independent_verification(&fixture.event, &binding)
510 .expect_err("an impostor signature is refused");
511 assert_eq!(refusal, VerificationRefusal::VerifierNotAuthor);
512 assert_eq!(refusal.token(), fixture.expected["code"].as_str().unwrap());
513 }
514
515 #[test]
516 fn distinct_keys_under_one_operator_are_not_independent() {
517 let (fixture, binding) = load(NOT_INDEPENDENT);
518 let refusal = admit_independent_verification(&fixture.event, &binding)
519 .expect_err("one operator on both sides is self-dealing");
520 assert_eq!(refusal, VerificationRefusal::SelfDealingOperators);
521 assert_eq!(refusal.token(), fixture.expected["code"].as_str().unwrap());
522 }
523
524 /// A revocation binds the subject regardless of arrival order, and it binds
525 /// it whatever the key goes on to sign.
526 #[test]
527 fn a_burned_verifier_key_is_refused_even_with_a_live_binding_row() {
528 let (fixture, binding) = load(BURNED);
529 assert!(
530 binding.operator_for_agent(&fixture.event.pubkey).is_some(),
531 "the burn must be what refuses this, not a missing binding"
532 );
533 let refusal = admit_independent_verification(&fixture.event, &binding)
534 .expect_err("a burned key verifies nothing");
535 assert_eq!(refusal, VerificationRefusal::VerifierKeyBurned);
536 }
537
538 /// `cw_verifier_operator_ref` is the verifier's own claim about itself.
539 /// Believing it would rebuild the self-dealing hole on the new carrier.
540 #[test]
541 fn a_verifier_claiming_an_operator_the_record_denies_is_refused() {
542 let (mut fixture, binding) = load(CANONICAL);
543 for tag in &mut fixture.event.tags {
544 if tag.first().map(String::as_str) == Some("cw_verifier_operator_ref") {
545 tag[1] = "5".repeat(64);
546 }
547 }
548 let refusal = admit_independent_verification(&fixture.event, &binding)
549 .expect_err("a self-asserted operator is not a binding");
550 assert_eq!(refusal, VerificationRefusal::VerifierBindingUnconfirmed);
551 }
552
553 /// This event is testimony, not a verdict. A verification that could say
554 /// `accepted` would be a second acceptance authority.
555 #[test]
556 fn a_verification_cannot_carry_sarahs_words() {
557 assert!(VerificationVerdict::parse_token("accepted").is_none());
558 assert!(VerificationVerdict::parse_token("rejected").is_none());
559 let (mut fixture, binding) = load(CANONICAL);
560 for tag in &mut fixture.event.tags {
561 if tag.first().map(String::as_str) == Some("status") {
562 tag[1] = "accepted".to_owned();
563 }
564 }
565 assert_eq!(
566 admit_independent_verification(&fixture.event, &binding),
567 Err(VerificationRefusal::Malformed)
568 );
569 }
570
571 #[test]
572 fn a_verification_that_does_not_disclaim_payment_is_refused() {
573 let (mut fixture, binding) = load(CANONICAL);
574 for tag in &mut fixture.event.tags {
575 if tag.first().map(String::as_str) == Some("cw_decides_payment") {
576 tag[1] = "true".to_owned();
577 }
578 }
579 assert_eq!(
580 admit_independent_verification(&fixture.event, &binding),
581 Err(VerificationRefusal::DecidesPaymentForbidden)
582 );
583 }
584}
585