]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/blobdiff - src/allmydata/test/test_introducer.py
introweb: fix connection hints for server announcements
[tahoe-lafs/tahoe-lafs.git] / src / allmydata / test / test_introducer.py
index b8dd50b364049c1f75dbd97f200bfda133871631..be7d05ddc37b638f7f1652a9a9d7485de8f79c4e 100644 (file)
@@ -1,10 +1,10 @@
 
-import os, re
+import os, re, itertools
 from base64 import b32decode
 import simplejson
 
 from twisted.trial import unittest
-from twisted.internet import defer
+from twisted.internet import defer, address
 from twisted.python import log
 
 from foolscap.api import Tub, Referenceable, fireEventually, flushEventualQueue
@@ -12,32 +12,82 @@ from twisted.application import service
 from allmydata.interfaces import InsufficientVersionError
 from allmydata.introducer.client import IntroducerClient, \
      WrapV2ClientInV1Interface
-from allmydata.introducer.server import IntroducerService
+from allmydata.introducer.server import IntroducerService, FurlFileConflictError
 from allmydata.introducer.common import get_tubid_string_from_ann, \
      get_tubid_string, sign_to_foolscap, unsign_from_foolscap, \
      UnknownKeyError
 from allmydata.introducer import old
 # test compatibility with old introducer .tac files
 from allmydata.introducer import IntroducerNode
-from allmydata.util import pollmixin, keyutil
+from allmydata.web import introweb
+from allmydata.client import Client as TahoeClient
+from allmydata.util import pollmixin, keyutil, idlib, fileutil
 import allmydata.test.common_util as testutil
 
 class LoggingMultiService(service.MultiService):
     def log(self, msg, **kw):
         log.msg(msg, **kw)
 
-class Node(testutil.SignalMixin, unittest.TestCase):
-    def test_loadable(self):
-        basedir = "introducer.IntroducerNode.test_loadable"
+class Node(testutil.SignalMixin, testutil.ReallyEqualMixin, unittest.TestCase):
+    def test_furl(self):
+        basedir = "introducer.IntroducerNode.test_furl"
         os.mkdir(basedir)
-        q = IntroducerNode(basedir)
+        public_fn = os.path.join(basedir, "introducer.furl")
+        private_fn = os.path.join(basedir, "private", "introducer.furl")
+        q1 = IntroducerNode(basedir)
         d = fireEventually(None)
-        d.addCallback(lambda res: q.startService())
-        d.addCallback(lambda res: q.when_tub_ready())
-        d.addCallback(lambda res: q.stopService())
+        d.addCallback(lambda res: q1.startService())
+        d.addCallback(lambda res: q1.when_tub_ready())
+        d.addCallback(lambda res: q1.stopService())
         d.addCallback(flushEventualQueue)
+        def _check_furl(res):
+            # new nodes create unguessable furls in private/introducer.furl
+            ifurl = fileutil.read(private_fn)
+            self.failUnless(ifurl)
+            ifurl = ifurl.strip()
+            self.failIf(ifurl.endswith("/introducer"), ifurl)
+
+            # old nodes created guessable furls in BASEDIR/introducer.furl
+            guessable = ifurl[:ifurl.rfind("/")] + "/introducer"
+            fileutil.write(public_fn, guessable+"\n", mode="w") # text
+
+            # if we see both files, throw an error
+            self.failUnlessRaises(FurlFileConflictError,
+                                  IntroducerNode, basedir)
+
+            # when we see only the public one, move it to private/ and use
+            # the existing furl instead of creating a new one
+            os.unlink(private_fn)
+            q2 = IntroducerNode(basedir)
+            d2 = fireEventually(None)
+            d2.addCallback(lambda res: q2.startService())
+            d2.addCallback(lambda res: q2.when_tub_ready())
+            d2.addCallback(lambda res: q2.stopService())
+            d2.addCallback(flushEventualQueue)
+            def _check_furl2(res):
+                self.failIf(os.path.exists(public_fn))
+                ifurl2 = fileutil.read(private_fn)
+                self.failUnless(ifurl2)
+                self.failUnlessEqual(ifurl2.strip(), guessable)
+            d2.addCallback(_check_furl2)
+            return d2
+        d.addCallback(_check_furl)
         return d
 
