def test_id_collision(self): # test replacement case where tubid equals a keyid (one should # not replace the other) i = IntroducerService() ic = IntroducerClient(None, "introducer.furl", u"my_nickname", "my_version", "oldest_version", {}) sk_s, vk_s = keyutil.make_keypair() sk, _ignored = keyutil.parse_privkey(sk_s) keyid = keyutil.remove_prefix(vk_s, "pub-v0-") furl1 = "pb://[email protected]:123/short" # base32("short") ann_t = ic.create_announcement("storage", make_ann(furl1), sk) i.remote_publish_v2(ann_t, Referenceable()) announcements = i.get_announcements() self.failUnlessEqual(len(announcements), 1) key1 = ("storage", "v0-"+keyid, None) self.failUnlessEqual(announcements[0].index, key1) ann1_out = announcements[0].announcement self.failUnlessEqual(ann1_out["anonymous-storage-FURL"], furl1) furl2 = "pb://%[email protected]:36106/swissnum" % keyid ann2 = (furl2, "storage", "RIStorage", "nick1", "ver23", "ver0") i.remote_publish(ann2) announcements = i.get_announcements() self.failUnlessEqual(len(announcements), 2) key2 = ("storage", None, keyid) wanted = [ad for ad in announcements if ad.index == key2] self.failUnlessEqual(len(wanted), 1) ann2_out = wanted[0].announcement self.failUnlessEqual(ann2_out["anonymous-storage-FURL"], furl2)
def test_id_collision(self): # test replacement case where tubid equals a keyid (one should # not replace the other) i = IntroducerService() ic = IntroducerClient(None, "introducer.furl", u"my_nickname", "my_version", "oldest_version", {}) sk_s, vk_s = keyutil.make_keypair() sk, _ignored = keyutil.parse_privkey(sk_s) keyid = keyutil.remove_prefix(vk_s, "pub-v0-") furl1 = "pb://[email protected]:123/short" # base32("short") ann_t = ic.create_announcement("storage", make_ann(furl1), sk) i.remote_publish_v2(ann_t, Referenceable()) announcements = i.get_announcements() self.failUnlessEqual(len(announcements), 1) key1 = ("storage", "v0-" + keyid, None) self.failUnlessEqual(announcements[0].index, key1) ann1_out = announcements[0].announcement self.failUnlessEqual(ann1_out["anonymous-storage-FURL"], furl1) furl2 = "pb://%[email protected]:36106/swissnum" % keyid ann2 = (furl2, "storage", "RIStorage", "nick1", "ver23", "ver0") i.remote_publish(ann2) announcements = i.get_announcements() self.failUnlessEqual(len(announcements), 2) key2 = ("storage", None, keyid) wanted = [ad for ad in announcements if ad.index == key2] self.failUnlessEqual(len(wanted), 1) ann2_out = wanted[0].announcement self.failUnlessEqual(ann2_out["anonymous-storage-FURL"], furl2)
def unsign_from_foolscap(ann_t): (msg, sig_vs, claimed_key_vs) = ann_t if not sig_vs or not claimed_key_vs: raise UnknownKeyError("only signed announcements recognized") if not sig_vs.startswith("v0-"): raise UnknownKeyError("only v0- signatures recognized") if not claimed_key_vs.startswith("v0-"): raise UnknownKeyError("only v0- keys recognized") claimed_key = keyutil.parse_pubkey("pub-" + claimed_key_vs) sig_bytes = base32.a2b(keyutil.remove_prefix(sig_vs, "v0-")) claimed_key.verify(sig_bytes, msg) key_vs = claimed_key_vs ann = json.loads(msg.decode("utf-8")) return (ann, key_vs)
def unsign_from_foolscap(ann_t): (msg, sig_vs, claimed_key_vs) = ann_t if not sig_vs or not claimed_key_vs: raise UnknownKeyError("only signed announcements recognized") if not sig_vs.startswith("v0-"): raise UnknownKeyError("only v0- signatures recognized") if not claimed_key_vs.startswith("v0-"): raise UnknownKeyError("only v0- keys recognized") claimed_key = keyutil.parse_pubkey("pub-"+claimed_key_vs) sig_bytes = base32.a2b(keyutil.remove_prefix(sig_vs, "v0-")) claimed_key.verify(sig_bytes, msg) key_vs = claimed_key_vs ann = json.loads(msg.decode("utf-8")) return (ann, key_vs)
def test_id_collision(self): # test replacement case where tubid equals a keyid (one should # not replace the other) ic = IntroducerClient(None, "introducer.furl", u"my_nickname", "my_version", "oldest_version", {}) announcements = [] ic.subscribe_to("storage", lambda key_s, ann: announcements.append(ann)) sk_s, vk_s = keyutil.make_keypair() sk, _ignored = keyutil.parse_privkey(sk_s) keyid = keyutil.remove_prefix(vk_s, "pub-v0-") furl1 = "pb://[email protected]:123/short" # base32("short") furl2 = "pb://%[email protected]:36106/swissnum" % keyid ann_t = ic.create_announcement("storage", make_ann(furl1), sk) ic.remote_announce_v2([ann_t]) d = fireEventually() def _then(ign): # first announcement has been processed self.failUnlessEqual(len(announcements), 1) self.failUnlessEqual(announcements[0]["anonymous-storage-FURL"], furl1) # now submit a second one, with a tubid that happens to look just # like the pubkey-based serverid we just processed. They should # not overlap. ann2 = (furl2, "storage", "RIStorage", "nick1", "ver23", "ver0") ca = WrapV2ClientInV1Interface(ic) ca.remote_announce([ann2]) return fireEventually() d.addCallback(_then) def _then2(ign): # if they overlapped, the second announcement would be ignored self.failUnlessEqual(len(announcements), 2) self.failUnlessEqual(announcements[1]["anonymous-storage-FURL"], furl2) d.addCallback(_then2) return d
def test_client_v2_signed(self): introducer = IntroducerService() tub = introducer_furl = None app_versions = {"whizzy": "fizzy"} client_v2 = IntroducerClient(tub, introducer_furl, u"nick-v2", "my_version", "oldest", app_versions) furl1 = "pb://[email protected]:0/swissnum" sk_s, vk_s = keyutil.make_keypair() sk, _ignored = keyutil.parse_privkey(sk_s) pks = keyutil.remove_prefix(vk_s, "pub-") ann_t0 = make_ann_t(client_v2, furl1, sk) canary0 = Referenceable() introducer.remote_publish_v2(ann_t0, canary0) a = introducer.get_announcements() self.failUnlessEqual(len(a), 1) self.failUnlessIdentical(a[0].canary, canary0) self.failUnlessEqual(a[0].index, ("storage", pks, None)) self.failUnlessEqual(a[0].announcement["app-versions"], app_versions) self.failUnlessEqual(a[0].nickname, u"nick-v2") self.failUnlessEqual(a[0].service_name, "storage") self.failUnlessEqual(a[0].version, "my_version") self.failUnlessEqual(a[0].announcement["anonymous-storage-FURL"], furl1)
def test_client_v2_signed(self): introducer = IntroducerService() tub = introducer_furl = None app_versions = {"whizzy": "fizzy"} client_v2 = IntroducerClient(tub, introducer_furl, u"nick-v2", "my_version", "oldest", app_versions, fakeseq, FilePath(self.mktemp())) furl1 = "pb://[email protected]:0/swissnum" sk_s, vk_s = keyutil.make_keypair() sk, _ignored = keyutil.parse_privkey(sk_s) pks = keyutil.remove_prefix(vk_s, "pub-") ann_t0 = make_ann_t(client_v2, furl1, sk, 10) canary0 = Referenceable() introducer.remote_publish_v2(ann_t0, canary0) a = introducer.get_announcements() self.failUnlessEqual(len(a), 1) self.failUnlessIdentical(a[0].canary, canary0) self.failUnlessEqual(a[0].index, ("storage", pks)) self.failUnlessEqual(a[0].announcement["app-versions"], app_versions) self.failUnlessEqual(a[0].nickname, u"nick-v2") self.failUnlessEqual(a[0].service_name, "storage") self.failUnlessEqual(a[0].version, "my_version") self.failUnlessEqual(a[0].announcement["anonymous-storage-FURL"], furl1)
def test_id_collision(self): # test replacement case where tubid equals a keyid (one should # not replace the other) ic = IntroducerClient(None, "introducer.furl", u"my_nickname", "my_version", "oldest_version", {}) announcements = [] ic.subscribe_to("storage", lambda key_s,ann: announcements.append(ann)) sk_s, vk_s = keyutil.make_keypair() sk, _ignored = keyutil.parse_privkey(sk_s) keyid = keyutil.remove_prefix(vk_s, "pub-v0-") furl1 = "pb://[email protected]:123/short" # base32("short") furl2 = "pb://%[email protected]:36106/swissnum" % keyid ann_t = ic.create_announcement("storage", make_ann(furl1), sk) ic.remote_announce_v2([ann_t]) d = fireEventually() def _then(ign): # first announcement has been processed self.failUnlessEqual(len(announcements), 1) self.failUnlessEqual(announcements[0]["anonymous-storage-FURL"], furl1) # now submit a second one, with a tubid that happens to look just # like the pubkey-based serverid we just processed. They should # not overlap. ann2 = (furl2, "storage", "RIStorage", "nick1", "ver23", "ver0") ca = WrapV2ClientInV1Interface(ic) ca.remote_announce([ann2]) return fireEventually() d.addCallback(_then) def _then2(ign): # if they overlapped, the second announcement would be ignored self.failUnlessEqual(len(announcements), 2) self.failUnlessEqual(announcements[1]["anonymous-storage-FURL"], furl2) d.addCallback(_then2) return d
def test_client_cache(self): basedir = "introducer/ClientSeqnums/test_client_cache_1" fileutil.make_dirs(basedir) cache_filepath = FilePath(os.path.join(basedir, "private", "introducer_default_cache.yaml")) # if storage is enabled, the Client will publish its storage server # during startup (although the announcement will wait in a queue # until the introducer connection is established). To avoid getting # confused by this, disable storage. f = open(os.path.join(basedir, "tahoe.cfg"), "w") f.write("[client]\n") f.write("introducer.furl = nope\n") f.write("[storage]\n") f.write("enabled = false\n") f.close() c = TahoeClient(basedir) ic = c.introducer_clients[0] sk_s, vk_s = keyutil.make_keypair() sk, _ignored = keyutil.parse_privkey(sk_s) pub1 = keyutil.remove_prefix(vk_s, "pub-") furl1 = "pb://[email protected]:123/short" # base32("short") ann_t = make_ann_t(ic, furl1, sk, 1) ic.got_announcements([ann_t]) yield flushEventualQueue() # check the cache for the announcement announcements = self._load_cache(cache_filepath) self.failUnlessEqual(len(announcements), 1) self.failUnlessEqual(announcements[0]['key_s'], pub1) ann = announcements[0]["ann"] self.failUnlessEqual(ann["anonymous-storage-FURL"], furl1) self.failUnlessEqual(ann["seqnum"], 1) # a new announcement that replaces the first should replace the # cached entry, not duplicate it furl2 = furl1 + "er" ann_t2 = make_ann_t(ic, furl2, sk, 2) ic.got_announcements([ann_t2]) yield flushEventualQueue() announcements = self._load_cache(cache_filepath) self.failUnlessEqual(len(announcements), 1) self.failUnlessEqual(announcements[0]['key_s'], pub1) ann = announcements[0]["ann"] self.failUnlessEqual(ann["anonymous-storage-FURL"], furl2) self.failUnlessEqual(ann["seqnum"], 2) # but a third announcement with a different key should add to the # cache sk_s2, vk_s2 = keyutil.make_keypair() sk2, _ignored = keyutil.parse_privkey(sk_s2) pub2 = keyutil.remove_prefix(vk_s2, "pub-") furl3 = "pb://[email protected]:456/short" ann_t3 = make_ann_t(ic, furl3, sk2, 1) ic.got_announcements([ann_t3]) yield flushEventualQueue() announcements = self._load_cache(cache_filepath) self.failUnlessEqual(len(announcements), 2) self.failUnlessEqual(set([pub1, pub2]), set([a["key_s"] for a in announcements])) self.failUnlessEqual(set([furl2, furl3]), set([a["ann"]["anonymous-storage-FURL"] for a in announcements])) # test loading yield flushEventualQueue() ic2 = IntroducerClient(None, "introducer.furl", u"my_nickname", "my_version", "oldest_version", {}, fakeseq, ic._cache_filepath) announcements = {} def got(key_s, ann): announcements[key_s] = ann ic2.subscribe_to("storage", got) ic2._load_announcements() # normally happens when connection fails yield flushEventualQueue() self.failUnless(pub1 in announcements) self.failUnlessEqual(announcements[pub1]["anonymous-storage-FURL"], furl2) self.failUnlessEqual(announcements[pub2]["anonymous-storage-FURL"], furl3) c2 = TahoeClient(basedir) c2.introducer_clients[0]._load_announcements() yield flushEventualQueue() self.assertEqual(c2.storage_broker.get_all_serverids(), frozenset([pub1, pub2]))
def test_duplicate_receive_v2(self): ic1 = IntroducerClient(None, "introducer.furl", u"my_nickname", "ver23", "oldest_version", {}, fakeseq, FilePath(self.mktemp())) # we use a second client just to create a different-looking # announcement ic2 = IntroducerClient(None, "introducer.furl", u"my_nickname", "ver24","oldest_version",{}, fakeseq, FilePath(self.mktemp())) announcements = [] def _received(key_s, ann): announcements.append( (key_s, ann) ) ic1.subscribe_to("storage", _received) furl1 = "pb://[email protected]:36106/gydnp" furl1a = "pb://[email protected]:7777/gydnp" furl2 = "pb://[email protected]:36106/ttwwoo" privkey_s, pubkey_vs = keyutil.make_keypair() privkey, _ignored = keyutil.parse_privkey(privkey_s) pubkey_s = keyutil.remove_prefix(pubkey_vs, "pub-") # ann1: ic1, furl1 # ann1a: ic1, furl1a (same SturdyRef, different connection hints) # ann1b: ic2, furl1 # ann2: ic2, furl2 self.ann1 = make_ann_t(ic1, furl1, privkey, seqnum=10) self.ann1old = make_ann_t(ic1, furl1, privkey, seqnum=9) self.ann1noseqnum = make_ann_t(ic1, furl1, privkey, seqnum=None) self.ann1b = make_ann_t(ic2, furl1, privkey, seqnum=11) self.ann1a = make_ann_t(ic1, furl1a, privkey, seqnum=12) self.ann2 = make_ann_t(ic2, furl2, privkey, seqnum=13) ic1.remote_announce_v2([self.ann1]) # queues eventual-send d = fireEventually() def _then1(ign): self.failUnlessEqual(len(announcements), 1) key_s,ann = announcements[0] self.failUnlessEqual(key_s, pubkey_s) self.failUnlessEqual(ann["anonymous-storage-FURL"], furl1) self.failUnlessEqual(ann["my-version"], "ver23") d.addCallback(_then1) # now send a duplicate announcement. This should not fire the # subscriber d.addCallback(lambda ign: ic1.remote_announce_v2([self.ann1])) d.addCallback(fireEventually) def _then2(ign): self.failUnlessEqual(len(announcements), 1) d.addCallback(_then2) # an older announcement shouldn't fire the subscriber either d.addCallback(lambda ign: ic1.remote_announce_v2([self.ann1old])) d.addCallback(fireEventually) def _then2a(ign): self.failUnlessEqual(len(announcements), 1) d.addCallback(_then2a) # announcement with no seqnum cannot replace one with-seqnum d.addCallback(lambda ign: ic1.remote_announce_v2([self.ann1noseqnum])) d.addCallback(fireEventually) def _then2b(ign): self.failUnlessEqual(len(announcements), 1) d.addCallback(_then2b) # and a replacement announcement: same FURL, new other stuff. The # subscriber *should* be fired. d.addCallback(lambda ign: ic1.remote_announce_v2([self.ann1b])) d.addCallback(fireEventually) def _then3(ign): self.failUnlessEqual(len(announcements), 2) key_s,ann = announcements[-1] self.failUnlessEqual(key_s, pubkey_s) self.failUnlessEqual(ann["anonymous-storage-FURL"], furl1) self.failUnlessEqual(ann["my-version"], "ver24") d.addCallback(_then3) # and a replacement announcement with a different FURL (it uses # different connection hints) d.addCallback(lambda ign: ic1.remote_announce_v2([self.ann1a])) d.addCallback(fireEventually) def _then4(ign): self.failUnlessEqual(len(announcements), 3) key_s,ann = announcements[-1] self.failUnlessEqual(key_s, pubkey_s) self.failUnlessEqual(ann["anonymous-storage-FURL"], furl1a) self.failUnlessEqual(ann["my-version"], "ver23") d.addCallback(_then4) # now add a new subscription, which should be called with the # backlog. The introducer only records one announcement per index, so # the backlog will only have the latest message. announcements2 = [] def _received2(key_s, ann): announcements2.append( (key_s, ann) ) d.addCallback(lambda ign: ic1.subscribe_to("storage", _received2)) d.addCallback(fireEventually) def _then5(ign): self.failUnlessEqual(len(announcements2), 1) key_s,ann = announcements2[-1] self.failUnlessEqual(key_s, pubkey_s) self.failUnlessEqual(ann["anonymous-storage-FURL"], furl1a) self.failUnlessEqual(ann["my-version"], "ver23") d.addCallback(_then5) return d
def test_client_cache(self): basedir = "introducer/ClientSeqnums/test_client_cache_1" fileutil.make_dirs(basedir) cache_filepath = FilePath( os.path.join(basedir, "private", "introducer_default_cache.yaml")) # if storage is enabled, the Client will publish its storage server # during startup (although the announcement will wait in a queue # until the introducer connection is established). To avoid getting # confused by this, disable storage. f = open(os.path.join(basedir, "tahoe.cfg"), "w") f.write("[client]\n") f.write("introducer.furl = nope\n") f.write("[storage]\n") f.write("enabled = false\n") f.close() c = create_client(basedir) ic = c.introducer_clients[0] sk_s, vk_s = keyutil.make_keypair() sk, _ignored = keyutil.parse_privkey(sk_s) pub1 = keyutil.remove_prefix(vk_s, "pub-") furl1 = "pb://[email protected]:123/short" # base32("short") ann_t = make_ann_t(ic, furl1, sk, 1) ic.got_announcements([ann_t]) yield flushEventualQueue() # check the cache for the announcement announcements = self._load_cache(cache_filepath) self.failUnlessEqual(len(announcements), 1) self.failUnlessEqual(announcements[0]['key_s'], pub1) ann = announcements[0]["ann"] self.failUnlessEqual(ann["anonymous-storage-FURL"], furl1) self.failUnlessEqual(ann["seqnum"], 1) # a new announcement that replaces the first should replace the # cached entry, not duplicate it furl2 = furl1 + "er" ann_t2 = make_ann_t(ic, furl2, sk, 2) ic.got_announcements([ann_t2]) yield flushEventualQueue() announcements = self._load_cache(cache_filepath) self.failUnlessEqual(len(announcements), 1) self.failUnlessEqual(announcements[0]['key_s'], pub1) ann = announcements[0]["ann"] self.failUnlessEqual(ann["anonymous-storage-FURL"], furl2) self.failUnlessEqual(ann["seqnum"], 2) # but a third announcement with a different key should add to the # cache sk_s2, vk_s2 = keyutil.make_keypair() sk2, _ignored = keyutil.parse_privkey(sk_s2) pub2 = keyutil.remove_prefix(vk_s2, "pub-") furl3 = "pb://[email protected]:456/short" ann_t3 = make_ann_t(ic, furl3, sk2, 1) ic.got_announcements([ann_t3]) yield flushEventualQueue() announcements = self._load_cache(cache_filepath) self.failUnlessEqual(len(announcements), 2) self.failUnlessEqual(set([pub1, pub2]), set([a["key_s"] for a in announcements])) self.failUnlessEqual( set([furl2, furl3]), set([a["ann"]["anonymous-storage-FURL"] for a in announcements])) # test loading yield flushEventualQueue() ic2 = IntroducerClient(None, "introducer.furl", u"my_nickname", "my_version", "oldest_version", {}, fakeseq, ic._cache_filepath) announcements = {} def got(key_s, ann): announcements[key_s] = ann ic2.subscribe_to("storage", got) ic2._load_announcements() # normally happens when connection fails yield flushEventualQueue() self.failUnless(pub1 in announcements) self.failUnlessEqual(announcements[pub1]["anonymous-storage-FURL"], furl2) self.failUnlessEqual(announcements[pub2]["anonymous-storage-FURL"], furl3) c2 = create_client(basedir) c2.introducer_clients[0]._load_announcements() yield flushEventualQueue() self.assertEqual(c2.storage_broker.get_all_serverids(), frozenset([pub1, pub2]))
def test_duplicate_receive_v2(self): ic1 = IntroducerClient(None, "introducer.furl", u"my_nickname", "ver23", "oldest_version", {}, fakeseq, FilePath(self.mktemp())) # we use a second client just to create a different-looking # announcement ic2 = IntroducerClient(None, "introducer.furl", u"my_nickname", "ver24", "oldest_version", {}, fakeseq, FilePath(self.mktemp())) announcements = [] def _received(key_s, ann): announcements.append((key_s, ann)) ic1.subscribe_to("storage", _received) furl1 = "pb://[email protected]:36106/gydnp" furl1a = "pb://[email protected]:7777/gydnp" furl2 = "pb://[email protected]:36106/ttwwoo" privkey_s, pubkey_vs = keyutil.make_keypair() privkey, _ignored = keyutil.parse_privkey(privkey_s) pubkey_s = keyutil.remove_prefix(pubkey_vs, "pub-") # ann1: ic1, furl1 # ann1a: ic1, furl1a (same SturdyRef, different connection hints) # ann1b: ic2, furl1 # ann2: ic2, furl2 self.ann1 = make_ann_t(ic1, furl1, privkey, seqnum=10) self.ann1old = make_ann_t(ic1, furl1, privkey, seqnum=9) self.ann1noseqnum = make_ann_t(ic1, furl1, privkey, seqnum=None) self.ann1b = make_ann_t(ic2, furl1, privkey, seqnum=11) self.ann1a = make_ann_t(ic1, furl1a, privkey, seqnum=12) self.ann2 = make_ann_t(ic2, furl2, privkey, seqnum=13) ic1.remote_announce_v2([self.ann1]) # queues eventual-send d = fireEventually() def _then1(ign): self.failUnlessEqual(len(announcements), 1) key_s, ann = announcements[0] self.failUnlessEqual(key_s, pubkey_s) self.failUnlessEqual(ann["anonymous-storage-FURL"], furl1) self.failUnlessEqual(ann["my-version"], "ver23") d.addCallback(_then1) # now send a duplicate announcement. This should not fire the # subscriber d.addCallback(lambda ign: ic1.remote_announce_v2([self.ann1])) d.addCallback(fireEventually) def _then2(ign): self.failUnlessEqual(len(announcements), 1) d.addCallback(_then2) # an older announcement shouldn't fire the subscriber either d.addCallback(lambda ign: ic1.remote_announce_v2([self.ann1old])) d.addCallback(fireEventually) def _then2a(ign): self.failUnlessEqual(len(announcements), 1) d.addCallback(_then2a) # announcement with no seqnum cannot replace one with-seqnum d.addCallback(lambda ign: ic1.remote_announce_v2([self.ann1noseqnum])) d.addCallback(fireEventually) def _then2b(ign): self.failUnlessEqual(len(announcements), 1) d.addCallback(_then2b) # and a replacement announcement: same FURL, new other stuff. The # subscriber *should* be fired. d.addCallback(lambda ign: ic1.remote_announce_v2([self.ann1b])) d.addCallback(fireEventually) def _then3(ign): self.failUnlessEqual(len(announcements), 2) key_s, ann = announcements[-1] self.failUnlessEqual(key_s, pubkey_s) self.failUnlessEqual(ann["anonymous-storage-FURL"], furl1) self.failUnlessEqual(ann["my-version"], "ver24") d.addCallback(_then3) # and a replacement announcement with a different FURL (it uses # different connection hints) d.addCallback(lambda ign: ic1.remote_announce_v2([self.ann1a])) d.addCallback(fireEventually) def _then4(ign): self.failUnlessEqual(len(announcements), 3) key_s, ann = announcements[-1] self.failUnlessEqual(key_s, pubkey_s) self.failUnlessEqual(ann["anonymous-storage-FURL"], furl1a) self.failUnlessEqual(ann["my-version"], "ver23") d.addCallback(_then4) # now add a new subscription, which should be called with the # backlog. The introducer only records one announcement per index, so # the backlog will only have the latest message. announcements2 = [] def _received2(key_s, ann): announcements2.append((key_s, ann)) d.addCallback(lambda ign: ic1.subscribe_to("storage", _received2)) d.addCallback(fireEventually) def _then5(ign): self.failUnlessEqual(len(announcements2), 1) key_s, ann = announcements2[-1] self.failUnlessEqual(key_s, pubkey_s) self.failUnlessEqual(ann["anonymous-storage-FURL"], furl1a) self.failUnlessEqual(ann["my-version"], "ver23") d.addCallback(_then5) return d