+    def test_web_static(self):
+        basedir = u"introducer.Node.test_web_static"
+        os.mkdir(basedir)
+        fileutil.write(os.path.join(basedir, "tahoe.cfg"),
+                       "[node]\n" +
+                       "web.port = tcp:0:interface=127.0.0.1\n" +
+                       "web.static = relative\n")
+        c = IntroducerNode(basedir)
+        w = c.getServiceNamed("webish")
+        abs_basedir = fileutil.abspath_expanduser_unicode(basedir)
+        expected = fileutil.abspath_expanduser_unicode(u"relative", abs_basedir)
+        self.failUnlessReallyEqual(w.staticdir, expected)
+
+
 class ServiceMixin:
     def setUp(self):
         self.parent = LoggingMultiService()
@@ -53,7 +103,7 @@ class Introducer(ServiceMixin, unittest.TestCase, pollmixin.PollMixin):
 
     def test_create(self):
         ic = IntroducerClient(None, "introducer.furl", u"my_nickname",
-                              "my_version", "oldest_version", {})
+                              "my_version", "oldest_version", {}, fakeseq)
         self.failUnless(isinstance(ic, IntroducerClient))
 
     def test_listen(self):
@@ -85,43 +135,56 @@ class Introducer(ServiceMixin, unittest.TestCase, pollmixin.PollMixin):
         i = IntroducerService()
         ic = IntroducerClient(None,
                               "introducer.furl", u"my_nickname",
-                              "my_version", "oldest_version", {})
+                              "my_version", "oldest_version", {}, fakeseq)
         sk_s, vk_s = keyutil.make_keypair()
         sk, _ignored = keyutil.parse_privkey(sk_s)
         keyid = keyutil.remove_prefix(vk_s, "pub-v0-")
         furl1 = "pb://onug64tu@127.0.0.1:123/short" # base32("short")
-        ann_t = ic.create_announcement("storage", make_ann(furl1), sk)
+        ann_t = make_ann_t(ic, furl1, sk, 1)
         i.remote_publish_v2(ann_t, Referenceable())
         announcements = i.get_announcements()
         self.failUnlessEqual(len(announcements), 1)
         key1 = ("storage", "v0-"+keyid, None)
-        self.failUnless(key1 in announcements)
-        (ign, ign, ann1_out, ign) = announcements[key1]
+        self.failUnlessEqual(announcements[0].index, key1)
+        ann1_out = announcements[0].announcement
         self.failUnlessEqual(ann1_out["anonymous-storage-FURL"], furl1)
 
         furl2 = "pb://%s@127.0.0.1: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)
-        self.failUnless(key2 in announcements)
-        (ign, ign, ann2_out, ign) = announcements[key2]
+        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 fakeseq():
+    return 1, "nonce"
+
+seqnum_counter = itertools.count(1)
+def realseq():
+    return seqnum_counter.next(), str(os.randint(1,100000))
+
 def make_ann(furl):
     ann = { "anonymous-storage-FURL": furl,
             "permutation-seed-base32": get_tubid_string(furl) }
     return ann
 
-def make_ann_t(ic, furl, privkey):
-    return ic.create_announcement("storage", make_ann(furl), privkey)
+def make_ann_t(ic, furl, privkey, seqnum):
+    ann_d = ic.create_announcement_dict("storage", make_ann(furl))
+    ann_d["seqnum"] = seqnum
+    ann_d["nonce"] = "nonce"
+    ann_t = sign_to_foolscap(ann_d, privkey)
+    return ann_t
 
 class Client(unittest.TestCase):
     def test_duplicate_receive_v1(self):
         ic = IntroducerClient(None,
                               "introducer.furl", u"my_nickname",
-                              "my_version", "oldest_version", {})
+                              "my_version", "oldest_version", {}, fakeseq)
         announcements = []
         ic.subscribe_to("storage",
                         lambda key_s,ann: announcements.append(ann))
@@ -170,12 +233,12 @@ class Client(unittest.TestCase):
     def test_duplicate_receive_v2(self):
         ic1 = IntroducerClient(None,
                                "introducer.furl", u"my_nickname",
-                               "ver23", "oldest_version", {})
+                               "ver23", "oldest_version", {}, fakeseq)
         # we use a second client just to create a different-looking
         # announcement
         ic2 = IntroducerClient(None,
                                "introducer.furl", u"my_nickname",
-                               "ver24","oldest_version",{})
+                               "ver24","oldest_version",{}, fakeseq)
         announcements = []
         def _received(key_s, ann):
             announcements.append( (key_s, ann) )
@@ -193,10 +256,12 @@ class Client(unittest.TestCase):
         # ann1b: ic2, furl1
         # ann2: ic2, furl2
 
-        self.ann1 = make_ann_t(ic1, furl1, privkey)
-        self.ann1a = make_ann_t(ic1, furl1a, privkey)
-        self.ann1b = make_ann_t(ic2, furl1, privkey)
-        self.ann2 = make_ann_t(ic2, furl2, privkey)
+        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()
@@ -216,6 +281,20 @@ class Client(unittest.TestCase):
             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]))
@@ -262,7 +341,7 @@ class Client(unittest.TestCase):
         # not replace the other)
         ic = IntroducerClient(None,
                               "introducer.furl", u"my_nickname",
-                              "my_version", "oldest_version", {})
+                              "my_version", "oldest_version", {}, fakeseq)
         announcements = []
         ic.subscribe_to("storage",
                         lambda key_s,ann: announcements.append(ann))
@@ -271,7 +350,7 @@ class Client(unittest.TestCase):
         keyid = keyutil.remove_prefix(vk_s, "pub-v0-")
         furl1 = "pb://onug64tu@127.0.0.1:123/short" # base32("short")
         furl2 = "pb://%s@127.0.0.1:36106/swissnum" % keyid
-        ann_t = ic.create_announcement("storage", make_ann(furl1), sk)
+        ann_t = make_ann_t(ic, furl1, sk, 1)
         ic.remote_announce_v2([ann_t])
         d = fireEventually()
         def _then(ign):
@@ -295,6 +374,85 @@ class Client(unittest.TestCase):
         d.addCallback(_then2)
         return d
 
+class Server(unittest.TestCase):
+    def test_duplicate(self):
+        i = IntroducerService()
+        ic1 = IntroducerClient(None,
+                               "introducer.furl", u"my_nickname",
+                               "ver23", "oldest_version", {}, realseq)
+        furl1 = "pb://62ubehyunnyhzs7r6vdonnm2hpi52w6y@127.0.0.1:36106/gydnp"
+
+        privkey_s, _ = keyutil.make_keypair()
+        privkey, _ = keyutil.parse_privkey(privkey_s)
+
+        ann1 = make_ann_t(ic1, furl1, privkey, seqnum=10)
+        ann1_old = make_ann_t(ic1, furl1, privkey, seqnum=9)
+        ann1_new = make_ann_t(ic1, furl1, privkey, seqnum=11)
+        ann1_noseqnum = make_ann_t(ic1, furl1, privkey, seqnum=None)
+        ann1_badseqnum = make_ann_t(ic1, furl1, privkey, seqnum="not an int")
+
+        i.remote_publish_v2(ann1, None)
+        all = i.get_announcements()
+        self.failUnlessEqual(len(all), 1)
+        self.failUnlessEqual(all[0].announcement["seqnum"], 10)
+        self.failUnlessEqual(i._debug_counts["inbound_message"], 1)
+        self.failUnlessEqual(i._debug_counts["inbound_duplicate"], 0)
+        self.failUnlessEqual(i._debug_counts["inbound_no_seqnum"], 0)
+        self.failUnlessEqual(i._debug_counts["inbound_old_replay"], 0)
+        self.failUnlessEqual(i._debug_counts["inbound_update"], 0)
+
+        i.remote_publish_v2(ann1, None)
+        all = i.get_announcements()
+        self.failUnlessEqual(len(all), 1)
+        self.failUnlessEqual(all[0].announcement["seqnum"], 10)
+        self.failUnlessEqual(i._debug_counts["inbound_message"], 2)
+        self.failUnlessEqual(i._debug_counts["inbound_duplicate"], 1)
+        self.failUnlessEqual(i._debug_counts["inbound_no_seqnum"], 0)
+        self.failUnlessEqual(i._debug_counts["inbound_old_replay"], 0)
+        self.failUnlessEqual(i._debug_counts["inbound_update"], 0)
+
+        i.remote_publish_v2(ann1_old, None)
+        all = i.get_announcements()
+        self.failUnlessEqual(len(all), 1)
+        self.failUnlessEqual(all[0].announcement["seqnum"], 10)
+        self.failUnlessEqual(i._debug_counts["inbound_message"], 3)
+        self.failUnlessEqual(i._debug_counts["inbound_duplicate"], 1)
+        self.failUnlessEqual(i._debug_counts["inbound_no_seqnum"], 0)
+        self.failUnlessEqual(i._debug_counts["inbound_old_replay"], 1)
+        self.failUnlessEqual(i._debug_counts["inbound_update"], 0)
+
+        i.remote_publish_v2(ann1_new, None)
+        all = i.get_announcements()
+        self.failUnlessEqual(len(all), 1)
+        self.failUnlessEqual(all[0].announcement["seqnum"], 11)
+        self.failUnlessEqual(i._debug_counts["inbound_message"], 4)
+        self.failUnlessEqual(i._debug_counts["inbound_duplicate"], 1)
+        self.failUnlessEqual(i._debug_counts["inbound_no_seqnum"], 0)
+        self.failUnlessEqual(i._debug_counts["inbound_old_replay"], 1)
+        self.failUnlessEqual(i._debug_counts["inbound_update"], 1)
+
+        i.remote_publish_v2(ann1_noseqnum, None)
+        all = i.get_announcements()
+        self.failUnlessEqual(len(all), 1)
+        self.failUnlessEqual(all[0].announcement["seqnum"], 11)
+        self.failUnlessEqual(i._debug_counts["inbound_message"], 5)
+        self.failUnlessEqual(i._debug_counts["inbound_duplicate"], 1)
+        self.failUnlessEqual(i._debug_counts["inbound_no_seqnum"], 1)
+        self.failUnlessEqual(i._debug_counts["inbound_old_replay"], 1)
+        self.failUnlessEqual(i._debug_counts["inbound_update"], 1)
+
+        i.remote_publish_v2(ann1_badseqnum, None)
+        all = i.get_announcements()
+        self.failUnlessEqual(len(all), 1)
+        self.failUnlessEqual(all[0].announcement["seqnum"], 11)
+        self.failUnlessEqual(i._debug_counts["inbound_message"], 6)
+        self.failUnlessEqual(i._debug_counts["inbound_duplicate"], 1)
+        self.failUnlessEqual(i._debug_counts["inbound_no_seqnum"], 2)
+        self.failUnlessEqual(i._debug_counts["inbound_old_replay"], 1)
+        self.failUnlessEqual(i._debug_counts["inbound_update"], 1)
+
+
+NICKNAME = u"n\u00EDickname-%s" # LATIN SMALL LETTER I WITH ACUTE
 
 class SystemTestMixin(ServiceMixin, pollmixin.PollMixin):
 
@@ -323,7 +481,7 @@ class Queue(SystemTestMixin, unittest.TestCase):
         tub2 = Tub()
         tub2.setServiceParent(self.parent)
         c = IntroducerClient(tub2, ifurl,
-                             u"nickname", "version", "oldest", {})
+                             u"nickname", "version", "oldest", {}, fakeseq)
         furl1 = "pb://onug64tu@127.0.0.1:123/short" # base32("short")
         sk_s, vk_s = keyutil.make_keypair()
         sk, _ignored = keyutil.parse_privkey(sk_s)
@@ -342,9 +500,9 @@ class Queue(SystemTestMixin, unittest.TestCase):
             return self.poll(_got_announcement)
         d.addCallback(_offline)
         def _done(ign):
-            v = list(introducer.get_announcements().values())[0]
-            (ign, ign, ann1_out, ign) = v
-            self.failUnlessEqual(ann1_out["anonymous-storage-FURL"], furl1)
+            v = introducer.get_announcements()[0]
+            furl = v.announcement["anonymous-storage-FURL"]
+            self.failUnlessEqual(furl, furl1)
         d.addCallback(_done)
 
         # now let the ack get back
@@ -387,6 +545,8 @@ class SystemTest(SystemTestMixin, unittest.TestCase):
         received_announcements = {}
         subscribing_clients = []
         publishing_clients = []
+        printable_serverids = {}
+        self.the_introducer = introducer
         privkeys = {}
         expected_announcements = [0 for c in range(NUM_CLIENTS)]
 
@@ -403,13 +563,13 @@ class SystemTest(SystemTestMixin, unittest.TestCase):
             log.msg("creating client %d: %s" % (i, tub.getShortTubID()))
             if i == 0:
                 c = old.IntroducerClient_v1(tub, self.introducer_furl,
-                                            u"nickname-%d" % i,
+                                            NICKNAME % str(i),
                                             "version", "oldest")
             else:
                 c = IntroducerClient(tub, self.introducer_furl,
-                                     u"nickname-%d" % i,
+                                     NICKNAME % str(i),
                                      "version", "oldest",
-                                     {"component": "component-v1"})
+                                     {"component": "component-v1"}, fakeseq)
             received_announcements[c] = {}
             def got(key_s_or_tubid, ann, announcements, i):
                 if i == 0:
@@ -425,14 +585,21 @@ class SystemTest(SystemTestMixin, unittest.TestCase):
             if i < NUM_STORAGE:
                 if i == 0:
                     c.publish(node_furl, "storage", "ri_name")
+                    printable_serverids[i] = get_tubid_string(node_furl)
                 elif i == 1:
                     # sign the announcement
                     privkey_s, pubkey_s = keyutil.make_keypair()
                     privkey, _ignored = keyutil.parse_privkey(privkey_s)
                     privkeys[c] = privkey
                     c.publish("storage", make_ann(node_furl), privkey)
+                    if server_version == V1:
+                        printable_serverids[i] = get_tubid_string(node_furl)
+                    else:
+                        assert pubkey_s.startswith("pub-")
+                        printable_serverids[i] = pubkey_s[len("pub-"):]
                 else:
                     c.publish("storage", make_ann(node_furl))
+                    printable_serverids[i] = get_tubid_string(node_furl)
                 publishing_clients.append(c)
             else:
                 # the last one does not publish anything
@@ -483,7 +650,7 @@ class SystemTest(SystemTestMixin, unittest.TestCase):
                 for c in subscribing_clients + publishing_clients:
                     if c._debug_outstanding:
                         return False
-                if introducer._debug_outstanding:
+                if self.the_introducer._debug_outstanding:
                     return False
                 return True
             return self.poll(_idle)
@@ -495,7 +662,7 @@ class SystemTest(SystemTestMixin, unittest.TestCase):
 
         def _check1(res):
             log.msg("doing _check1")
-            dc = introducer._debug_counts
+            dc = self.the_introducer._debug_counts
             if server_version == V1:
                 # each storage server publishes a record, and (after its
                 # 'subscribe' has been ACKed) also publishes a "stub_client".
@@ -540,7 +707,7 @@ class SystemTest(SystemTestMixin, unittest.TestCase):
                 ann = anns[nodeid0]
                 nick = ann["nickname"]
                 self.failUnlessEqual(type(nick), unicode)
-                self.failUnlessEqual(nick, u"nickname-0")
+                self.failUnlessEqual(nick, NICKNAME % "0")
             if server_version == V1:
                 for c in publishing_clients:
                     cdc = c._debug_counts
@@ -565,6 +732,18 @@ class SystemTest(SystemTestMixin, unittest.TestCase):
                              ]:
                         expected = 2
                     self.failUnlessEqual(cdc["outbound_message"], expected)
+            # now check the web status, make sure it renders without error
+            ir = introweb.IntroducerRoot(self.parent)
+            self.parent.nodeid = "NODEID"
+            text = ir.renderSynchronously().decode("utf-8")
+            self.failUnlessIn(NICKNAME % "0", text) # the v1 client
+            self.failUnlessIn(NICKNAME % "1", text) # a v2 client
+            for i in range(NUM_STORAGE):
+                self.failUnlessIn(printable_serverids[i], text,
+                                  (i,printable_serverids[i],text))
+                # make sure there isn't a double-base32ed string too
+                self.failIfIn(idlib.nodeid_b2a(printable_serverids[i]), text,
+                              (i,printable_serverids[i],text))
             log.msg("_check1 done")
         d.addCallback(_check1)
 
@@ -594,11 +773,11 @@ class SystemTest(SystemTestMixin, unittest.TestCase):
                 c = subscribing_clients[i]
                 for k in c._debug_counts:
                     c._debug_counts[k] = 0
-            for k in introducer._debug_counts:
-                introducer._debug_counts[k] = 0
+            for k in self.the_introducer._debug_counts:
+                self.the_introducer._debug_counts[k] = 0
             expected_announcements[i] += 1 # new 'storage' for everyone
             self.create_tub(self.central_portnum)
-            newfurl = self.central_tub.registerReference(introducer,
+            newfurl = self.central_tub.registerReference(self.the_introducer,
                                                          furlFile=iff)
             assert newfurl == self.introducer_furl
         d.addCallback(_restart_introducer_tub)
@@ -614,7 +793,7 @@ class SystemTest(SystemTestMixin, unittest.TestCase):
             log.msg("doing _check2")
             # assert that the introducer sent out new messages, one per
             # subscriber
-            dc = introducer._debug_counts
+            dc = self.the_introducer._debug_counts
             self.failUnlessEqual(dc["outbound_announcements"],
                                  NUM_STORAGE*NUM_CLIENTS)
             self.failUnless(dc["outbound_message"] > 0)
@@ -652,7 +831,8 @@ class SystemTest(SystemTestMixin, unittest.TestCase):
                 introducer = old.IntroducerService_v1()
             else:
                 introducer = IntroducerService()
-            newfurl = self.central_tub.registerReference(introducer,
+            self.the_introducer = introducer
+            newfurl = self.central_tub.registerReference(self.the_introducer,
                                                          furlFile=iff)
             assert newfurl == self.introducer_furl
         d.addCallback(_restart_introducer)
@@ -663,7 +843,7 @@ class SystemTest(SystemTestMixin, unittest.TestCase):
 
         def _check3(res):
             log.msg("doing _check3")
-            dc = introducer._debug_counts
+            dc = self.the_introducer._debug_counts
             self.failUnlessEqual(dc["outbound_announcements"],
                                  NUM_STORAGE*NUM_CLIENTS)
             self.failUnless(dc["outbound_message"] > 0)
@@ -697,30 +877,32 @@ class SystemTest(SystemTestMixin, unittest.TestCase):
 class FakeRemoteReference:
     def notifyOnDisconnect(self, *args, **kwargs): pass
     def getRemoteTubID(self): return "62ubehyunnyhzs7r6vdonnm2hpi52w6y"
+    def getLocationHints(self): return ["tcp:here.example.com:1234",
+                                        "tcp:there.example.com2345"]
+    def getPeer(self): return address.IPv4Address("TCP", "remote.example.com",
+                                                  3456)
 
 class ClientInfo(unittest.TestCase):
     def test_client_v2(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)
+        client_v2 = IntroducerClient(tub, introducer_furl, NICKNAME % u"v2",
+                                     "my_version", "oldest", app_versions,
+                                     fakeseq)
         #furl1 = "pb://62ubehyunnyhzs7r6vdonnm2hpi52w6y@127.0.0.1:0/swissnum"
-        #ann_s = make_ann_t(client_v2, furl1, None)
+        #ann_s = make_ann_t(client_v2, furl1, None, 10)
         #introducer.remote_publish_v2(ann_s, Referenceable())
         subscriber = FakeRemoteReference()
         introducer.remote_subscribe_v2(subscriber, "storage",
                                        client_v2._my_subscriber_info)
-        s = introducer.get_subscribers()
-        self.failUnlessEqual(len(s), 1)
-        sn, when, si, rref = s[0]
-        self.failUnlessIdentical(rref, subscriber)
-        self.failUnlessEqual(sn, "storage")
-        self.failUnlessEqual(si["version"], 0)
-        self.failUnlessEqual(si["oldest-supported"], "oldest")
-        self.failUnlessEqual(si["app-versions"], app_versions)
-        self.failUnlessEqual(si["nickname"], u"nick-v2")
-        self.failUnlessEqual(si["my-version"], "my_version")
+        subs = introducer.get_subscribers()
+        self.failUnlessEqual(len(subs), 1)
+        s0 = subs[0]
+        self.failUnlessEqual(s0.service_name, "storage")
+        self.failUnlessEqual(s0.app_versions, app_versions)
+        self.failUnlessEqual(s0.nickname, NICKNAME % u"v2")
+        self.failUnlessEqual(s0.version, "my_version")
 
     def test_client_v1(self):
         introducer = IntroducerService()
@@ -728,50 +910,39 @@ class ClientInfo(unittest.TestCase):
         introducer.remote_subscribe(subscriber, "storage")
         # the v1 subscribe interface had no subscriber_info: that was usually
         # sent in a separate stub_client pseudo-announcement
-        s = introducer.get_subscribers()
-        self.failUnlessEqual(len(s), 1)
-        sn, when, si, rref = s[0]
-        # rref will be a WrapV1SubscriberInV2Interface around the real
-        # subscriber
-        self.failUnlessIdentical(rref.original, subscriber)
-        self.failUnlessEqual(si, None) # not known yet
-        self.failUnlessEqual(sn, "storage")
+        subs = introducer.get_subscribers()
+        self.failUnlessEqual(len(subs), 1)
+        s0 = subs[0]
+        self.failUnlessEqual(s0.nickname, u"?") # not known yet
+        self.failUnlessEqual(s0.service_name, "storage")
 
         # now submit the stub_client announcement
         furl1 = "pb://62ubehyunnyhzs7r6vdonnm2hpi52w6y@127.0.0.1:0/swissnum"
         ann = (furl1, "stub_client", "RIStubClient",
-               u"nick-v1".encode("utf-8"), "my_version", "oldest")
+               (NICKNAME % u"v1").encode("utf-8"), "my_version", "oldest")
         introducer.remote_publish(ann)
         # the server should correlate the two
-        s = introducer.get_subscribers()
-        self.failUnlessEqual(len(s), 1)
-        sn, when, si, rref = s[0]
-        self.failUnlessIdentical(rref.original, subscriber)
-        self.failUnlessEqual(sn, "storage")
-
-        self.failUnlessEqual(si["version"], 0)
-        self.failUnlessEqual(si["oldest-supported"], "oldest")
+        subs = introducer.get_subscribers()
+        self.failUnlessEqual(len(subs), 1)
+        s0 = subs[0]
+        self.failUnlessEqual(s0.service_name, "storage")
         # v1 announcements do not contain app-versions
-        self.failUnlessEqual(si["app-versions"], {})
-        self.failUnlessEqual(si["nickname"], u"nick-v1")
-        self.failUnlessEqual(si["my-version"], "my_version")
+        self.failUnlessEqual(s0.app_versions, {})
+        self.failUnlessEqual(s0.nickname, NICKNAME % u"v1")
+        self.failUnlessEqual(s0.version, "my_version")
 
         # a subscription that arrives after the stub_client announcement
         # should be correlated too
         subscriber2 = FakeRemoteReference()
         introducer.remote_subscribe(subscriber2, "thing2")
 
-        s = introducer.get_subscribers()
-        subs = dict([(sn, (si,rref)) for sn, when, si, rref in s])
+        subs = introducer.get_subscribers()
         self.failUnlessEqual(len(subs), 2)
-        (si,rref) = subs["thing2"]
-        self.failUnlessIdentical(rref.original, subscriber2)
-        self.failUnlessEqual(si["version"], 0)
-        self.failUnlessEqual(si["oldest-supported"], "oldest")
+        s0 = [s for s in subs if s.service_name == "thing2"][0]
         # v1 announcements do not contain app-versions
-        self.failUnlessEqual(si["app-versions"], {})
-        self.failUnlessEqual(si["nickname"], u"nick-v1")
-        self.failUnlessEqual(si["my-version"], "my_version")
+        self.failUnlessEqual(s0.app_versions, {})
+        self.failUnlessEqual(s0.nickname, NICKNAME % u"v1")
+        self.failUnlessEqual(s0.version, "my_version")
 
 class Announcements(unittest.TestCase):
     def test_client_v2_unsigned(self):
@@ -779,46 +950,46 @@ class Announcements(unittest.TestCase):
         tub = introducer_furl = None
         app_versions = {"whizzy": "fizzy"}
         client_v2 = IntroducerClient(tub, introducer_furl, u"nick-v2",
-                                     "my_version", "oldest", app_versions)
+                                     "my_version", "oldest", app_versions,
+                                     fakeseq)
         furl1 = "pb://62ubehyunnyhzs7r6vdonnm2hpi52w6y@127.0.0.1:0/swissnum"
         tubid = "62ubehyunnyhzs7r6vdonnm2hpi52w6y"
-        ann_s0 = make_ann_t(client_v2, furl1, None)
+        ann_s0 = make_ann_t(client_v2, furl1, None, 10)
         canary0 = Referenceable()
         introducer.remote_publish_v2(ann_s0, canary0)
         a = introducer.get_announcements()
         self.failUnlessEqual(len(a), 1)
-        (index, (ann_s, canary, ann, when)) = a.items()[0]
-        self.failUnlessIdentical(canary, canary0)
-        self.failUnlessEqual(index, ("storage", None, tubid))
-        self.failUnlessEqual(ann["app-versions"], app_versions)
-        self.failUnlessEqual(ann["nickname"], u"nick-v2")
-        self.failUnlessEqual(ann["service-name"], "storage")
-        self.failUnlessEqual(ann["my-version"], "my_version")
-        self.failUnlessEqual(ann["anonymous-storage-FURL"], furl1)
+        self.failUnlessIdentical(a[0].canary, canary0)
+        self.failUnlessEqual(a[0].index, ("storage", None, tubid))
+        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)
+                                     "my_version", "oldest", app_versions,
+                                     fakeseq)
         furl1 = "pb://62ubehyunnyhzs7r6vdonnm2hpi52w6y@127.0.0.1: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)
+        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)
-        (index, (ann_s, canary, ann, when)) = a.items()[0]
-        self.failUnlessIdentical(canary, canary0)
-        self.failUnlessEqual(index, ("storage", pks, None))
-        self.failUnlessEqual(ann["app-versions"], app_versions)
-        self.failUnlessEqual(ann["nickname"], u"nick-v2")
-        self.failUnlessEqual(ann["service-name"], "storage")
-        self.failUnlessEqual(ann["my-version"], "my_version")
-        self.failUnlessEqual(ann["anonymous-storage-FURL"], furl1)
+        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_v1(self):
         introducer = IntroducerService()
@@ -831,14 +1002,58 @@ class Announcements(unittest.TestCase):
 
         a = introducer.get_announcements()
         self.failUnlessEqual(len(a), 1)
-        (index, (ann_s, canary, ann, when)) = a.items()[0]
-        self.failUnlessEqual(canary, None)
-        self.failUnlessEqual(index, ("storage", None, tubid))
-        self.failUnlessEqual(ann["app-versions"], {})
-        self.failUnlessEqual(ann["nickname"], u"nick-v1".encode("utf-8"))
-        self.failUnlessEqual(ann["service-name"], "storage")
-        self.failUnlessEqual(ann["my-version"], "my_version")
-        self.failUnlessEqual(ann["anonymous-storage-FURL"], furl1)
+        self.failUnlessEqual(a[0].index, ("storage", None, tubid))
+        self.failUnlessEqual(a[0].canary, None)
+        self.failUnlessEqual(a[0].announcement["app-versions"], {})
+        self.failUnlessEqual(a[0].nickname, u"nick-v1".encode("utf-8"))
+        self.failUnlessEqual(a[0].service_name, "storage")
+        self.failUnlessEqual(a[0].version, "my_version")
+        self.failUnlessEqual(a[0].announcement["anonymous-storage-FURL"], furl1)
+
+class ClientSeqnums(unittest.TestCase):
+    def test_client(self):
+        basedir = "introducer/ClientSeqnums/test_client"
+        fileutil.make_dirs(basedir)
+        f = open(os.path.join(basedir, "tahoe.cfg"), "w")
+        f.write("[client]\n")
+        f.write("introducer.furl = nope\n")
+        f.close()
+        c = TahoeClient(basedir)
+        ic = c.introducer_client
+        outbound = ic._outbound_announcements
+        published = ic._published_announcements
+        def read_seqnum():
+            f = open(os.path.join(basedir, "announcement-seqnum"))
+            seqnum = f.read().strip()
+            f.close()
+            return int(seqnum)
+
+        ic.publish("sA", {"key": "value1"}, c._node_key)
+        self.failUnlessEqual(read_seqnum(), 1)
+        self.failUnless("sA" in outbound)
+        self.failUnlessEqual(outbound["sA"]["seqnum"], 1)
+        nonce1 = outbound["sA"]["nonce"]
+        self.failUnless(isinstance(nonce1, str))
+        self.failUnlessEqual(simplejson.loads(published["sA"][0]),
+                             outbound["sA"])
+        # [1] is the signature, [2] is the pubkey
+
+        # publishing a second service causes both services to be
+        # re-published, with the next higher sequence number
+        ic.publish("sB", {"key": "value2"}, c._node_key)
+        self.failUnlessEqual(read_seqnum(), 2)
+        self.failUnless("sB" in outbound)
+        self.failUnlessEqual(outbound["sB"]["seqnum"], 2)
+        self.failUnless("sA" in outbound)
+        self.failUnlessEqual(outbound["sA"]["seqnum"], 2)
+        nonce2 = outbound["sA"]["nonce"]
+        self.failUnless(isinstance(nonce2, str))
+        self.failIfEqual(nonce1, nonce2)
+        self.failUnlessEqual(simplejson.loads(published["sA"][0]),
+                             outbound["sA"])
+        self.failUnlessEqual(simplejson.loads(published["sB"][0]),
+                             outbound["sB"])
+
 
 
 class TooNewServer(IntroducerService):
@@ -868,7 +1083,8 @@ class NonV1Server(SystemTestMixin, unittest.TestCase):
         tub.setLocation("localhost:%d" % portnum)
 
         c = IntroducerClient(tub, self.introducer_furl,
-                             u"nickname-client", "version", "oldest", {})
+                             u"nickname-client", "version", "oldest", {},
+                             fakeseq)
         announcements = {}
         def got(key_s, ann):
             announcements[key_s] = ann