[freenet-cvs] r14803 - in branches/freenet-jfk/src/freenet/node: . fcp simulator updater useralerts

kryptos at freenetproject.org kryptos at freenetproject.org
Mon Aug 20 14:10:53 UTC 2007


Author: kryptos
Date: 2007-08-20 14:10:51 +0000 (Mon, 20 Aug 2007)
New Revision: 14803

Added:
   branches/freenet-jfk/src/freenet/node/DarknetPeerNode.java
   branches/freenet-jfk/src/freenet/node/DarknetPeerNodeStatus.java
   branches/freenet-jfk/src/freenet/node/FailureTable.java
   branches/freenet-jfk/src/freenet/node/FailureTableEntry.java
   branches/freenet-jfk/src/freenet/node/NodeCrypto.java
   branches/freenet-jfk/src/freenet/node/NodeCryptoConfig.java
   branches/freenet-jfk/src/freenet/node/NodeIPPortDetector.java
   branches/freenet-jfk/src/freenet/node/NodeInitException.java
   branches/freenet-jfk/src/freenet/node/OpennetDisabledException.java
   branches/freenet-jfk/src/freenet/node/OpennetManager.java
   branches/freenet-jfk/src/freenet/node/OpennetPeerNode.java
   branches/freenet-jfk/src/freenet/node/OpennetPeerNodeStatus.java
   branches/freenet-jfk/src/freenet/node/TimeSkewDetectorCallback.java
   branches/freenet-jfk/src/freenet/node/fcp/ListPeerMessage.java
   branches/freenet-jfk/src/freenet/node/updater/UpdateOverMandatoryManager.java
   branches/freenet-jfk/src/freenet/node/useralerts/OpennetUserAlert.java
   branches/freenet-jfk/src/freenet/node/useralerts/TimeSkewDetectedUserAlert.java
Modified:
   branches/freenet-jfk/src/freenet/node/CHKInsertSender.java
   branches/freenet-jfk/src/freenet/node/ConfigurablePersister.java
   branches/freenet-jfk/src/freenet/node/DNSRequester.java
   branches/freenet-jfk/src/freenet/node/GlobalProbe.java
   branches/freenet-jfk/src/freenet/node/IPDetectorPluginManager.java
   branches/freenet-jfk/src/freenet/node/InsertHandler.java
   branches/freenet-jfk/src/freenet/node/KeyTracker.java
   branches/freenet-jfk/src/freenet/node/Location.java
   branches/freenet-jfk/src/freenet/node/LocationManager.java
   branches/freenet-jfk/src/freenet/node/LoggingConfigHandler.java
   branches/freenet-jfk/src/freenet/node/LowLevelGetException.java
   branches/freenet-jfk/src/freenet/node/Node.java
   branches/freenet-jfk/src/freenet/node/NodeARKInserter.java
   branches/freenet-jfk/src/freenet/node/NodeClientCore.java
   branches/freenet-jfk/src/freenet/node/NodeDispatcher.java
   branches/freenet-jfk/src/freenet/node/NodeIPDetector.java
   branches/freenet-jfk/src/freenet/node/NodePinger.java
   branches/freenet-jfk/src/freenet/node/NodeStarter.java
   branches/freenet-jfk/src/freenet/node/NodeStats.java
   branches/freenet-jfk/src/freenet/node/OutgoingPacketMangler.java
   branches/freenet-jfk/src/freenet/node/PacketSender.java
   branches/freenet-jfk/src/freenet/node/PeerManager.java
   branches/freenet-jfk/src/freenet/node/PeerNode.java
   branches/freenet-jfk/src/freenet/node/PeerNodeStatus.java
   branches/freenet-jfk/src/freenet/node/Persister.java
   branches/freenet-jfk/src/freenet/node/ProbeCallback.java
   branches/freenet-jfk/src/freenet/node/RequestHandler.java
   branches/freenet-jfk/src/freenet/node/RequestSender.java
   branches/freenet-jfk/src/freenet/node/RequestStarter.java
   branches/freenet-jfk/src/freenet/node/SSKInsertHandler.java
   branches/freenet-jfk/src/freenet/node/SSKInsertSender.java
   branches/freenet-jfk/src/freenet/node/SemiOrderedShutdownHook.java
   branches/freenet-jfk/src/freenet/node/SendableGet.java
   branches/freenet-jfk/src/freenet/node/SendableRequest.java
   branches/freenet-jfk/src/freenet/node/SimpleSendableInsert.java
   branches/freenet-jfk/src/freenet/node/TestnetHandler.java
   branches/freenet-jfk/src/freenet/node/TestnetStatusUploader.java
   branches/freenet-jfk/src/freenet/node/TextModeClientInterface.java
   branches/freenet-jfk/src/freenet/node/TextModeClientInterfaceServer.java
   branches/freenet-jfk/src/freenet/node/Version.java
   branches/freenet-jfk/src/freenet/node/fcp/AddPeer.java
   branches/freenet-jfk/src/freenet/node/fcp/ClientGet.java
   branches/freenet-jfk/src/freenet/node/fcp/ClientGetMessage.java
   branches/freenet-jfk/src/freenet/node/fcp/ClientPut.java
   branches/freenet-jfk/src/freenet/node/fcp/DiskDirPutFile.java
   branches/freenet-jfk/src/freenet/node/fcp/FCPClient.java
   branches/freenet-jfk/src/freenet/node/fcp/FCPConnectionHandler.java
   branches/freenet-jfk/src/freenet/node/fcp/FCPConnectionInputHandler.java
   branches/freenet-jfk/src/freenet/node/fcp/FCPConnectionOutputHandler.java
   branches/freenet-jfk/src/freenet/node/fcp/FCPMessage.java
   branches/freenet-jfk/src/freenet/node/fcp/FCPServer.java
   branches/freenet-jfk/src/freenet/node/fcp/GetFailedMessage.java
   branches/freenet-jfk/src/freenet/node/fcp/GetNode.java
   branches/freenet-jfk/src/freenet/node/fcp/ListPeerNotesMessage.java
   branches/freenet-jfk/src/freenet/node/fcp/ModifyPeer.java
   branches/freenet-jfk/src/freenet/node/fcp/ModifyPeerNote.java
   branches/freenet-jfk/src/freenet/node/fcp/NodeData.java
   branches/freenet-jfk/src/freenet/node/fcp/ProtocolErrorMessage.java
   branches/freenet-jfk/src/freenet/node/fcp/RemovePeer.java
   branches/freenet-jfk/src/freenet/node/fcp/TestDDACompleteMessage.java
   branches/freenet-jfk/src/freenet/node/simulator/RealNodePingTest.java
   branches/freenet-jfk/src/freenet/node/simulator/RealNodeRequestInsertTest.java
   branches/freenet-jfk/src/freenet/node/simulator/RealNodeRoutingTest.java
   branches/freenet-jfk/src/freenet/node/updater/NodeUpdateManager.java
   branches/freenet-jfk/src/freenet/node/updater/NodeUpdater.java
   branches/freenet-jfk/src/freenet/node/updater/RevocationChecker.java
   branches/freenet-jfk/src/freenet/node/useralerts/IPUndetectedUserAlert.java
   branches/freenet-jfk/src/freenet/node/useralerts/MeaningfulNodeNameUserAlert.java
   branches/freenet-jfk/src/freenet/node/useralerts/N2NTMUserAlert.java
   branches/freenet-jfk/src/freenet/node/useralerts/PeerManagerUserAlert.java
   branches/freenet-jfk/src/freenet/node/useralerts/UpdatedVersionAvailableUserAlert.java
   branches/freenet-jfk/src/freenet/node/useralerts/UserAlertManager.java
Log:
Node Merged with trunk(r14796) and r13448:Link level encryption using JFK

Modified: branches/freenet-jfk/src/freenet/node/CHKInsertSender.java
===================================================================
--- branches/freenet-jfk/src/freenet/node/CHKInsertSender.java	2007-08-20 13:57:42 UTC (rev 14802)
+++ branches/freenet-jfk/src/freenet/node/CHKInsertSender.java	2007-08-20 14:10:51 UTC (rev 14803)
@@ -18,6 +18,7 @@
 import freenet.keys.CHKBlock;
 import freenet.keys.CHKVerifyException;
 import freenet.keys.NodeCHK;
+import freenet.support.Executor;
 import freenet.support.Logger;
 import freenet.support.OOMHandler;
 
@@ -27,15 +28,17 @@
 		
 		final AwaitingCompletion completion;
 		final BlockTransmitter bt;
+		final Executor executor;
 		
-		public Sender(AwaitingCompletion ac) {
+		public Sender(AwaitingCompletion ac, Executor executor) {
 			this.bt = ac.bt;
 			this.completion = ac;
+			this.executor = executor;
 		}
 		
 		public void run() {
 			try {
-				bt.send();
+				bt.send(executor);
 				if(bt.failedDueToOverload()) {
 					completion.completedTransfer(false);
 				} else {
@@ -75,10 +78,8 @@
 		}
 		
 		void start() {
-			Sender s = new Sender(this);
-            Thread senderThread = new Thread(s, "Sender for "+uid+" to "+pn.getPeer());
-            senderThread.setDaemon(true);
-            senderThread.start();
+			Sender s = new Sender(this, node.executor);
+			node.executor.execute(s, "Sender for "+uid+" to "+pn.getPeer());
 		}
 		
 		void completed(boolean timeout, boolean success) {
@@ -137,9 +138,7 @@
     }
 
 	void start() {
-        Thread t = new Thread(this, "CHKInsertSender for UID "+uid+" on "+node.portNumber+" at "+System.currentTimeMillis());
-        t.setDaemon(true);
-        t.start();
+		node.executor.execute(this, "CHKInsertSender for UID "+uid+" on "+node.getDarknetPortNumber()+" at "+System.currentTimeMillis());
 	}
 
 	static boolean logMINOR;
@@ -247,9 +246,9 @@
             PeerNode next;
             // Can backtrack, so only route to nodes closer than we are to target.
             double nextValue;
-            next = node.peers.closerPeer(source, nodesRoutedTo, nodesNotIgnored, target, true, node.isAdvancedModeEnabled(), -1);
+            next = node.peers.closerPeer(source, nodesRoutedTo, nodesNotIgnored, target, true, node.isAdvancedModeEnabled(), -1, null);
             if(next != null)
-                nextValue = next.getLocation().getValue();
+                nextValue = next.getLocation();
             else
                 nextValue = -1.0;
             
@@ -263,7 +262,7 @@
             
             Message req;
             synchronized (this) {
-            	if(PeerManager.distance(target, nextValue) > PeerManager.distance(target, closestLocation)) {
+            	if(Location.distance(target, nextValue) > Location.distance(target, closestLocation)) {
             		if(logMINOR) Logger.minor(this, "Backtracking: target="+target+" next="+nextValue+" closest="+closestLocation);
             		htl = node.decrementHTL(source, htl);
             	}
@@ -572,6 +571,10 @@
         		notifyAll();
         	}
         }
+        
+        if(status == SUCCESS && next != null)
+        	next.onSuccess(true, false);
+        
         if(logMINOR) Logger.minor(this, "Returning from finish()");
     }
 
@@ -622,16 +625,13 @@
 	private void makeCompletionWaiter() {
 		if(logMINOR)
 			Logger.minor(this, "Creating completion waiter for "+uid);
-		Thread t;
 		synchronized (this) {
 			if(cw == null)
 				cw = new CompletionWaiter();
 			else
 				return;
 		}
-		t = new Thread(cw, "Completion waiter for "+uid);
-		t.setDaemon(true);
-		t.start();
+		node.executor.execute(cw, "Completion waiter for "+uid);
 	}
 	
 	private class CompletionWaiter implements Runnable {

Modified: branches/freenet-jfk/src/freenet/node/ConfigurablePersister.java
===================================================================
--- branches/freenet-jfk/src/freenet/node/ConfigurablePersister.java	2007-08-20 13:57:42 UTC (rev 14802)
+++ branches/freenet-jfk/src/freenet/node/ConfigurablePersister.java	2007-08-20 14:10:51 UTC (rev 14803)
@@ -6,7 +6,6 @@
 import freenet.config.InvalidConfigValueException;
 import freenet.config.SubConfig;
 import freenet.l10n.L10n;
-import freenet.node.Node.NodeInitException;
 import freenet.support.api.StringCallback;
 
 public class ConfigurablePersister extends Persister {
@@ -30,7 +29,7 @@
 		try {
 			setThrottles(throttleFile);
 		} catch (InvalidConfigValueException e2) {
-			throw new NodeInitException(Node.EXIT_THROTTLE_FILE_ERROR, e2.getMessage());
+			throw new NodeInitException(NodeInitException.EXIT_THROTTLE_FILE_ERROR, e2.getMessage());
 		}
 	}
 
@@ -44,7 +43,10 @@
 				break;
 			} else {
 				try {
-					f.createNewFile();
+					if(!f.createNewFile()) {
+						if(f.exists()) continue;
+						throw new InvalidConfigValueException(l10n("doesNotExistCannotCreate"));
+					}
 				} catch (IOException e) {
 					throw new InvalidConfigValueException(l10n("doesNotExistCannotCreate"));
 				}

Modified: branches/freenet-jfk/src/freenet/node/DNSRequester.java
===================================================================
--- branches/freenet-jfk/src/freenet/node/DNSRequester.java	2007-08-20 13:57:42 UTC (rev 14802)
+++ branches/freenet-jfk/src/freenet/node/DNSRequester.java	2007-08-20 14:10:51 UTC (rev 14803)
@@ -19,7 +19,7 @@
 
     DNSRequester(Node node) {
         this.node = node;
-        myThread = new Thread(this, "DNSRequester thread for "+node.portNumber);
+        myThread = new Thread(this, "DNSRequester thread for "+node.getDarknetPortNumber());
         myThread.setDaemon(true);
     }
 

Copied: branches/freenet-jfk/src/freenet/node/DarknetPeerNode.java (from rev 14796, trunk/freenet/src/freenet/node/DarknetPeerNode.java)
===================================================================
--- branches/freenet-jfk/src/freenet/node/DarknetPeerNode.java	                        (rev 0)
+++ branches/freenet-jfk/src/freenet/node/DarknetPeerNode.java	2007-08-20 14:10:51 UTC (rev 14803)
@@ -0,0 +1,1529 @@
+package freenet.node;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.EOFException;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.io.UnsupportedEncodingException;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+
+import freenet.client.DefaultMIMETypes;
+import freenet.io.comm.DMT;
+import freenet.io.comm.DisconnectedException;
+import freenet.io.comm.FreenetInetAddress;
+import freenet.io.comm.Message;
+import freenet.io.comm.NotConnectedException;
+import freenet.io.comm.Peer;
+import freenet.io.comm.PeerParseException;
+import freenet.io.comm.ReferenceSignatureVerificationException;
+import freenet.io.comm.RetrievalException;
+import freenet.io.xfer.BulkReceiver;
+import freenet.io.xfer.BulkTransmitter;
+import freenet.io.xfer.PartiallyReceivedBulk;
+import freenet.l10n.L10n;
+import freenet.node.useralerts.N2NTMUserAlert;
+import freenet.node.useralerts.UserAlert;
+import freenet.support.Base64;
+import freenet.support.Fields;
+import freenet.support.HTMLNode;
+import freenet.support.IllegalBase64Exception;
+import freenet.support.Logger;
+import freenet.support.SimpleFieldSet;
+import freenet.support.SizeUtil;
+import freenet.support.io.FileUtil;
+import freenet.support.io.RandomAccessFileWrapper;
+import freenet.support.io.RandomAccessThing;
+
+public class DarknetPeerNode extends PeerNode {
+
+    /** Name of this node */
+    String myName;
+    
+    /** True if this peer is not to be connected with */
+    private boolean isDisabled;
+    
+    /** True if we don't send handshake requests to this peer, but will connect if we receive one */
+    private boolean isListenOnly;
+    
+    /** True if we send handshake requests to this peer in infrequent bursts */
+    private boolean isBurstOnly;
+    
+    /** True if we are currently sending this peer a burst of handshake requests */
+    private boolean isBursting;
+
+    /** True if we want to ignore the source port of the node's sent packets.
+     * This is normally set when dealing with an Evil Corporate Firewall which rewrites the port on outgoing
+     * packets but does not redirect incoming packets destined to the rewritten port.
+     * What it does is this: If we have an address with the same IP but a different port, to the detectedPeer,
+     * we use that instead. */
+    private boolean ignoreSourcePort;
+    
+    /** True if we want to allow LAN/localhost addresses. */
+    private boolean allowLocalAddresses;
+    
+    /** Extra peer data file numbers */
+    private LinkedHashSet extraPeerDataFileNumbers;
+
+    /** Private comment on the peer for /friends/ page */
+    private String privateDarknetComment;
+    
+    /** Private comment on the peer for /friends/ page's extra peer data file number */
+    private int privateDarknetCommentFileNumber;
+    
+    /** Queued-to-send N2NTM extra peer data file numbers */
+    private LinkedHashSet queuedToSendN2NTMExtraPeerDataFileNumbers;
+
+    /** Number of handshake attempts (while in ListenOnly mode) since the beginning of this burst */
+    private int listeningHandshakeBurstCount;
+    
+    /** Total number of handshake attempts (while in ListenOnly mode) to be in this burst */
+    private int listeningHandshakeBurstSize;
+    
+    private static boolean logMINOR;
+    
+    /**
+     * Create a darknet PeerNode from a SimpleFieldSet
+     * @param fs The SimpleFieldSet to parse
+     * @param node2 The running Node we are part of.
+     */
+    public DarknetPeerNode(SimpleFieldSet fs, Node node2, NodeCrypto crypto, PeerManager peers, boolean fromLocal, OutgoingPacketMangler mangler) throws FSParseException, PeerParseException, ReferenceSignatureVerificationException {
+    	super(fs, node2, crypto, peers, fromLocal, mangler, false);
+    	
+    	logMINOR = Logger.shouldLog(Logger.MINOR, this);
+    	
+    	long now = System.currentTimeMillis();
+    	
+        String name = fs.get("myName");
+        if(name == null) throw new FSParseException("No name");
+        myName = name;
+
+        if(fromLocal) {
+        	SimpleFieldSet metadata = fs.subset("metadata");
+        	
+        	isDisabled = Fields.stringToBool(metadata.get("isDisabled"), false);
+        	isListenOnly = Fields.stringToBool(metadata.get("isListenOnly"), false);
+        	isBurstOnly = Fields.stringToBool(metadata.get("isBurstOnly"), false);
+        	ignoreSourcePort = Fields.stringToBool(metadata.get("ignoreSourcePort"), false);
+        	allowLocalAddresses = Fields.stringToBool(metadata.get("allowLocalAddresses"), false);
+        }
+	
+        listeningHandshakeBurstCount = 0;
+        listeningHandshakeBurstSize = Node.MIN_BURSTING_HANDSHAKE_BURST_SIZE
+        	+ node.random.nextInt(Node.RANDOMIZED_BURSTING_HANDSHAKE_BURST_SIZE);
+        if(isBurstOnly) {
+        	Logger.minor(this, "First BurstOnly mode handshake in "+(sendHandshakeTime - now)+"ms for "+getName()+" (count: "+listeningHandshakeBurstCount+", size: "+listeningHandshakeBurstSize+ ')');
+        }
+
+		// Setup the private darknet comment note
+        privateDarknetComment = "";
+        privateDarknetCommentFileNumber = -1;
+
+		// Setup the extraPeerDataFileNumbers
+		extraPeerDataFileNumbers = new LinkedHashSet();
+		
+		// Setup the queuedToSendN2NTMExtraPeerDataFileNumbers
+		queuedToSendN2NTMExtraPeerDataFileNumbers = new LinkedHashSet();
+        
+    }
+
+    /**
+     * 
+     * Normally this is the address that packets have been received from from this node.
+     * However, if ignoreSourcePort is set, we will search for a similar address with a different port 
+     * number in the node reference.
+     */
+    public synchronized Peer getPeer(){
+    	Peer detectedPeer = super.getPeer();
+    	if(ignoreSourcePort) {
+    		FreenetInetAddress addr = detectedPeer == null ? null : detectedPeer.getFreenetAddress();
+    		int port = detectedPeer == null ? -1 : detectedPeer.getPort();
+    		if(nominalPeer == null) return detectedPeer;
+    		for(int i=0;i<nominalPeer.size();i++) {
+    			Peer p = (Peer) nominalPeer.get(i);
+    			if(p.getPort() != port && p.getFreenetAddress().equals(addr)) {
+    				return p;
+    			}
+    		}
+    	}
+    	return detectedPeer;
+    }
+
+    /**
+     * @return True, if we are disconnected and it has been a
+     * sufficient time period since we last sent a handshake
+     * attempt.
+     */
+    public boolean shouldSendHandshake() {
+    	synchronized(this) {
+    		if(isDisabled) return false;
+    		if(isListenOnly) return false;
+    		if(!super.shouldSendHandshake()) return false;
+    		if(isBurstOnly())
+    			isBursting = true;
+    		else
+    			return true;
+    	}
+    	// Might have changed from burst only to bursting
+		setPeerNodeStatus(System.currentTimeMillis());
+		return true;
+    }
+    
+    protected synchronized boolean innerProcessNewNoderef(SimpleFieldSet fs, boolean forARK) throws FSParseException {
+    	boolean changedAnything = super.innerProcessNewNoderef(fs, forARK);
+        String name = fs.get("myName");
+        if(name != null && !name.equals(myName)) {
+        	changedAnything = true;
+            myName = name;
+        }
+        return changedAnything;
+    }
+    
+    public synchronized SimpleFieldSet exportFieldSet() {
+    	SimpleFieldSet fs = super.exportFieldSet();
+    	fs.putSingle("myName", getName());
+    	return fs;
+    }
+    	
+    public synchronized SimpleFieldSet exportMetadataFieldSet() {
+    	SimpleFieldSet fs = super.exportMetadataFieldSet();
+    	if(isDisabled)
+    		fs.putSingle("isDisabled", "true");
+    	if(isListenOnly)
+    		fs.putSingle("isListenOnly", "true");
+    	if(isBurstOnly)
+    		fs.putSingle("isBurstOnly", "true");
+    	if(ignoreSourcePort)
+    		fs.putSingle("ignoreSourcePort", "true");
+    	if(allowLocalAddresses)
+    		fs.putSingle("allowLocalAddresses", "true");
+    	return fs;
+    }
+
+	public synchronized String getName() {
+		return myName;
+	}
+
+	protected synchronized int getPeerNodeStatus(long now, long backedOffUntil) {
+		if(isDisabled) {
+			return PeerManager.PEER_NODE_STATUS_DISABLED;
+		}
+		int status = super.getPeerNodeStatus(now, backedOffUntil);
+		if(status == PeerManager.PEER_NODE_STATUS_CONNECTED || 
+				status == PeerManager.PEER_NODE_STATUS_CLOCK_PROBLEM ||
+				status == PeerManager.PEER_NODE_STATUS_ROUTING_BACKED_OFF ||
+				status == PeerManager.PEER_NODE_STATUS_CONN_ERROR ||
+				status == PeerManager.PEER_NODE_STATUS_TOO_NEW ||
+				status == PeerManager.PEER_NODE_STATUS_TOO_OLD)
+			return status;
+		if(isListenOnly)
+			return PeerManager.PEER_NODE_STATUS_LISTEN_ONLY;
+		if(isBursting)
+			return PeerManager.PEER_NODE_STATUS_BURSTING;
+		if(isBurstOnly)
+			return PeerManager.PEER_NODE_STATUS_LISTENING;
+		return status;
+	}
+	
+	public void enablePeer() {
+		synchronized(this) {
+			isDisabled = false;
+		}
+		setPeerNodeStatus(System.currentTimeMillis());
+        node.peers.writePeers();
+	}
+	
+	public void disablePeer() {
+		synchronized(this) {
+			isDisabled = true;
+		}
+		if(isConnected()) {
+			forceDisconnect();
+		}
+		stopARKFetcher();
+		setPeerNodeStatus(System.currentTimeMillis());
+        node.peers.writePeers();
+	}
+
+	public synchronized boolean isDisabled() {
+		return isDisabled;
+	}
+	
+	public void setListenOnly(boolean setting) {
+		synchronized(this) {
+			isListenOnly = setting;
+		}
+		if(setting && isBurstOnly()) {
+			setBurstOnly(false);
+		}
+		if(setting) {
+			stopARKFetcher();
+		}
+		setPeerNodeStatus(System.currentTimeMillis());
+        node.peers.writePeers();
+	}
+
+	public synchronized boolean isListenOnly() {
+		return isListenOnly;
+	}
+	
+	public void setBurstOnly(boolean setting) {
+		synchronized(this) {
+			isBurstOnly = setting;
+		}
+		if(setting && isListenOnly()) {
+			setListenOnly(false);
+		}
+		long now = System.currentTimeMillis();
+		if(!setting) {
+			synchronized(this) {
+				sendHandshakeTime = now;  // don't keep any long handshake delays we might have had under BurstOnly
+			}
+		}
+		setPeerNodeStatus(now);
+		node.peers.writePeers();
+	}
+
+	public void setIgnoreSourcePort(boolean setting) {
+		synchronized(this) {
+			ignoreSourcePort = setting;
+		}
+	}
+	
+
+	public boolean isIgnoreSourcePort() {
+		return ignoreSourcePort;
+	}
+	
+	public boolean isIgnoreSource() {
+		return ignoreSourcePort;
+	}
+	
+	public synchronized boolean isBurstOnly() {
+		return isBurstOnly;
+	}
+
+	public boolean allowLocalAddresses() {
+		synchronized(this) {
+			if(allowLocalAddresses) return true;
+		}
+		return super.allowLocalAddresses();
+	}
+
+	public void setAllowLocalAddresses(boolean setting) {
+		synchronized(this) {
+			allowLocalAddresses = setting;
+		}
+        node.peers.writePeers();
+	}
+	
+	public boolean readExtraPeerData() {
+		String extraPeerDataDirPath = node.getExtraPeerDataDir();
+		File extraPeerDataPeerDir = new File(extraPeerDataDirPath+File.separator+getIdentityString());
+	 	if(!extraPeerDataPeerDir.exists()) {
+	 		return false;
+	 	}
+	 	if(!extraPeerDataPeerDir.isDirectory()) {
+	   		Logger.error(this, "Extra peer data directory for peer not a directory: "+extraPeerDataPeerDir.getPath());
+	 		return false;
+	 	}
+	 	File[] extraPeerDataFiles = extraPeerDataPeerDir.listFiles();
+	 	if(extraPeerDataFiles == null) {
+	 		return false;
+	 	}
+		boolean gotError = false;
+		boolean readResult = false;
+		for (int i = 0; i < extraPeerDataFiles.length; i++) {
+			Integer fileNumber;
+			try {
+				fileNumber = new Integer(extraPeerDataFiles[i].getName());
+			} catch (NumberFormatException e) {
+				gotError = true;
+				continue;
+			}
+			synchronized(extraPeerDataFileNumbers) {
+				extraPeerDataFileNumbers.add(fileNumber);
+			}
+			readResult = readExtraPeerDataFile(extraPeerDataFiles[i], fileNumber.intValue());
+			if(!readResult) {
+				gotError = true;
+			}
+		}
+		return !gotError;
+	}
+
+	public boolean rereadExtraPeerDataFile(int fileNumber) {
+		if(logMINOR)
+			Logger.minor(this, "Rereading peer data file "+fileNumber+" for "+shortToString());
+		String extraPeerDataDirPath = node.getExtraPeerDataDir();
+		File extraPeerDataPeerDir = new File(extraPeerDataDirPath+File.separator+getIdentityString());
+		if(!extraPeerDataPeerDir.exists()) {
+			Logger.error(this, "Extra peer data directory for peer does not exist: "+extraPeerDataPeerDir.getPath());
+			return false;
+		}
+		if(!extraPeerDataPeerDir.isDirectory()) {
+			Logger.error(this, "Extra peer data directory for peer not a directory: "+extraPeerDataPeerDir.getPath());
+			return false;
+		}
+		File extraPeerDataFile = new File(extraPeerDataDirPath+File.separator+getIdentityString()+File.separator+fileNumber);
+		if(!extraPeerDataFile.exists()) {
+			Logger.error(this, "Extra peer data file for peer does not exist: "+extraPeerDataFile.getPath());
+			return false;
+		}
+		return readExtraPeerDataFile(extraPeerDataFile, fileNumber);
+	}
+
+	public boolean readExtraPeerDataFile(File extraPeerDataFile, int fileNumber) {
+		if(logMINOR) Logger.minor(this, "Reading "+extraPeerDataFile+" : "+fileNumber+" for "+shortToString());
+		boolean gotError = false;
+	 	if(!extraPeerDataFile.exists()) {
+	 		if(logMINOR)
+	 			Logger.minor(this, "Does not exist");
+	 		return false;
+	 	}
+		Logger.normal(this, "extraPeerDataFile: "+extraPeerDataFile.getPath());
+		FileInputStream fis;
+		try {
+			fis = new FileInputStream(extraPeerDataFile);
+		} catch (FileNotFoundException e1) {
+			Logger.normal(this, "Extra peer data file not found: "+extraPeerDataFile.getPath());
+			return false;
+		}
+		InputStreamReader isr;
+		try {
+			isr = new InputStreamReader(fis, "UTF-8");
+		} catch (UnsupportedEncodingException e) {
+			throw new Error("Impossible: JVM doesn't support UTF-8: "+e, e);
+		}
+		BufferedReader br = new BufferedReader(isr);
+		SimpleFieldSet fs = null;
+		try {
+			// Read in the single SimpleFieldSet
+			fs = new SimpleFieldSet(br, false, true);
+		} catch (EOFException e3) {
+			// End of file, fine
+		} catch (IOException e4) {
+			Logger.error(this, "Could not read extra peer data file: "+e4, e4);
+		} finally {
+			try {
+				br.close();
+			} catch (IOException e5) {
+				Logger.error(this, "Ignoring "+e5+" caught reading "+extraPeerDataFile.getPath(), e5);
+			}
+		}
+		if(fs == null) {
+			Logger.normal(this, "Deleting corrupt (too short?) file: "+extraPeerDataFile);
+			deleteExtraPeerDataFile(fileNumber);
+			return true;
+		}
+		boolean parseResult = false;
+		try {
+			parseResult = parseExtraPeerData(fs, extraPeerDataFile, fileNumber);
+			if(!parseResult) {
+				gotError = true;
+			}
+		} catch (FSParseException e2) {
+			Logger.error(this, "Could not parse extra peer data: "+e2+ '\n' +fs.toString(),e2);
+			gotError = true;
+		}
+		return !gotError;
+	}
+
+	private boolean parseExtraPeerData(SimpleFieldSet fs, File extraPeerDataFile, int fileNumber) throws FSParseException {
+		String extraPeerDataTypeString = fs.get("extraPeerDataType");
+		int extraPeerDataType = -1;
+		try {
+			extraPeerDataType = Integer.parseInt(extraPeerDataTypeString);
+		} catch (NumberFormatException e) {
+			Logger.error(this, "NumberFormatException parsing extraPeerDataType ("+extraPeerDataTypeString+") in file "+extraPeerDataFile.getPath());
+			return false;
+		}
+		if(extraPeerDataType == Node.EXTRA_PEER_DATA_TYPE_N2NTM) {
+			node.handleNodeToNodeTextMessageSimpleFieldSet(fs, this, fileNumber);
+			return true;
+		} else if(extraPeerDataType == Node.EXTRA_PEER_DATA_TYPE_PEER_NOTE) {
+			String peerNoteTypeString = fs.get("peerNoteType");
+			int peerNoteType = -1;
+			try {
+				peerNoteType = Integer.parseInt(peerNoteTypeString);
+			} catch (NumberFormatException e) {
+				Logger.error(this, "NumberFormatException parsing peerNoteType ("+peerNoteTypeString+") in file "+extraPeerDataFile.getPath());
+				return false;
+			}
+			if(peerNoteType == Node.PEER_NOTE_TYPE_PRIVATE_DARKNET_COMMENT) {
+				synchronized(privateDarknetComment) {
+				  	try {
+						privateDarknetComment = new String(Base64.decode(fs.get("privateDarknetComment")));
+					} catch (IllegalBase64Exception e) {
+						Logger.error(this, "Bad Base64 encoding when decoding a private darknet comment SimpleFieldSet", e);
+						return false;
+					}
+					privateDarknetCommentFileNumber = fileNumber;
+				}
+				return true;
+			}
+			Logger.error(this, "Read unknown peer note type '"+peerNoteType+"' from file "+extraPeerDataFile.getPath());
+			return false;
+		} else if(extraPeerDataType == Node.EXTRA_PEER_DATA_TYPE_QUEUED_TO_SEND_N2NTM) {
+			boolean sendSuccess = false;
+			int type = fs.getInt("n2nType", 1); // FIXME remove default
+			fs.putOverwrite("n2nType", Integer.toString(type));
+			if(isConnected()) {
+				Message n2ntm;
+				if(fs.get("extraPeerDataType") != null) {
+					fs.removeValue("extraPeerDataType");
+				}
+				if(fs.get("senderFileNumber") != null) {
+					fs.removeValue("senderFileNumber");
+				}
+				fs.putOverwrite("senderFileNumber", String.valueOf(fileNumber));
+				if(fs.get("sentTime") != null) {
+					fs.removeValue("sentTime");
+				}
+				fs.putOverwrite("sentTime", Long.toString(System.currentTimeMillis()));
+				
+				try {
+					n2ntm = DMT.createNodeToNodeMessage(type, fs.toString().getBytes("UTF-8"));
+				} catch (UnsupportedEncodingException e) {
+					Logger.error(this, "UnsupportedEncodingException processing extraPeerDataType ("+extraPeerDataTypeString+") in file "+extraPeerDataFile.getPath(), e);
+					return false;
+				}
+
+				try {
+					synchronized(queuedToSendN2NTMExtraPeerDataFileNumbers) {
+						node.usm.send(this, n2ntm, null);
+						Logger.normal(this, "Sent queued ("+fileNumber+") N2NTM to '"+getName()+"': "+n2ntm);
+						sendSuccess = true;
+						queuedToSendN2NTMExtraPeerDataFileNumbers.remove(new Integer(fileNumber));
+					}
+					deleteExtraPeerDataFile(fileNumber);
+				} catch (NotConnectedException e) {
+					sendSuccess = false;  // redundant, but clear
+				}
+			}
+			if(!sendSuccess) {
+				synchronized(queuedToSendN2NTMExtraPeerDataFileNumbers) {
+					fs.putOverwrite("extraPeerDataType", Integer.toString(extraPeerDataType));
+					fs.removeValue("sentTime");
+					queuedToSendN2NTMExtraPeerDataFileNumbers.add(new Integer(fileNumber));
+				}
+			}
+			return true;
+		}
+		Logger.error(this, "Read unknown extra peer data type '"+extraPeerDataType+"' from file "+extraPeerDataFile.getPath());
+		return false;
+	}
+
+	public int writeNewExtraPeerDataFile(SimpleFieldSet fs, int extraPeerDataType) {
+		String extraPeerDataDirPath = node.getExtraPeerDataDir();
+		if(extraPeerDataType > 0)
+			fs.putOverwrite("extraPeerDataType", Integer.toString(extraPeerDataType));
+		File extraPeerDataPeerDir = new File(extraPeerDataDirPath+File.separator+getIdentityString());
+	 	if(!extraPeerDataPeerDir.exists()) {
+	 		if(!extraPeerDataPeerDir.mkdir()) {
+		   		Logger.error(this, "Extra peer data directory for peer could not be created: "+extraPeerDataPeerDir.getPath());
+		 		return -1;
+		 	}
+	 	}
+	 	if(!extraPeerDataPeerDir.isDirectory()) {
+	   		Logger.error(this, "Extra peer data directory for peer not a directory: "+extraPeerDataPeerDir.getPath());
+	 		return -1;
+	 	}
+		Integer[] localFileNumbers = null;
+		int nextFileNumber = 0;
+		synchronized(extraPeerDataFileNumbers) {
+			// Find the first free slot
+			localFileNumbers = (Integer[]) extraPeerDataFileNumbers.toArray(new Integer[extraPeerDataFileNumbers.size()]);
+			Arrays.sort(localFileNumbers);
+			for (int i = 0; i < localFileNumbers.length; i++) {
+				if(localFileNumbers[i].intValue() > nextFileNumber) {
+					break;
+				}
+				nextFileNumber = localFileNumbers[i].intValue() + 1;
+			}
+			extraPeerDataFileNumbers.add(new Integer(nextFileNumber));
+		}
+		FileOutputStream fos;
+		File extraPeerDataFile = new File(extraPeerDataPeerDir.getPath()+File.separator+nextFileNumber);
+	 	if(extraPeerDataFile.exists()) {
+   			Logger.error(this, "Extra peer data file already exists: "+extraPeerDataFile.getPath());
+		 	return -1;
+	 	}
+		String f = extraPeerDataFile.getPath();
+		try {
+			fos = new FileOutputStream(f);
+		} catch (FileNotFoundException e2) {
+			Logger.error(this, "Cannot write extra peer data file to disk: Cannot create "
+					+ f + " - " + e2, e2);
+			return -1;
+		}
+		OutputStreamWriter w;
+		try {
+			w = new OutputStreamWriter(fos, "UTF-8");
+		} catch (UnsupportedEncodingException e2) {
+			throw new Error("UTF-8 unsupported!: "+e2, e2);
+		}
+		BufferedWriter bw = new BufferedWriter(w);
+		try {
+			fs.writeTo(bw);
+			bw.close();
+		} catch (IOException e) {
+			try {
+				fos.close();
+			} catch (IOException e1) {
+				Logger.error(this, "Cannot close extra peer data file: "+e, e);
+			}
+			Logger.error(this, "Cannot write file: " + e, e);
+			return -1;
+		}
+		return nextFileNumber;
+	}
+
+	public void deleteExtraPeerDataFile(int fileNumber) {
+		String extraPeerDataDirPath = node.getExtraPeerDataDir();
+		File extraPeerDataPeerDir = new File(extraPeerDataDirPath, getIdentityString());
+	 	if(!extraPeerDataPeerDir.exists()) {
+	   		Logger.error(this, "Extra peer data directory for peer does not exist: "+extraPeerDataPeerDir.getPath());
+	 		return;
+	 	}
+	 	if(!extraPeerDataPeerDir.isDirectory()) {
+	   		Logger.error(this, "Extra peer data directory for peer not a directory: "+extraPeerDataPeerDir.getPath());
+	 		return;
+	 	}
+		File extraPeerDataFile = new File(extraPeerDataPeerDir, Integer.toString(fileNumber));
+	 	if(!extraPeerDataFile.exists()) {
+	   		Logger.error(this, "Extra peer data file for peer does not exist: "+extraPeerDataFile.getPath());
+	 		return;
+	 	}
+		synchronized(extraPeerDataFileNumbers) {
+			extraPeerDataFileNumbers.remove(new Integer(fileNumber));
+		}
+		if(!extraPeerDataFile.delete()) {
+			if(extraPeerDataFile.exists()) {
+				Logger.error(this, "Cannot delete file "+extraPeerDataFile+" after sending message to "+getPeer()+" - it may be resent on resting the node");
+			} else {
+				Logger.normal(this, "File does not exist when deleting: "+extraPeerDataFile+" after sending message to "+getPeer());
+			}
+		}
+	}
+
+	public void removeExtraPeerDataDir() {
+		String extraPeerDataDirPath = node.getExtraPeerDataDir();
+		File extraPeerDataPeerDir = new File(extraPeerDataDirPath+File.separator+getIdentityString());
+	 	if(!extraPeerDataPeerDir.exists()) {
+			Logger.error(this, "Extra peer data directory for peer does not exist: "+extraPeerDataPeerDir.getPath());
+			return;
+	 	}
+	 	if(!extraPeerDataPeerDir.isDirectory()) {
+	   		Logger.error(this, "Extra peer data directory for peer not a directory: "+extraPeerDataPeerDir.getPath());
+	 		return;
+	 	}
+		Integer[] localFileNumbers = null;
+		synchronized(extraPeerDataFileNumbers) {
+			localFileNumbers = (Integer[]) extraPeerDataFileNumbers.toArray(new Integer[extraPeerDataFileNumbers.size()]);
+		}
+		for (int i = 0; i < localFileNumbers.length; i++) {
+			deleteExtraPeerDataFile(localFileNumbers[i].intValue());
+		}
+		extraPeerDataPeerDir.delete();
+	}
+
+	public boolean rewriteExtraPeerDataFile(SimpleFieldSet fs, int extraPeerDataType, int fileNumber) {
+		String extraPeerDataDirPath = node.getExtraPeerDataDir();
+		if(extraPeerDataType > 0)
+			fs.putOverwrite("extraPeerDataType", Integer.toString(extraPeerDataType));
+		File extraPeerDataPeerDir = new File(extraPeerDataDirPath+File.separator+getIdentityString());
+	 	if(!extraPeerDataPeerDir.exists()) {
+	   		Logger.error(this, "Extra peer data directory for peer does not exist: "+extraPeerDataPeerDir.getPath());
+	 		return false;
+	 	}
+	 	if(!extraPeerDataPeerDir.isDirectory()) {
+	   		Logger.error(this, "Extra peer data directory for peer not a directory: "+extraPeerDataPeerDir.getPath());
+	 		return false;
+	 	}
+		File extraPeerDataFile = new File(extraPeerDataDirPath+File.separator+getIdentityString()+File.separator+fileNumber);
+	 	if(!extraPeerDataFile.exists()) {
+	   		Logger.error(this, "Extra peer data file for peer does not exist: "+extraPeerDataFile.getPath());
+	 		return false;
+	 	}
+		String f = extraPeerDataFile.getPath();
+		FileOutputStream fos;
+		try {
+			fos = new FileOutputStream(f);
+		} catch (FileNotFoundException e2) {
+			Logger.error(this, "Cannot write extra peer data file to disk: Cannot open "
+					+ f + " - " + e2, e2);
+			return false;
+		}
+		OutputStreamWriter w;
+		try {
+			w = new OutputStreamWriter(fos, "UTF-8");
+		} catch (UnsupportedEncodingException e2) {
+			throw new Error("JVM doesn't support UTF-8 charset!: "+e2, e2);
+		}
+		BufferedWriter bw = new BufferedWriter(w);
+		try {
+			fs.writeTo(bw);
+			bw.close();
+		} catch (IOException e) {
+			try {
+				fos.close();
+			} catch (IOException e1) {
+				Logger.error(this, "Cannot close extra peer data file: "+e, e);
+			}
+			Logger.error(this, "Cannot write file: " + e, e);
+			return false;
+		}
+		return true;
+	}
+	
+	public synchronized String getPrivateDarknetCommentNote() {
+		return privateDarknetComment;
+	}
+	
+	public synchronized void setPrivateDarknetCommentNote(String comment) {
+		int localFileNumber;
+		synchronized(privateDarknetComment) {
+			privateDarknetComment = comment;
+			localFileNumber = privateDarknetCommentFileNumber;
+		}
+		SimpleFieldSet fs = new SimpleFieldSet(true);
+		fs.put("peerNoteType", Node.PEER_NOTE_TYPE_PRIVATE_DARKNET_COMMENT);
+		fs.putSingle("privateDarknetComment", Base64.encode(comment.getBytes()));
+		if(localFileNumber == -1) {
+			localFileNumber = writeNewExtraPeerDataFile(fs, Node.EXTRA_PEER_DATA_TYPE_PEER_NOTE);
+			synchronized(privateDarknetComment) {
+				privateDarknetCommentFileNumber = localFileNumber;
+			}
+		} else {
+			rewriteExtraPeerDataFile(fs, Node.EXTRA_PEER_DATA_TYPE_PEER_NOTE, localFileNumber);
+		}
+	}
+
+	public void queueN2NTM(SimpleFieldSet fs) {
+		int fileNumber = writeNewExtraPeerDataFile( fs, Node.EXTRA_PEER_DATA_TYPE_QUEUED_TO_SEND_N2NTM);
+		synchronized(queuedToSendN2NTMExtraPeerDataFileNumbers) {
+			queuedToSendN2NTMExtraPeerDataFileNumbers.add(new Integer(fileNumber));
+		}
+	}
+
+	public void sendQueuedN2NTMs() {
+		if(logMINOR)
+			Logger.minor(this, "Sending queued N2NTMs for "+shortToString());
+		Integer[] localFileNumbers = null;
+		synchronized(queuedToSendN2NTMExtraPeerDataFileNumbers) {
+			localFileNumbers = (Integer[]) queuedToSendN2NTMExtraPeerDataFileNumbers.toArray(new Integer[queuedToSendN2NTMExtraPeerDataFileNumbers.size()]);
+		}
+		Arrays.sort(localFileNumbers);
+		for (int i = 0; i < localFileNumbers.length; i++) {
+			rereadExtraPeerDataFile(localFileNumbers[i].intValue());
+		}
+	}
+
+	void startARKFetcher() {
+		synchronized(this) {
+			if(isListenOnly) {
+				Logger.minor(this, "Not starting ark fetcher for "+this+" as it's in listen-only mode.");
+				return;
+			}
+		}
+		super.startARKFetcher();
+	}
+	
+	public String getTMCIPeerInfo() {
+		return getName()+'\t'+super.getTMCIPeerInfo();
+	}
+	
+	/**
+	 * A method to be called once at the beginning of every time isConnected() is true
+	 */
+	protected void onConnect() {
+		sendQueuedN2NTMs();
+	}
+
+	// File transfer offers
+	// FIXME this should probably be somewhere else, along with the N2NTM stuff... but where?
+	// FIXME this should be persistent across node restarts
+
+	/** Files I have offered to this peer */
+	private final HashMap myFileOffersByUID = new HashMap();
+	/** Files this peer has offered to me */
+	private final HashMap hisFileOffersByUID = new HashMap();
+	
+	private void storeOffers() {
+		// FIXME do something
+	}
+	
+	class FileOffer {
+		final long uid;
+		final String filename;
+		final String mimeType;
+		final String comment;
+		private RandomAccessThing data;
+		final long size;
+		/** Who is offering it? True = I am offering it, False = I am being offered it */
+		final boolean amIOffering;
+		private PartiallyReceivedBulk prb;
+		private BulkTransmitter transmitter;
+		private BulkReceiver receiver;
+		/** True if the offer has either been accepted or rejected */
+		private boolean acceptedOrRejected;
+		
+		FileOffer(long uid, RandomAccessThing data, String filename, String mimeType, String comment) throws IOException {
+			this.uid = uid;
+			this.data = data;
+			this.filename = filename;
+			this.mimeType = mimeType;
+			this.comment = comment;
+			size = data.size();
+			amIOffering = true;
+		}
+
+		public FileOffer(SimpleFieldSet fs, boolean amIOffering) throws FSParseException {
+			uid = fs.getLong("uid");
+			size = fs.getLong("size");
+			mimeType = fs.get("metadata.contentType");
+			filename = FileUtil.sanitize(fs.get("filename"), mimeType);
+			String s = fs.get("comment");
+			if(s != null) {
+				try {
+					s = new String(Base64.decode(s), "UTF-8");
+				} catch (UnsupportedEncodingException e) {
+					throw new Error(e);
+				} catch (IllegalBase64Exception e) {
+					// Maybe it wasn't encoded? FIXME remove
+				}
+			}
+			comment = s;
+			this.amIOffering = amIOffering;
+		}
+
+		public void toFieldSet(SimpleFieldSet fs) {
+			fs.put("uid", uid);
+			fs.putSingle("filename", filename);
+			fs.putSingle("metadata.contentType", mimeType);
+			try {
+				fs.putSingle("comment", Base64.encode(comment.getBytes("UTF-8")));
+			} catch (UnsupportedEncodingException e) {
+				throw new Error(e);
+			}
+			fs.put("size", size);
+		}
+
+		public void accept() {
+			acceptedOrRejected = true;
+			File dest = new File(node.clientCore.downloadDir, "direct-"+FileUtil.sanitize(getName())+"-"+filename);
+			try {
+				data = new RandomAccessFileWrapper(dest, "rw");
+			} catch (FileNotFoundException e) {
+				// Impossible
+				throw new Error("Impossible: FileNotFoundException opening with RAF with rw! "+e, e);
+			}
+			prb = new PartiallyReceivedBulk(node.usm, size, Node.PACKET_SIZE, data, false);
+			receiver = new BulkReceiver(prb, DarknetPeerNode.this, uid);
+			// FIXME make this persistent
+			node.executor.execute(new Runnable() {
+				public void run() {
+					if(logMINOR)
+						Logger.minor(this, "Received file");
+					try {
+						if(!receiver.receive()) {
+							String err = "Failed to receive "+this;
+							Logger.error(this, err);
+							System.err.println(err);
+							onReceiveFailure();
+						} else {
+							onReceiveSuccess();
+						}
+					} catch (Throwable t) {
+						Logger.error(this, "Caught "+t+" receiving file", t);
+						onReceiveFailure();
+					} finally {
+						remove();
+					}
+					if(logMINOR)
+						Logger.minor(this, "Received file");
+				}
+			}, "Receiver for bulk transfer "+uid+":"+filename);
+			sendFileOfferAccepted(uid);
+		}
+
+		protected void remove() {
+			Long l = new Long(uid);
+			synchronized(DarknetPeerNode.this) {
+				myFileOffersByUID.remove(l);
+				hisFileOffersByUID.remove(l);
+			}
+			data.close();
+		}
+
+		public void send() throws DisconnectedException {
+			prb = new PartiallyReceivedBulk(node.usm, size, Node.PACKET_SIZE, data, true);
+			transmitter = new BulkTransmitter(prb, DarknetPeerNode.this, uid, node.outputThrottle);
+			if(logMINOR)
+				Logger.minor(this, "Sending "+uid);
+			node.executor.execute(new Runnable() {
+				public void run() {
+					if(logMINOR)
+						Logger.minor(this, "Sending file");
+					try {
+						if(!transmitter.send()) {
+							String err = "Failed to send "+uid+" for "+FileOffer.this;
+							Logger.error(this, err);
+							System.err.println(err);
+						}
+					} catch (Throwable t) {
+						Logger.error(this, "Caught "+t+" sending file", t);
+						remove();
+					}
+					if(logMINOR)
+						Logger.minor(this, "Sent file");
+				}
+
+			}, "Sender for bulk transfer "+uid+":"+filename);
+		}
+
+		public void reject() {
+			acceptedOrRejected = true;
+			sendFileOfferRejected(uid);
+		}
+
+		public void onRejected() {
+			transmitter.cancel();
+			// FIXME prb's can't be shared, right? Well they aren't here...
+			prb.abort(RetrievalException.CANCELLED_BY_RECEIVER, "Cancelled by receiver");
+		}
+
+		protected void onReceiveFailure() {
+			UserAlert alert = new UserAlert() {
+				public String dismissButtonText() {
+					return L10n.getString("UserAlert.hide");
+				}
+				public HTMLNode getHTMLText() {
+					HTMLNode div = new HTMLNode("div");
+					
+					div.addChild("p", l10n("failedReceiveHeader", new String[] { "filename", "node" },
+							new String[] { filename, getName() }));
+					
+					// Descriptive table
+					
+					HTMLNode table = div.addChild("table", "border", "0");
+					HTMLNode row = table.addChild("tr");
+					row.addChild("td").addChild("#", l10n("fileLabel"));
+					row.addChild("td").addChild("#", filename);
+					row = table.addChild("tr");
+					row.addChild("td").addChild("#", l10n("sizeLabel"));
+					row.addChild("td").addChild("#", SizeUtil.formatSize(size));
+					row = table.addChild("tr");
+					row.addChild("td").addChild("#", l10n("mimeLabel"));
+					row.addChild("td").addChild("#", mimeType);
+					row = table.addChild("tr");
+					row.addChild("td").addChild("#", l10n("senderLabel"));
+					row.addChild("td").addChild("#", getName());
+					row = table.addChild("tr");
+					if(comment != null && comment.length() > 0) {
+						row.addChild("td").addChild("#", l10n("commentLabel"));
+						addComment(row.addChild("td"));
+					}
+					
+					return div;
+				}
+
+				public short getPriorityClass() {
+					return UserAlert.MINOR;
+				}
+
+				public String getText() {
+					StringBuffer sb = new StringBuffer();
+					sb.append(l10n("failedReceiveHeader", new String[] { "filename", "node" },
+							new String[] { filename, getName() }));
+					sb.append('\n');
+					sb.append(l10n("fileLabel"));
+					sb.append(' ');
+					sb.append(filename);
+					sb.append('\n');
+					sb.append(l10n("sizeLabel"));
+					sb.append(' ');
+					sb.append(SizeUtil.formatSize(size));
+					sb.append('\n');
+					sb.append(l10n("mimeLabel"));
+					sb.append(' ');
+					sb.append(mimeType);
+					sb.append('\n');
+					sb.append(l10n("senderLabel"));
+					sb.append(' ');
+					sb.append(getName());
+					sb.append('\n');
+					if(comment != null && comment.length() > 0) {
+						sb.append(l10n("commentLabel"));
+						sb.append(' ');
+						sb.append(comment);
+					}
+					return sb.toString();
+				}
+
+				public String getTitle() {
+					return l10n("failedReceiveTitle");
+				}
+
+				public boolean isValid() {
+					return true;
+				}
+
+				public void isValid(boolean validity) {
+					// Ignore
+				}
+
+				public void onDismiss() {
+					// Ignore
+				}
+
+				public boolean shouldUnregisterOnDismiss() {
+					return true;
+				}
+
+				public boolean userCanDismiss() {
+					return true;
+				}
+				
+			};
+			node.clientCore.alerts.register(alert);
+		}
+
+		private void onReceiveSuccess() {
+			UserAlert alert = new UserAlert() {
+				public String dismissButtonText() {
+					return L10n.getString("UserAlert.hide");
+				}
+				public HTMLNode getHTMLText() {
+					HTMLNode div = new HTMLNode("div");
+					
+					// FIXME localise!!!
+					
+					div.addChild("p", l10n("succeededReceiveHeader", new String[] { "filename", "node" },
+							new String[] { filename, getName() }));
+					
+					// Descriptive table
+					
+					HTMLNode table = div.addChild("table", "border", "0");
+					HTMLNode row = table.addChild("tr");
+					row.addChild("td").addChild("#", l10n("fileLabel"));
+					row.addChild("td").addChild("#", filename);
+					row = table.addChild("tr");
+					row.addChild("td").addChild("#", l10n("sizeLabel"));
+					row.addChild("td").addChild("#", SizeUtil.formatSize(size));
+					row = table.addChild("tr");
+					row.addChild("td").addChild("#", l10n("mimeLabel"));
+					row.addChild("td").addChild("#", mimeType);
+					row = table.addChild("tr");
+					row.addChild("td").addChild("#", l10n("senderLabel"));
+					row.addChild("td").addChild("#", getName());
+					row = table.addChild("tr");
+					if(comment != null && comment.length() > 0) {
+						row.addChild("td").addChild("#", l10n("commentLabel"));
+						addComment(row.addChild("td"));
+					}
+					
+					return div;
+				}
+
+				public short getPriorityClass() {
+					return UserAlert.MINOR;
+				}
+
+				public String getText() {
+					StringBuffer sb = new StringBuffer();
+					sb.append(l10n("succeededReceiveHeader", new String[] { "filename", "node" },
+							new String[] { filename, getName() }));
+					sb.append('\n');
+					sb.append(l10n("fileLabel"));
+					sb.append(' ');
+					sb.append(filename);
+					sb.append('\n');
+					sb.append(l10n("sizeLabel"));
+					sb.append(' ');
+					sb.append(SizeUtil.formatSize(size));
+					sb.append('\n');
+					sb.append(l10n("mimeLabel"));
+					sb.append(' ');
+					sb.append(mimeType);
+					sb.append('\n');
+					sb.append(l10n("senderLabel"));
+					sb.append(' ');
+					sb.append(userToString());
+					sb.append('\n');
+					if(comment != null && comment.length() > 0) {
+						sb.append(l10n("commentLabel"));
+						sb.append(' ');
+						sb.append(comment);
+					}
+					return sb.toString();
+				}
+
+				public String getTitle() {
+					return l10n("succeededReceiveTitle");
+				}
+
+				public boolean isValid() {
+					return true;
+				}
+
+				public void isValid(boolean validity) {
+					// Ignore
+				}
+
+				public void onDismiss() {
+					// Ignore
+				}
+
+				public boolean shouldUnregisterOnDismiss() {
+					return true;
+				}
+
+				public boolean userCanDismiss() {
+					return true;
+				}
+				
+			};
+			node.clientCore.alerts.register(alert);
+		}
+
+		
+		/** Ask the user whether (s)he wants to download a file from a direct peer */
+		public UserAlert askUserUserAlert() {
+			return new UserAlert() {
+				public String dismissButtonText() {
+					return null; // Cannot hide, but can reject
+				}
+				public HTMLNode getHTMLText() {
+					HTMLNode div = new HTMLNode("div");
+					
+					div.addChild("p", l10n("offeredFileHeader", "name", getName()));
+					
+					// Descriptive table
+					
+					HTMLNode table = div.addChild("table", "border", "0");
+					HTMLNode row = table.addChild("tr");
+					row.addChild("td").addChild("#", l10n("fileLabel"));
+					row.addChild("td").addChild("#", filename);
+					row = table.addChild("tr");
+					row.addChild("td").addChild("#", l10n("sizeLabel"));
+					row.addChild("td").addChild("#", SizeUtil.formatSize(size));
+					row = table.addChild("tr");
+					row.addChild("td").addChild("#", l10n("mimeLabel"));
+					row.addChild("td").addChild("#", mimeType);
+					row = table.addChild("tr");
+					row.addChild("td").addChild("#", l10n("senderLabel"));
+					row.addChild("td").addChild("#", getName());
+					row = table.addChild("tr");
+					if(comment != null && comment.length() > 0) {
+						row.addChild("td").addChild("#", l10n("commentLabel"));
+						addComment(row.addChild("td"));
+					}
+					
+					// Accept/reject form
+					
+					// Hopefully we will have a container when this function is called!
+					HTMLNode form = node.clientCore.getToadletContainer().addFormChild(div, "/friends/", "f2fFileOfferAcceptForm");
+					
+					// FIXME node_ is inefficient
+					form.addChild("input", new String[] { "type", "name" },
+							new String[] { "hidden", "node_"+DarknetPeerNode.this.hashCode() });
+
+					form.addChild("input", new String[] { "type", "name", "value" },
+							new String[] { "hidden", "id", Long.toString(uid) });
+					
+					form.addChild("input", new String[] { "type", "name", "value" }, 
+							new String[] { "submit", "acceptTransfer", l10n("acceptTransferButton") });
+
+					form.addChild("input", new String[] { "type", "name", "value" }, 
+							new String[] { "submit", "rejectTransfer", l10n("rejectTransferButton") });
+					
+					return div;
+				}
+				public short getPriorityClass() {
+					return UserAlert.MINOR;
+				}
+				public String getText() {
+					StringBuffer sb = new StringBuffer();
+					sb.append(l10n("offeredFileHeader", "name", getName()));
+					sb.append('\n');
+					sb.append(l10n("fileLabel"));
+					sb.append(' ');
+					sb.append(filename);
+					sb.append('\n');
+					sb.append(l10n("sizeLabel"));
+					sb.append(' ');
+					sb.append(SizeUtil.formatSize(size));
+					sb.append('\n');
+					sb.append(l10n("mimeLabel"));
+					sb.append(' ');
+					sb.append(mimeType);
+					sb.append('\n');
+					sb.append(l10n("senderLabel"));
+					sb.append(' ');
+					sb.append(userToString());
+					sb.append('\n');
+					if(comment != null && comment.length() > 0) {
+						sb.append(l10n("commentLabel"));
+						sb.append(' ');
+						sb.append(comment);
+					}
+					return sb.toString();
+				}
+				public String getTitle() {
+					return l10n("askUserTitle");
+				}
+
+				public boolean isValid() {
+					if(acceptedOrRejected) {
+						node.clientCore.alerts.unregister(this);
+						return false;
+					}
+					return true;
+				}
+				public void isValid(boolean validity) {
+					// Ignore
+				}
+				public void onDismiss() {
+					// Ignore
+				}
+				public boolean shouldUnregisterOnDismiss() {
+					return false;
+				}
+
+				public boolean userCanDismiss() {
+					return false; // should accept or reject
+				}
+			};
+			
+		}
+		protected void addComment(HTMLNode node) {
+			String[] lines = comment.split("\n");
+			for (int i = 0, c = lines.length; i < c; i++) {
+				node.addChild("#", lines[i]);
+				if(i != lines.length - 1)
+					node.addChild("br");
+			}
+		}
+
+		private String l10n(String key) {
+			return L10n.getString("FileOffer."+key);
+		}
+		private String l10n(String key, String pattern, String value) {
+			return L10n.getString("FileOffer."+key, pattern, value);
+		}
+		private String l10n(String key, String[] pattern, String[] value) {
+			return L10n.getString("FileOffer."+key, pattern, value);
+		}
+	}
+
+	public int sendTextMessage(String message) {
+		long now = System.currentTimeMillis();
+		SimpleFieldSet fs = new SimpleFieldSet(true);
+		fs.put("n2nType", Node.N2N_MESSAGE_TYPE_FPROXY);
+		fs.put("type", Node.N2N_TEXT_MESSAGE_TYPE_USERALERT);
+		try {
+			fs.putSingle("source_nodename", Base64.encode(node.getMyName().getBytes("UTF-8")));
+			fs.putSingle("target_nodename", Base64.encode(getName().getBytes("UTF-8")));
+			fs.putSingle("text", Base64.encode(message.getBytes("UTF-8")));
+			fs.put("composedTime", now);
+			fs.put("sentTime", now);
+			Message n2ntm;
+			n2ntm = DMT.createNodeToNodeMessage(
+					Node.N2N_MESSAGE_TYPE_FPROXY, fs
+							.toString().getBytes("UTF-8"));
+			try {
+				sendAsync(n2ntm, null, 0, null);
+			} catch (NotConnectedException e) {
+				fs.removeValue("sentTime");
+				queueN2NTM(fs);
+				setPeerNodeStatus(System.currentTimeMillis());
+				return getPeerNodeStatus();
+			}
+			this.setPeerNodeStatus(System.currentTimeMillis());
+			return getPeerNodeStatus();
+		} catch (UnsupportedEncodingException e) {
+			throw new Error("Impossible: "+e, e);
+		}
+	}
+
+	public int sendFileOfferAccepted(long uid) {
+		storeOffers();
+		long now = System.currentTimeMillis();
+		SimpleFieldSet fs = new SimpleFieldSet(true);
+		fs.put("n2nType", Node.N2N_MESSAGE_TYPE_FPROXY);
+		fs.put("type", Node.N2N_TEXT_MESSAGE_TYPE_FILE_OFFER_ACCEPTED);
+		try {
+			fs.putSingle("source_nodename", Base64.encode(node.getMyName().getBytes("UTF-8")));
+			fs.putSingle("target_nodename", Base64.encode(getName().getBytes("UTF-8")));
+			fs.put("composedTime", now);
+			fs.put("sentTime", now);
+			fs.put("uid", uid);
+			if(logMINOR)
+				Logger.minor(this, "Sending node to node message (file offer accepted):\n"+fs);
+			Message n2ntm;
+			n2ntm = DMT.createNodeToNodeMessage(
+					Node.N2N_MESSAGE_TYPE_FPROXY, fs
+							.toString().getBytes("UTF-8"));
+			try {
+				sendAsync(n2ntm, null, 0, null);
+			} catch (NotConnectedException e) {
+				fs.removeValue("sentTime");
+				queueN2NTM(fs);
+				setPeerNodeStatus(System.currentTimeMillis());
+				return getPeerNodeStatus();
+			}
+			this.setPeerNodeStatus(System.currentTimeMillis());
+			return getPeerNodeStatus();
+		} catch (UnsupportedEncodingException e) {
+			throw new Error("Impossible: "+e, e);
+		}
+	}
+
+	public int sendFileOfferRejected(long uid) {
+		storeOffers();
+		long now = System.currentTimeMillis();
+		SimpleFieldSet fs = new SimpleFieldSet(true);
+		fs.put("n2nType", Node.N2N_MESSAGE_TYPE_FPROXY);
+		fs.put("type", Node.N2N_TEXT_MESSAGE_TYPE_FILE_OFFER_REJECTED);
+		try {
+			fs.putSingle("source_nodename", Base64.encode(node.getMyName().getBytes("UTF-8")));
+			fs.putSingle("target_nodename", Base64.encode(getName().getBytes("UTF-8")));
+			fs.put("composedTime", now);
+			fs.put("sentTime", now);
+			fs.put("uid", uid);
+			if(logMINOR)
+				Logger.minor(this, "Sending node to node message (file offer rejected):\n"+fs);
+			Message n2ntm;
+			n2ntm = DMT.createNodeToNodeMessage(
+					Node.N2N_MESSAGE_TYPE_FPROXY, fs
+							.toString().getBytes("UTF-8"));
+			try {
+				sendAsync(n2ntm, null, 0, null);
+			} catch (NotConnectedException e) {
+				fs.removeValue("sentTime");
+				queueN2NTM(fs);
+				setPeerNodeStatus(System.currentTimeMillis());
+				return getPeerNodeStatus();
+			}
+			this.setPeerNodeStatus(System.currentTimeMillis());
+			return getPeerNodeStatus();
+		} catch (UnsupportedEncodingException e) {
+			throw new Error("Impossible: "+e, e);
+		}
+	}
+
+	public int sendFileOffer(File filename, String message) throws IOException {
+		String fnam = filename.getName();
+		String mime = DefaultMIMETypes.guessMIMEType(fnam, false);
+		long uid = node.random.nextLong();
+		RandomAccessThing data = new RandomAccessFileWrapper(filename, "r");
+		FileOffer fo = new FileOffer(uid, data, fnam, mime, message);
+		synchronized(this) {
+			myFileOffersByUID.put(new Long(uid), fo);
+		}
+		storeOffers();
+		long now = System.currentTimeMillis();
+		SimpleFieldSet fs = new SimpleFieldSet(true);
+		fs.put("n2nType", Node.N2N_MESSAGE_TYPE_FPROXY);
+		fs.put("type", Node.N2N_TEXT_MESSAGE_TYPE_FILE_OFFER);
+		try {
+			fs.putSingle("source_nodename", Base64.encode(node.getMyName().getBytes("UTF-8")));
+			fs.putSingle("target_nodename", Base64.encode(getName().getBytes("UTF-8")));
+			fs.put("composedTime", now);
+			fs.put("sentTime", now);
+			fo.toFieldSet(fs);
+			if(logMINOR)
+				Logger.minor(this, "Sending node to node message (file offer):\n"+fs);
+			Message n2ntm;
+			int status = getPeerNodeStatus();
+			n2ntm = DMT.createNodeToNodeMessage(
+					Node.N2N_MESSAGE_TYPE_FPROXY, fs
+							.toString().getBytes("UTF-8"));
+			try {
+				sendAsync(n2ntm, null, 0, null);
+			} catch (NotConnectedException e) {
+				fs.removeValue("sentTime");
+				queueN2NTM(fs);
+				setPeerNodeStatus(System.currentTimeMillis());
+				return getPeerNodeStatus();
+			}
+			return status;
+		} catch (UnsupportedEncodingException e) {
+			throw new Error("Impossible: "+e, e);
+		}
+	}
+
+	public void handleFproxyN2NTM(SimpleFieldSet fs, int fileNumber) {
+		String source_nodename = null;
+		String target_nodename = null;
+		String text = null;
+		long composedTime;
+		long sentTime;
+		long receivedTime;
+	  	try {
+			source_nodename = new String(Base64.decode(fs.get("source_nodename")));
+			target_nodename = new String(Base64.decode(fs.get("target_nodename")));
+			text = new String(Base64.decode(fs.get("text")));
+			composedTime = fs.getLong("composedTime", -1);
+			sentTime = fs.getLong("sentTime", -1);
+			receivedTime = fs.getLong("receivedTime", -1);
+		} catch (IllegalBase64Exception e) {
+			Logger.error(this, "Bad Base64 encoding when decoding a N2NTM SimpleFieldSet", e);
+			return;
+		}
+		N2NTMUserAlert userAlert = new N2NTMUserAlert(this, source_nodename, target_nodename, text, fileNumber, composedTime, sentTime, receivedTime);
+		node.clientCore.alerts.register(userAlert);
+	}
+
+	public void handleFproxyFileOffer(SimpleFieldSet fs, int fileNumber) {
+		final FileOffer offer;
+		try {
+			offer = new FileOffer(fs, false);
+		} catch (FSParseException e) {
+			Logger.error(this, "Could not parse offer: "+e+" on "+this+" :\n"+fs, e);
+			return;
+		}
+		Long u = new Long(offer.uid);
+		synchronized(this) {
+			if(hisFileOffersByUID.containsKey(u)) return; // Ignore re-advertisement
+			hisFileOffersByUID.put(u, offer);
+		}
+		
+		// Don't persist for now - FIXME
+		this.deleteExtraPeerDataFile(fileNumber);
+		
+		UserAlert alert = offer.askUserUserAlert();
+			
+		node.clientCore.alerts.register(alert);
+	}
+
+	public void acceptTransfer(long id) {
+		if(logMINOR)
+			Logger.minor(this, "Accepting transfer "+id+" on "+this);
+		FileOffer fo;
+		synchronized(this) {
+			fo = (FileOffer) hisFileOffersByUID.get(new Long(id));
+		}
+		fo.accept();
+	}
+	
+	public void rejectTransfer(long id) {
+		FileOffer fo;
+		synchronized(this) {
+			fo = (FileOffer) hisFileOffersByUID.remove(new Long(id));
+		}
+		fo.reject();
+	}
+	
+	public void handleFproxyFileOfferAccepted(SimpleFieldSet fs, int fileNumber) {
+		// Don't persist for now - FIXME
+		this.deleteExtraPeerDataFile(fileNumber);
+		
+		long uid;
+		try {
+			uid = fs.getLong("uid");
+		} catch (FSParseException e) {
+			Logger.error(this, "Could not parse offer accepted: "+e+" on "+this+" :\n"+fs, e);
+			return;
+		}
+		if(logMINOR)
+			Logger.minor(this, "Offer accepted for "+uid);
+		Long u = new Long(uid);
+		FileOffer fo;
+		synchronized(this) {
+			fo = (FileOffer) (myFileOffersByUID.get(u));
+		}
+		if(fo == null) {
+			Logger.error(this, "No such offer: "+uid);
+			try {
+				sendAsync(DMT.createFNPBulkSendAborted(uid), null, fileNumber, null);
+			} catch (NotConnectedException e) {
+				// Fine by me!
+			}
+			return;
+		}
+		try {
+			fo.send();
+		} catch (DisconnectedException e) {
+			Logger.error(this, "Cannot send because node disconnected: "+e+" for "+uid+":"+fo.filename, e);
+		}
+	}
+
+	public void handleFproxyFileOfferRejected(SimpleFieldSet fs, int fileNumber) {
+		// Don't persist for now - FIXME
+		this.deleteExtraPeerDataFile(fileNumber);
+		
+		long uid;
+		try {
+			uid = fs.getLong("uid");
+		} catch (FSParseException e) {
+			Logger.error(this, "Could not parse offer rejected: "+e+" on "+this+" :\n"+fs, e);
+			return;
+		}
+		
+		FileOffer fo;
+		synchronized(this) {
+			fo = (FileOffer) myFileOffersByUID.remove(new Long(uid));
+		}
+		fo.onRejected();
+	}
+
+	public String userToString() {
+		return ""+getPeer()+" : "+getName();
+	}
+
+	protected synchronized boolean innerCalcNextHandshake(boolean successfulHandshakeSend, boolean dontFetchARK, long now) {
+		if(isBurstOnly) {
+			boolean fetchARKFlag = false;
+			listeningHandshakeBurstCount++;
+			if(verifiedIncompatibleOlderVersion || verifiedIncompatibleNewerVersion) { 
+				// Let them know we're here, but have no hope of connecting
+				// Send one packet only.
+				listeningHandshakeBurstCount = 0;
+			} else if(listeningHandshakeBurstCount >= listeningHandshakeBurstSize) {
+				listeningHandshakeBurstCount = 0;
+				fetchARKFlag = true;
+			}
+			if(listeningHandshakeBurstCount == 0) {  // 0 only if we just reset it above
+				sendHandshakeTime = now + Node.MIN_TIME_BETWEEN_BURSTING_HANDSHAKE_BURSTS
+					+ node.random.nextInt(Node.RANDOMIZED_TIME_BETWEEN_BURSTING_HANDSHAKE_BURSTS);
+				listeningHandshakeBurstSize = Node.MIN_BURSTING_HANDSHAKE_BURST_SIZE
+						+ node.random.nextInt(Node.RANDOMIZED_BURSTING_HANDSHAKE_BURST_SIZE);
+				isBursting = false;
+			} else {
+				sendHandshakeTime = now + Node.MIN_TIME_BETWEEN_HANDSHAKE_SENDS
+					+ node.random.nextInt(Node.RANDOMIZED_TIME_BETWEEN_HANDSHAKE_SENDS);
+			}
+			if(logMINOR) Logger.minor(this, "Next BurstOnly mode handshake in "+(sendHandshakeTime - now)+"ms for "+getName()+" (count: "+listeningHandshakeBurstCount+", size: "+listeningHandshakeBurstSize+ ')', new Exception("double-called debug"));
+			return fetchARKFlag;
+		} else {
+			return super.innerCalcNextHandshake(successfulHandshakeSend, dontFetchARK, now);
+		}
+	}
+
+	public PeerNodeStatus getStatus() {
+		return new DarknetPeerNodeStatus(this);
+	}
+
+	public boolean isOpennet() {
+		return false;
+	}
+
+	public void onSuccess(boolean insert, boolean ssk) {
+		// Ignore it
+	}
+
+	public void onRemove() {
+		// Do nothing
+		// FIXME is there anything we should do?
+	}
+}

Copied: branches/freenet-jfk/src/freenet/node/DarknetPeerNodeStatus.java (from rev 14796, trunk/freenet/src/freenet/node/DarknetPeerNodeStatus.java)
===================================================================
--- branches/freenet-jfk/src/freenet/node/DarknetPeerNodeStatus.java	                        (rev 0)
+++ branches/freenet-jfk/src/freenet/node/DarknetPeerNodeStatus.java	2007-08-20 14:10:51 UTC (rev 14803)
@@ -0,0 +1,62 @@
+package freenet.node;
+
+public class DarknetPeerNodeStatus extends PeerNodeStatus {
+
+	private final String name;
+
+	private final boolean burstOnly;
+
+	private final boolean listening;
+
+	private final boolean disabled;
+
+	private final String privateDarknetCommentNote;
+	
+	public DarknetPeerNodeStatus(DarknetPeerNode peerNode) {
+		super(peerNode);
+		this.name = peerNode.getName();
+		this.burstOnly = peerNode.isBurstOnly();
+		this.listening = peerNode.isListenOnly();
+		this.disabled = peerNode.isDisabled();
+		this.privateDarknetCommentNote = peerNode.getPrivateDarknetCommentNote();
+	}
+	
+	/**
+	 * @return the name
+	 */
+	public String getName() {
+		return name;
+	}
+
+	/**
+	 * @return the burstOnly
+	 */
+	public boolean isBurstOnly() {
+		return burstOnly;
+	}
+
+	/**
+	 * @return the disabled
+	 */
+	public boolean isDisabled() {
+		return disabled;
+	}
+
+	/**
+	 * @return the listening
+	 */
+	public boolean isListening() {
+		return listening;
+	}
+
+	/**
+	 * @return the privateDarknetCommentNote
+	 */
+	public String getPrivateDarknetCommentNote() {
+		return privateDarknetCommentNote;
+	}
+
+	public String toString() {
+		return name + ' ' + super.toString();
+	}
+}

Copied: branches/freenet-jfk/src/freenet/node/FailureTable.java (from rev 14796, trunk/freenet/src/freenet/node/FailureTable.java)
===================================================================
--- branches/freenet-jfk/src/freenet/node/FailureTable.java	                        (rev 0)
+++ branches/freenet-jfk/src/freenet/node/FailureTable.java	2007-08-20 14:10:51 UTC (rev 14803)
@@ -0,0 +1,267 @@
+/* This code is part of Freenet. It is distributed under the GNU General
+ * Public License, version 2 (or at your option any later version). See
+ * http://www.gnu.org/ for further details of the GPL. */
+package freenet.node;
+
+import java.lang.ref.WeakReference;
+
+import freenet.keys.Key;
+import freenet.keys.KeyBlock;
+import freenet.keys.NodeCHK;
+import freenet.support.LRUHashtable;
+
+/**
+ * Tracks recently DNFed keys, where they were routed to, what the location was at the time, who requested them.
+ * Implements Ultra-Lightweight Persistent Requests: Refuse requests for a key for 10 minutes after it's DNFed 
+ * (UNLESS we find a better route for the request), and when it is found, offer it to those who've asked for it
+ * in the last hour.
+ * @author toad
+ */
+public class FailureTable {
+	
+	/** FailureTableEntry's by key. Note that we push an entry only when sentTime changes. */
+	private final LRUHashtable entriesByKey;
+	/** BlockOfferList by key */
+	private final LRUHashtable blockOfferListByKey;
+	/** Peers */
+	private final PeerManager peers;
+	private final Node node;
+	
+	/** Maximum number of keys to track */
+	static final int MAX_ENTRIES = 20*1000;
+	/** Maximum number of offers to track */
+	static final int MAX_OFFERS = 10*1000;
+	/** Terminate a request if there was a DNF on the same key less than 10 minutes ago */
+	static final int REJECT_TIME = 10*60*1000;
+	/** After 1 hour we forget about an entry completely */
+	static final int MAX_LIFETIME = 60*60*1000;
+	/** Offers expire after 10 minutes */
+	static final int OFFER_EXPIRY_TIME = 10*60*1000;
+	
+	FailureTable(PeerManager peers, Node node) {
+		entriesByKey = new LRUHashtable();
+		blockOfferListByKey = new LRUHashtable();
+		this.peers = peers;
+		this.node = node;
+	}
+	
+	/**
+	 * Called when a key DNFs, or is killed by a RecentlyFailed message. Either way this can create a 
+	 * FailureTableEntry.
+	 * @param key The key that was fetched.
+	 * @param htl The HTL it was fetched at.
+	 * @param requestors The nodes requesting it (if any).
+	 * @param requested The single node it was forwarded to, which DNFed.
+	 * @param now The time at which the request was sent.
+	 * @param timeout The number of millis from when the request was sent to when the failure block times out.
+	 * I.e. between 0 and REJECT_TIME.
+	 */
+	public void onFailure(Key key, short htl, PeerNode[] requestors, PeerNode requested, int timeout, long now) {
+		FailureTableEntry entry;
+		synchronized(this) {
+			entry = (FailureTableEntry) entriesByKey.get(key);
+			if(entry == null) {
+				entry = new FailureTableEntry(key, htl, requestors, requested);
+				entriesByKey.push(key, entry);
+				return;
+			} else {
+				entriesByKey.push(key, entry);
+			}
+			trimEntries(now);
+		}
+		entry.onFailure(htl, requestors, requested, timeout, now);
+	}
+	
+	private void trimEntries(long now) {
+		while(entriesByKey.size() > MAX_ENTRIES) {
+			entriesByKey.popKey();
+		}
+		while(true) {
+			FailureTableEntry e = (FailureTableEntry) entriesByKey.peekValue();
+			if(now - e.creationTime > MAX_LIFETIME) entriesByKey.popKey();
+			else break;
+		}
+	}
+
+	/**
+	 * Called when a request is made. Determine whether we should fail the request, and add the requestors to the list
+	 * of interested nodes.
+	 * @param key The key to fetch.
+	 * @param htl The HTL it will be fetched at.
+	 * @param requestor The node requesting it.
+	 * @return True if the request should be failed with an FNPRecentlyFailed.
+	 */
+	public synchronized boolean shouldFail(Key key, short htl, PeerNode requestor) {
+		long now = System.currentTimeMillis();
+		FailureTableEntry entry = (FailureTableEntry) entriesByKey.get(key);
+		if(entry == null) {
+			// Don't know anything about the key
+			return false;
+		}
+		entry.addRequestors(new PeerNode[] { requestor }, now);
+		if(htl > entry.htl) {
+			// If the HTL is higher this time, let it through
+			entriesByKey.push(key, entry);
+			return false;
+		}
+		if(now > entry.timeoutTime) {
+			// If it's more than 10 minutes since we sent a request, let it through
+			return false;
+		}
+		/*
+		 * If the best node available now is closer than the best location we have routed to so far, out of those
+		 * nodes which are still connected, then accept the request.
+		 * 
+		 * Note that this means we can route to the same node twice - but only if its location improves.
+		 */
+		double bestLiveLocDiff = entry.bestLiveLocDiff();
+		
+		PeerNode p = peers.closerPeer(requestor, null, null, key.toNormalizedDouble(), true, false, 0, null, bestLiveLocDiff);
+		
+		if(p != null) return false; // there is a better route now / we want to retry an old one
+		
+		return true; // kill the request
+	}
+	
+	private final class BlockOfferList {
+		private BlockOffer[] offers;
+		final FailureTableEntry entry;
+		
+		BlockOfferList(FailureTableEntry entry, BlockOffer offer) {
+			this.entry = entry;
+			this.offers = new BlockOffer[] { offer };
+		}
+
+		public long expires() {
+			long last = 0;
+			for(int i=0;i<offers.length;i++) {
+				if(offers[i].offeredTime > last) last = offers[i].offeredTime;
+			}
+			return last;
+		}
+
+		public boolean isEmpty(long now) {
+			for(int i=0;i<offers.length;i++) {
+				if(offers[i].offeredTime > now) return false;
+			}
+			return true;
+		}
+	}
+	
+	private final class BlockOffer {
+		final long offeredTime;
+		/** Either offered by or offered to this node */
+		final WeakReference nodeRef;
+		
+		BlockOffer(PeerNode pn, long now) {
+			this.nodeRef = pn.myRef;
+			this.offeredTime = now;
+		}
+	}
+	
+	/**
+	 * Called when a data block is found (after it has been stored; there is a good chance of its being available in the
+	 * near future). If there are nodes waiting for it, we will offer it to them.
+	 */
+	public void onFound(KeyBlock block) {
+		Key key = block.getKey();
+		FailureTableEntry entry;
+		synchronized(this) {
+			entry = (FailureTableEntry) entriesByKey.get(key);
+			if(entry == null) return; // Nobody cares
+			entriesByKey.removeKey(key);
+		}
+		entry.offer();
+	}
+	
+	/**
+	 * Called when we get an offer for a key. If this is an SSK, we will only accept it if we have previously asked for it.
+	 * If it is a CHK, we will accept it if we want it.
+	 * @param key The key we are being offered.
+	 * @param peer The node offering it.
+	 */
+	public void onOffer(Key key, PeerNode peer) {
+		if(wantOffer(key, peer)) {
+			// Okay, we want the offer. Now what?
+			// Two ClientRequestScheduler's? Then you'd have to remove the key from two different RGA's :(
+			// Anyway, we don't want a key to be requested just because another key in the same group has an offer.
+			// So what we want is a list of keys at each level which have been offered.
+			// These would be considered before the other keys at that level, but only if the offer is still valid.
+		}
+	}
+	
+	boolean wantOffer(Key key, PeerNode peer) {
+		FailureTableEntry entry;
+		long now = System.currentTimeMillis();
+		synchronized(this) {
+			entry = (FailureTableEntry) entriesByKey.get(key);
+			if(entry == null) return false; // we haven't asked for it
+			
+			/*
+			 * Accept (subject to later checks) if we asked for it.
+			 * Should we accept it if we were asked for it? This is "bidirectional propagation".
+			 * It's good because it makes the whole structure much more reliable; it's bad because
+			 * it's not entirely under our control - we didn't choose to route it to the node, the node
+			 * routed it to us. Now it's found it before we did...
+			 * 
+			 * Attacks:
+			 * - Frost spamming etc: Is it easier to offer data to our peers rather than inserting it? Will
+			 * it result in it being propagated further? The peer node would then do the request, rather than
+			 * this node doing an insert. Is that beneficial?
+			 * 
+			 * Not relevant with CHKs anyway.
+			 * 
+			 * On the plus side, propagation to nodes that have asked is worthwhile because reduced polling 
+			 * cost enables more secure messaging systems e.g. outbox polling...
+			 * - Social engineering: If a key is unpopular, you can put a different copy of it on different 
+			 * nodes. You can then use this to trace the requestor - identify that he is or isn't on the target. 
+			 * You can't do this with a regular insert because it will often go several nodes even at htl 0. 
+			 * With subscriptions, you might be able to bypass this - but only if you know no other nodes in the
+			 * neighbourhood are subscribed. Easier with SSKs; with CHKs you have only binary information of 
+			 * whether the person got the key (with social engineering). Hard to exploit on darknet; if you're 
+			 * that close to the suspect there are easier ways to get at them e.g. correlation attacks.
+			 * 
+			 * Conclusion: We should accept the request if:
+			 * - We asked for it from that node. (Note that a node might both have asked us and been asked).
+			 * - That node asked for it, and it's a CHK.
+			 */
+			
+			if(!(entry.askedFromPeer(peer, now) || 
+					((key instanceof NodeCHK) && entry.askedByPeer(peer, now)))) {
+				if(entry.isEmpty(now)) entriesByKey.removeKey(key);
+				return false;
+			}
+			if(entry.isEmpty(now)) entriesByKey.removeKey(key);
+			
+			// Valid offer.
+			
+			// Add to offers list
+			
+			BlockOfferList bl = (BlockOfferList) blockOfferListByKey.get(key);
+			BlockOffer offer = new BlockOffer(peer, now);
+			if(bl == null) {
+				bl = new BlockOfferList(entry, offer);
+			}
+			blockOfferListByKey.push(key, offer);
+			trimOffersList(now);
+		}
+		
+		// Now, does anyone want it?
+		// Firstly, do we want it?
+		if(!node.clientCore.clientWantKey(key)) return true;
+		if(entry.othersWant(peer)) return true;
+		return false;
+	}
+
+	private synchronized void trimOffersList(long now) {
+		while(true) {
+			if(blockOfferListByKey.isEmpty()) return;
+			BlockOfferList bl = (BlockOfferList) blockOfferListByKey.peekValue();
+			if(bl.isEmpty(now) || bl.expires() < now || blockOfferListByKey.size() > MAX_OFFERS) {
+				blockOfferListByKey.popKey();
+			} else {
+				return;
+			}
+		}
+	}
+}

Copied: branches/freenet-jfk/src/freenet/node/FailureTableEntry.java (from rev 14796, trunk/freenet/src/freenet/node/FailureTableEntry.java)
===================================================================
--- branches/freenet-jfk/src/freenet/node/FailureTableEntry.java	                        (rev 0)
+++ branches/freenet-jfk/src/freenet/node/FailureTableEntry.java	2007-08-20 14:10:51 UTC (rev 14803)
@@ -0,0 +1,394 @@
+/**
+ * 
+ */
+package freenet.node;
+
+import java.lang.ref.WeakReference;
+
+import freenet.keys.Key;
+
+class FailureTableEntry {
+	
+	/** The key */
+	Key key; // FIXME should this be stored compressed somehow e.g. just the routing key?
+	/** The HTL at which it was requested last time. Any request of higher HTL will be let through. */
+	short htl;
+	/** Time of creation of this entry */
+	long creationTime;
+	/** Time we last received a request for the key */
+	long receivedTime;
+	/** Time we last received a DNF after sending a request for a key */
+	long sentTime;
+	/** Time at which we can send a request again */
+	long timeoutTime;
+	/** WeakReference's to PeerNode's who have requested the key */
+	WeakReference[] requestorNodes;
+	/** Times at which they requested it */
+	long[] requestorTimes;
+	/** Boot ID when they requested it. We don't send it to restarted nodes, as a 
+	 * (weak, but useful if combined with other measures) protection against seizure. */
+	long[] requestorBootIDs;
+	/** WeakReference's to PeerNode's we have requested it from */
+	WeakReference[] requestedNodes;
+	/** Their locations when we requested it */
+	double[] requestedLocs;
+	long[] requestedBootIDs;
+	long[] requestedTimes;
+	
+	/** We remember that a node has asked us for a key for up to an hour; after that, we won't offer the key, and
+	 * if we receive an offer from that node, we will reject it */
+	static final int MAX_TIME_BETWEEN_REQUEST_AND_OFFER = 60 * 60 * 1000;
+	
+	FailureTableEntry(Key key2, short htl2, PeerNode[] requestors, PeerNode requested) {
+		long now = System.currentTimeMillis();
+		this.key = key2;
+		this.htl = htl2;
+		creationTime = now;
+		receivedTime = now;
+		sentTime = now;
+		requestorNodes = new WeakReference[requestors.length];
+		requestorTimes = new long[requestors.length];
+		requestorBootIDs = new long[requestors.length];
+		for(int i=0;i<requestorNodes.length;i++) {
+			requestorNodes[i] = requestors[i].myRef;
+			requestorTimes[i] = now;
+			requestorBootIDs[i] = requestors[i].getBootID();
+		}
+		requestedNodes = new WeakReference[] { requested.myRef };
+		requestedLocs = new double[] { requested.getLocation() };
+		requestedBootIDs = new long[] { requested.getBootID() };
+	}
+	
+	/**
+	 * Called when there is a failure which could cause a block to be added: Either a DataNotFound, or a
+	 * RecentlyFailed.
+	 * @param htl2
+	 * @param requestors
+	 * @param requested
+	 */
+	public void onFailure(short htl2, PeerNode[] requestors, PeerNode requested, int timeout, long now) {
+		synchronized(this) {
+			long newTimeoutTime = now + timeout;
+			if(now > timeoutTime /* has expired */ || newTimeoutTime > timeoutTime) {
+				htl = htl2;
+				timeoutTime = newTimeoutTime;
+			}
+			addRequestors(requestors, now);
+			addRequestedFrom(new PeerNode[] { requested }, now);
+		}
+	}
+
+	// These are rather low level, in an attempt to absolutely minimize memory usage...
+	// The two methods have almost identical code/logic.
+	// Dunno if there's a more elegant way of dealing with this which doesn't significantly increase
+	// per entry byte cost.
+	// Note also this will generate some churn...
+	
+	synchronized void addRequestors(PeerNode[] requestors, long now) {
+		receivedTime = now;
+		int notIncluded = 0;
+		int nulls = 0;
+		int ptr = 0;
+		for(int i=0;i<requestors.length;i++) {
+			PeerNode req = requestors[i];
+			boolean requestorIncluded = false;
+			for(int j=0;j<requestorNodes.length;j++) {
+				PeerNode got = requestorNodes[i] == null ? null : (PeerNode) requestorNodes[i].get();
+				// No longer subscribed if they have rebooted, or expired
+				if(got.getBootID() != requestorBootIDs[i] ||
+						now - requestorTimes[i] > MAX_TIME_BETWEEN_REQUEST_AND_OFFER) {
+					requestorNodes[i] = null;
+					got = null;
+				}
+				if(got == null)
+					nulls++;
+				if(got == req) {
+					// Update existing entry
+					requestorIncluded = true;
+					requestorTimes[j] = now;
+					requestorBootIDs[j] = req.getBootID();
+					break;
+				}
+			}
+			if(!requestorIncluded) {
+				notIncluded++;
+				requestors[ptr++] = requestors[i];
+			} // if it's new, keep it in requestors
+		}
+		for(int i=ptr;i<requestors.length;i++) requestors[i] = null;
+		if(notIncluded == 0 && nulls == 0) return;
+		// Because weak, these can become null; doesn't matter, but we want to minimise memory usage
+		if(notIncluded == nulls) {
+			// Nice special case
+			int x = 0;
+			for(int i=0;i<requestorNodes.length;i++) {
+				if(requestorNodes[i] == null || requestorNodes[i].get() == null) {
+					PeerNode pn = requestors[x++];
+					requestorNodes[i] = pn.myRef;
+					requestorTimes[i] = now;
+					requestorBootIDs[i] = pn.getBootID();
+					if(x == ptr) break;
+				}
+			}
+			return;
+		}
+		WeakReference[] newRequestorNodes = new WeakReference[requestorNodes.length+notIncluded-nulls];
+		long[] newRequestorTimes = new long[requestorNodes.length+notIncluded-nulls];
+		long[] newRequestorBootIDs = new long[requestorNodes.length+notIncluded-nulls];
+		int toIndex = 0;
+		
+		for(int i=0;i<requestorNodes.length;i++) {
+			WeakReference ref = requestorNodes[i];
+			if(ref == null || ref.get() == null) continue;
+			newRequestorNodes[toIndex] = requestorNodes[i];
+			newRequestorTimes[toIndex] = requestorTimes[i];
+			newRequestorBootIDs[toIndex] = requestorBootIDs[i];
+			toIndex++;
+		}
+		
+		for(int fromIndex=0;fromIndex<ptr;fromIndex++) {
+			PeerNode pn = requestors[fromIndex];
+			if(pn != null) {
+				newRequestorNodes[toIndex] = pn.myRef;
+				newRequestorTimes[toIndex] = now;
+				newRequestorBootIDs[toIndex] = pn.getBootID();
+				toIndex++;
+			}
+		}
+		
+		for(int i=toIndex;i<newRequestorNodes.length;i++) newRequestorNodes[i] = null;
+		if(toIndex > newRequestorNodes.length + 2) {
+			WeakReference[] newNewRequestorNodes = new WeakReference[toIndex];
+			long[] newNewRequestorTimes = new long[toIndex];
+			long[] newNewRequestorBootIDs = new long[toIndex];
+			System.arraycopy(newRequestorNodes, 0, newNewRequestorNodes, 0, toIndex);
+			System.arraycopy(newRequestorTimes, 0, newNewRequestorTimes, 0, toIndex);
+			System.arraycopy(newRequestorBootIDs, 0, newNewRequestorBootIDs, 0, toIndex);
+			newRequestorNodes = newNewRequestorNodes;
+			newRequestorTimes = newNewRequestorTimes;
+			newRequestorBootIDs = newNewRequestorBootIDs;
+		}
+		requestorNodes = newRequestorNodes;
+		requestorTimes = newRequestorTimes;
+		requestorBootIDs = newRequestorBootIDs;
+	}
+
+	private synchronized void addRequestedFrom(PeerNode[] requestedFrom, long now) {
+		sentTime = now;
+		int notIncluded = 0;
+		int nulls = 0;
+		int ptr = 0;
+		for(int i=0;i<requestedFrom.length;i++) {
+			PeerNode req = requestedFrom[i];
+			boolean requestorIncluded = false;
+			for(int j=0;j<requestedNodes.length;j++) {
+				PeerNode got = requestedNodes[i] == null ? null : (PeerNode) requestedNodes[i].get();
+				if(got == null)
+					nulls++;
+				if(got == req) {
+					// Update existing entry
+					requestorIncluded = true;
+					requestedLocs[j] = req.getLocation();
+					requestedBootIDs[j] = req.getBootID();
+					requestedTimes[j] = now;
+					break;
+				}
+			}
+			if(!requestorIncluded) {
+				notIncluded++;
+				requestedFrom[ptr++] = requestedFrom[i];
+			} // if it's new, keep it in requestedFrom, otherwise delete it
+		}
+		for(int i=ptr;i<requestedFrom.length;i++) requestedFrom[i] = null;
+		if(notIncluded == 0 && nulls == 0) return;
+		// Because weak, these can become null; doesn't matter, but we want to minimise memory usage
+		if(notIncluded == nulls) {
+			// Nice special case
+			int x = 0;
+			for(int i=0;i<requestedNodes.length;i++) {
+				if(requestedNodes[i] == null || requestedNodes[i].get() == null) {
+					PeerNode pn = requestedFrom[x++];
+					requestedNodes[i] = pn.myRef;
+					requestedLocs[i] = pn.getLocation();
+					requestedTimes[i] = now;
+					if(x == ptr) break;
+				}
+			}
+			return;
+		}
+		WeakReference[] newRequestedNodes = new WeakReference[requestedNodes.length+notIncluded-nulls];
+		double[] newRequestedLocs = new double[requestedNodes.length+notIncluded-nulls];
+		long[] newRequestedBootIDs = new long[requestedNodes.length+notIncluded-nulls];
+		long[] newRequestedTimes = new long[requestedNodes.length+notIncluded-nulls];
+
+		int toIndex = 0;
+		for(int i=0;i<requestorNodes.length;i++) {
+			WeakReference ref = requestorNodes[i];
+			if(ref == null || ref.get() == null) continue;
+			newRequestedNodes[toIndex] = requestedNodes[i];
+			newRequestedTimes[toIndex] = requestedTimes[i];
+			newRequestedBootIDs[toIndex] = requestedBootIDs[i];
+			newRequestedLocs[toIndex] = requestedLocs[i];
+			toIndex++;
+		}
+		
+		for(int fromIndex=0;fromIndex<ptr;fromIndex++) {
+			PeerNode pn = requestedFrom[fromIndex];
+			if(pn != null) {
+				newRequestedNodes[toIndex] = pn.myRef;
+				newRequestedTimes[toIndex] = now;
+				newRequestedBootIDs[toIndex] = pn.getBootID();
+				newRequestedLocs[toIndex] = pn.getLocation();
+				toIndex++;
+			}
+		}
+		
+		for(int i=toIndex;i<newRequestedNodes.length;i++) newRequestedNodes[i] = null;
+		if(toIndex > newRequestedNodes.length + 2) {
+			WeakReference[] newNewRequestedNodes = new WeakReference[toIndex];
+			double[] newNewRequestedLocs = new double[toIndex];
+			long[] newNewRequestedBootIDs = new long[toIndex];
+			long[] newNewRequestedTimes = new long[toIndex];
+			System.arraycopy(newRequestedNodes, 0, newNewRequestedNodes, 0, toIndex);
+			System.arraycopy(newRequestedLocs, 0, newNewRequestedLocs, 0, toIndex);
+			System.arraycopy(newRequestedBootIDs, 0, newNewRequestedBootIDs, 0, toIndex);
+			System.arraycopy(newRequestedTimes, 0, newNewRequestedTimes, 0, toIndex);
+			newRequestedNodes = newNewRequestedNodes;
+			newRequestedLocs = newNewRequestedLocs;
+			newRequestedBootIDs = newNewRequestedBootIDs;
+			newRequestedTimes = newNewRequestedTimes;
+		}
+		requestedNodes = newRequestedNodes;
+		requestedLocs = newRequestedLocs;
+		requestedBootIDs = newRequestedBootIDs;
+		requestedTimes = newRequestedTimes;
+	}
+
+	public synchronized double bestLiveLocDiff() {
+		double bestDiff = 2.0;
+		for(int i=0;i<requestedNodes.length;i++) {
+			if(requestedNodes[i] == null) continue;
+			PeerNode pn = (PeerNode) requestedNodes[i].get();
+			if(pn == null) continue;
+			if(!(pn.isRoutable() && pn.isRoutingBackedOff())) continue;
+			double diff = Location.distance(key.toNormalizedDouble(), requestedLocs[i]);
+			if(diff < bestDiff) bestDiff = diff;
+		}
+		return bestDiff;
+	}
+
+	/** Offer this key to all the nodes that have requested it, and all the nodes it has been requested from.
+	 * Called after a) the data has been stored, and b) this entry has been removed from the FT */
+	public void offer() {
+		for(int i=0;i<requestorNodes.length;i++) {
+			WeakReference ref = requestorNodes[i];
+			if(ref == null) continue;
+			PeerNode pn = (PeerNode) ref.get();
+			if(pn == null) continue;
+			if(pn.getBootID() != requestorBootIDs[i]) continue;
+			pn.offer(key);
+		}
+		for(int i=0;i<requestedNodes.length;i++) {
+			WeakReference ref = requestedNodes[i];
+			if(ref == null) continue;
+			PeerNode pn = (PeerNode) ref.get();
+			if(pn == null) continue;
+			if(pn.getBootID() != requestedBootIDs[i]) continue;
+			pn.offer(key);
+		}
+	}
+
+	/**
+	 * Has any node asked for this key?
+	 */
+	public synchronized boolean othersWant(PeerNode peer) {
+		boolean anyValid = false;
+		for(int i=0;i<requestorNodes.length;i++) {
+			WeakReference ref = requestorNodes[i];
+			if(ref == null) continue;
+			PeerNode pn = (PeerNode) ref.get();
+			if(pn == null) {
+				requestorNodes[i] = null;
+				continue;
+			}
+			long bootID = pn.getBootID();
+			if(bootID != requestorBootIDs[i]) {
+				requestorNodes[i] = null;
+				continue;
+			}
+			anyValid = true;
+			if(pn == peer) return true;
+		}
+		if(!anyValid) {
+			requestorNodes = new WeakReference[0];
+			requestorTimes = requestorBootIDs = new long[0];
+		}
+		return false;
+	}
+
+	/**
+	 * Has this peer asked us for the key?
+	 */
+	public synchronized boolean askedByPeer(PeerNode peer, long now) {
+		boolean anyValid = false;
+		for(int i=0;i<requestorNodes.length;i++) {
+			WeakReference ref = requestorNodes[i];
+			if(ref == null) continue;
+			PeerNode pn = (PeerNode) ref.get();
+			if(pn == null) {
+				requestorNodes[i] = null;
+				continue;
+			}
+			long bootID = pn.getBootID();
+			if(bootID != requestorBootIDs[i]) {
+				requestorNodes[i] = null;
+				continue;
+			}
+			if(now - requestorTimes[i] > MAX_TIME_BETWEEN_REQUEST_AND_OFFER) return true;
+			anyValid = true;
+			if(pn == peer) return true;
+		}
+		if(!anyValid) {
+			requestorNodes = new WeakReference[0];
+			requestorTimes = requestorBootIDs = new long[0];
+		}
+		return false;
+	}
+
+	/**
+	 * Have we asked this peer for the key?
+	 */
+	public synchronized boolean askedFromPeer(PeerNode peer, long now) {
+		boolean anyValid = false;
+		for(int i=0;i<requestedNodes.length;i++) {
+			WeakReference ref = requestedNodes[i];
+			if(ref == null) continue;
+			PeerNode pn = (PeerNode) ref.get();
+			if(pn == null) {
+				requestedNodes[i] = null;
+				continue;
+			}
+			long bootID = pn.getBootID();
+			if(bootID != requestedBootIDs[i]) {
+				requestedNodes[i] = null;
+				continue;
+			}
+			if(now - requestedTimes[i] > MAX_TIME_BETWEEN_REQUEST_AND_OFFER) return true;
+			anyValid = true;
+			if(pn == peer) return true;
+		}
+		if(!anyValid) {
+			requestedNodes = new WeakReference[0];
+			requestedTimes = requestedBootIDs = new long[0];
+		}
+		return false;
+	}
+
+	public synchronized boolean isEmpty(long now) {
+		if(timeoutTime > now) return false;
+		if(requestedNodes.length > 0) return false;
+		if(requestorNodes.length > 0) return false;
+		return true;
+	}
+
+}
\ No newline at end of file

Modified: branches/freenet-jfk/src/freenet/node/GlobalProbe.java
===================================================================
--- branches/freenet-jfk/src/freenet/node/GlobalProbe.java	2007-08-20 13:57:42 UTC (rev 14802)
+++ branches/freenet-jfk/src/freenet/node/GlobalProbe.java	2007-08-20 14:10:51 UTC (rev 14803)
@@ -19,8 +19,8 @@
 	GlobalProbe(Node n) {
 		this.node = n;
     	cb = new ProbeCallback() {
-			public void onCompleted(String reason, double target, double best, double nearest, long id, short counter) {
-				String msg = "Completed probe request: "+target+" -> "+best+"\r\nNearest actually hit "+nearest+", "+counter+" hops in "+(System.currentTimeMillis() - lastTime)+", id "+id+"\r\n";
+			public void onCompleted(String reason, double target, double best, double nearest, long id, short counter, short linearCount) {
+				String msg = "Completed probe request: "+target+" -> "+best+"\r\nNearest actually hit "+nearest+", "+counter+" nodes ("+linearCount+" hops) in "+(System.currentTimeMillis() - lastTime)+", id "+id+"\r\n";
 				Logger.error(this, msg);
 				synchronized(GlobalProbe.this) {
 					doneSomething = true;
@@ -30,9 +30,9 @@
 				}
 			}
 
-			public void onTrace(long uid, double target, double nearest, double best, short htl, short counter, double location, long nodeUID, double[] peerLocs, long[] peerUIDs) {
-				String msg = "Probe trace: UID="+uid+" target="+target+" nearest="+nearest+" best="+best+" htl="+htl+" counter="+counter+" location="+location+" node UID="+nodeUID+" peer locs="+StringArray.toString(peerLocs)+" peer UIDs="+StringArray.toString(peerUIDs);
-				Logger.error(this, msg);
+			public void onTrace(long uid, double target, double nearest, double best, short htl, short counter, double location, long nodeUID, double[] peerLocs, long[] peerUIDs, double[] locsNotVisited, short forkCount, short linearCount, String reason, long prevUID) {
+				String msg = "Probe trace: UID="+uid+" target="+target+" nearest="+nearest+" best="+best+" htl="+htl+" counter="+counter+" location="+location+" node UID="+nodeUID+" prev UID="+prevUID+" peers="+NodeDispatcher.peersUIDsToString(peerUIDs, peerLocs)+" locs not visited: "+StringArray.toString(locsNotVisited)+" fork count: "+forkCount+" linear count: "+linearCount+" from "+reason;
+				Logger.normal(this, msg);
 			}
     	};
 		

Modified: branches/freenet-jfk/src/freenet/node/IPDetectorPluginManager.java
===================================================================
--- branches/freenet-jfk/src/freenet/node/IPDetectorPluginManager.java	2007-08-20 13:57:42 UTC (rev 14802)
+++ branches/freenet-jfk/src/freenet/node/IPDetectorPluginManager.java	2007-08-20 14:10:51 UTC (rev 14803)
@@ -4,6 +4,7 @@
 import java.util.HashSet;
 import java.util.Vector;
 
+import freenet.io.comm.FreenetInetAddress;
 import freenet.io.comm.Peer;
 import freenet.l10n.L10n;
 import freenet.node.useralerts.ProxyUserAlert;
@@ -45,7 +46,7 @@
 			div.addChild("#", text);
 			if(suggestPortForward) {
 				L10n.addL10nSubstitution(div, "IPDetectorPluginManager.suggestForwardPortWithLink", new String[] { "link", "/link", "port" },
-						new String[] { "<a href=\"/?_CHECKED_HTTP_=http://wiki.freenetproject.org/FirewallAndRouterIssues\">", "</a>", Integer.toString(node.portNumber) });
+						new String[] { "<a href=\"/?_CHECKED_HTTP_=http://wiki.freenetproject.org/FirewallAndRouterIssues\">", "</a>", Integer.toString(node.getDarknetPortNumber()) });
 			}
 			return div;
 		}
@@ -58,7 +59,7 @@
 			if(!suggestPortForward) return text;
 			StringBuffer sb = new StringBuffer();
 			sb.append(text);
-			sb.append(l10n("suggestForwardPort", "port", Integer.toString(node.portNumber)));
+			sb.append(l10n("suggestForwardPort", "port", Integer.toString(node.getDarknetPortNumber())));
 			return sb.toString();
 		}
 
@@ -90,7 +91,6 @@
 
 	static boolean logMINOR;
 	private final NodeIPDetector detector;
-	private final Ticker ticker;
 	private final Node node;
 	FredPluginIPDetector[] plugins;
 	private final MyUserAlert noConnectionAlert;
@@ -105,7 +105,6 @@
 		logMINOR = Logger.shouldLog(Logger.MINOR, getClass());
 		plugins = new FredPluginIPDetector[0];
 		this.node = node;
-		this.ticker = node.ps;
 		this.detector = detector;
 		noConnectionAlert = new MyUserAlert( l10n("noConnectivityTitle"), l10n("noConnectivity"), 
 				true, UserAlert.ERROR);
@@ -151,7 +150,7 @@
 		} catch (Throwable t) {
 			Logger.error(this, "Caught "+t, t);
 		}
-		ticker.queueTimedJob(new Runnable() {
+		node.getTicker().queueTimedJob(new Runnable() {
 			public void run() {
 				tryMaybeRun();
 			}
@@ -239,7 +238,7 @@
 		if(logMINOR) Logger.minor(this, "Maybe running IP detection plugins", new Exception("debug"));
 		PeerNode[] peers = node.getPeerNodes();
 		PeerNode[] conns = node.getConnectedPeers();
-		Peer[] nodeAddrs = detector.getPrimaryIPAddress();
+		FreenetInetAddress[] nodeAddrs = detector.getPrimaryIPAddress();
 		long now = System.currentTimeMillis();
 		synchronized(this) {
 			if(plugins.length == 0) {
@@ -407,7 +406,7 @@
 	 * @param nodeAddrs Our peers' addresses.
 	 * @return True if we should run a detection.
 	 */
-	private boolean shouldDetectDespiteRealIP(long now, PeerNode[] peers, Peer[] nodeAddrs) {
+	private boolean shouldDetectDespiteRealIP(long now, PeerNode[] peers, FreenetInetAddress[] nodeAddrs) {
 		// We might still be firewalled?
 		// First, check only once per day or startup
 		if(now - lastDetectAttemptEndedTime < 12*60*60*1000) {
@@ -432,7 +431,7 @@
 						// Is it internal?
 						boolean internal = false;
 						for(int j=0;j<nodeAddrs.length;j++) {
-							if(addr.equals(nodeAddrs[j].getAddress())) {
+							if(addr.equals(nodeAddrs[j])) {
 								// Internal
 								internal = true;
 								break;
@@ -469,9 +468,7 @@
 		if(logMINOR) Logger.minor(this, "Detecting...");
 		synchronized(this) {
 			runner = new DetectorRunner();
-			Thread t = new Thread(runner);
-			t.setDaemon(true);
-			t.start();
+			node.executor.execute(runner, "Plugin detector runner");
 		}
 	}
 

Modified: branches/freenet-jfk/src/freenet/node/InsertHandler.java
===================================================================
--- branches/freenet-jfk/src/freenet/node/InsertHandler.java	2007-08-20 13:57:42 UTC (rev 14802)
+++ branches/freenet-jfk/src/freenet/node/InsertHandler.java	2007-08-20 14:10:51 UTC (rev 14803)
@@ -57,8 +57,8 @@
         htl = req.getShort(DMT.HTL);
         closestLoc = req.getDouble(DMT.NEAREST_LOCATION);
         double targetLoc = key.toNormalizedDouble();
-        double myLoc = node.lm.getLocation().getValue();
-        if(PeerManager.distance(targetLoc, myLoc) < PeerManager.distance(targetLoc, closestLoc)) {
+        double myLoc = node.lm.getLocation();
+        if(Location.distance(targetLoc, myLoc) < Location.distance(targetLoc, closestLoc)) {
             closestLoc = myLoc;
             htl = node.maxHTL();
         }
@@ -79,7 +79,7 @@
             Logger.error(this, "Caught in run() "+t, t);
         } finally {
         	if(logMINOR) Logger.minor(this, "Exiting InsertHandler.run() for "+uid);
-            node.unlockUID(uid, false, true);
+            node.unlockUID(uid, false, true, false);
         }
     }
 
@@ -147,9 +147,7 @@
         // Receive the data, off thread
         
         Runnable dataReceiver = new DataReceiver();
-        Thread t = new Thread(dataReceiver, "InsertHandler$DataReceiver for UID "+uid);
-        t.setDaemon(true);
-        t.start();
+        node.executor.execute(dataReceiver, "InsertHandler$DataReceiver for UID "+uid);
 
         if(htl == 0) {
             canCommit = true;

Modified: branches/freenet-jfk/src/freenet/node/KeyTracker.java
===================================================================
--- branches/freenet-jfk/src/freenet/node/KeyTracker.java	2007-08-20 13:57:42 UTC (rev 14802)
+++ branches/freenet-jfk/src/freenet/node/KeyTracker.java	2007-08-20 14:10:51 UTC (rev 14803)
@@ -407,8 +407,8 @@
             super(packetNumber);
             this.createdTime = System.currentTimeMillis();
             if(sendSoon) {
-                activeTime -= 500;
-                urgentTime -= 500;
+                activeTime -= activeDelay;
+                urgentTime -= activeDelay;
             }
         }
         

Modified: branches/freenet-jfk/src/freenet/node/Location.java
===================================================================
--- branches/freenet-jfk/src/freenet/node/Location.java	2007-08-20 13:57:42 UTC (rev 14802)
+++ branches/freenet-jfk/src/freenet/node/Location.java	2007-08-20 14:10:51 UTC (rev 14803)
@@ -3,7 +3,7 @@
  * http://www.gnu.org/ for further details of the GPL. */
 package freenet.node;
 
-import freenet.crypt.RandomSource;
+import freenet.support.Logger;
 
 /**
  * @author amphibian
@@ -12,55 +12,43 @@
  * Simply a number from 0.0 to 1.0.
  */
 public class Location {
-    private double loc;
-    private int hashCode;
-    
-    private Location(double location) {
-        setValue(location);
-    }
 
-    public Location(String init) throws FSParseException {
-        try {
-            setValue(Double.parseDouble(init));
-        } catch (NumberFormatException e) {
-            throw new FSParseException(e);
-        }
-    }
+	public static double getLocation(String init) throws FSParseException {
+		try {
+			if(init == null) throw new FSParseException("Null location");
+			double d = Double.parseDouble(init);
+			if(d < 0.0 || d > 1.0) throw new FSParseException("Invalid location "+d);
+			return d;
+		} catch (NumberFormatException e) {
+			throw new FSParseException(e);
+		}
+	}
+	
+	static double distance(PeerNode p, double loc) {
+		double d = distance(p.getLocation(), loc);
+		return d;
+		//return d * p.getBias();
+	}
 
-    public double getValue() {
-        return loc;
-    }
+	/**
+	 * Distance between two locations.
+	 * Both parameters must be in [0.0, 1.0].
+	 */
+	public static double distance(double a, double b) {
+		return distance(a, b, false);
+	}
 
-    /**
-     * @return A random Location to initialize the node to.
-     */
-    public static Location randomInitialLocation(RandomSource r) {
-        return new Location(r.nextDouble());
-    }
+	public static double distance(double a, double b, boolean allowCrazy) {
+	    if(((a < 0.0 || a > 1.0)||(b < 0.0 || b > 1.0)) && !allowCrazy) {
+	    	Logger.error(PeerManager.class, "Invalid Location ! a = "+a +" b = "+ b + "Please report this bug!", new Exception("error"));
+	    	throw new NullPointerException();
+	    }
+	    // Circular keyspace
+		if (a > b) return Math.min (a - b, 1.0 - a + b);
+		else return Math.min (b - a, 1.0 - b + a);
+	}
 
-    public void setValue(double newLoc) {
-        if((loc < 0.0) || (loc > 1.0))
-            throw new IllegalArgumentException();
-        this.loc = newLoc;
-        long l = Double.doubleToLongBits(newLoc);
-        hashCode = ((int)(l >>> 32)) ^ ((int)l);
-    }
-    
-    public boolean equals(Object o) {
-        if(o instanceof Location) {
-            return Math.abs(((Location)o).loc - loc) <= Double.MIN_VALUE;
-        }
-        return false;
-    }
-    
-    public int hashCode() {
-        return hashCode;
-    }
-
-    /**
-     * Randomize the location.
-     */
-    public synchronized void randomize(RandomSource r) {
-        setValue(r.nextDouble());
-    }
+	public static boolean equals(double newLoc, double currentLocation) {
+		return Math.abs(newLoc - currentLocation) < Double.MIN_VALUE * 2;
+	}
 }

Modified: branches/freenet-jfk/src/freenet/node/LocationManager.java
===================================================================
--- branches/freenet-jfk/src/freenet/node/LocationManager.java	2007-08-20 13:57:42 UTC (rev 14802)
+++ branches/freenet-jfk/src/freenet/node/LocationManager.java	2007-08-20 14:10:51 UTC (rev 14803)
@@ -57,34 +57,38 @@
     long timeLastSuccessfullySwapped;
     
     public LocationManager(RandomSource r) {
-        loc = Location.randomInitialLocation(r);
+        loc = r.nextDouble();
         sender = new SwapRequestSender();
         this.r = r;
         recentlyForwardedIDs = new Hashtable();
         logMINOR = Logger.shouldLog(Logger.MINOR, this);
     }
 
-    Location loc;
-    double locChangeSession = 0.0;
+    private double loc;
+    private double locChangeSession = 0.0;
     
     int numberOfRemotePeerLocationsSeenInSwaps = 0;
 
     /**
      * @return The current Location of this node.
      */
-    public Location getLocation() {
+    public synchronized double getLocation() {
         return loc;
     }
 
     /**
      * @param l
      */
-    public void setLocation(Location l) {
+    public synchronized void setLocation(double l) {
+    	if(l < 0.0 || l > 1.0) {
+    		Logger.error(this, "Setting invalid location: "+l, new Exception("error"));
+    		return;
+    	}
         this.loc = l;
     }
     
-    public void updateLocationChangeSession(double newLoc) {
-    	double oldLoc = this.loc.getValue();
+    public synchronized void updateLocationChangeSession(double newLoc) {
+    	double oldLoc = loc;
     	// Patterned after PeerManager.distance( double, double ), but also need to know the direction of the change
 		if (newLoc > oldLoc) {
 			double directDifference = newLoc - oldLoc;
@@ -116,9 +120,7 @@
     public void startSender(Node n, SwapRequestInterval interval) {
         this.node = n;
         this.interval = interval;
-        Thread t = new Thread(sender, "SwapRequest sender");
-        t.setDaemon(true);
-        t.start();
+        n.executor.execute(sender, "SwapRequest sender");
     }
 
     /**
@@ -154,12 +156,12 @@
                         if(System.currentTimeMillis() - timeLastSuccessfullySwapped > 30*1000) {
                             try {
                                 boolean myFlag = false;
-                                double myLoc = loc.getValue();
+                                double myLoc = getLocation();
                                 PeerNode[] peers = node.peers.connectedPeers;
                                 for(int i=0;i<peers.length;i++) {
                                     PeerNode pn = peers[i];
                                     if(pn.isRoutable()) {
-                                        double ploc = pn.getLocation().getValue();
+                                        double ploc = pn.getLocation();
                                         if(Math.abs(ploc - myLoc) <= Double.MIN_VALUE) {
                                             myFlag = true;
                                             // Log an ERROR
@@ -171,7 +173,7 @@
                                     }
                                 }
                                 if(myFlag) {
-                                    loc.randomize(node.random);
+                                    setLocation(node.random.nextDouble());
                                     announceLocChange();
                                     node.writeNodeFile();
                                 }
@@ -196,10 +198,8 @@
      * the wilderness.
      */
     private void startSwapRequest() {
-        Thread t = new Thread(new OutgoingSwapRequestHandler(),
-                "Outgoing swap request handler for port "+node.portNumber);
-        t.setDaemon(true);
-        t.start();
+    	node.executor.execute(new OutgoingSwapRequestHandler(),
+                "Outgoing swap request handler for port "+node.getDarknetPortNumber());
     }
     
     /**
@@ -246,7 +246,7 @@
             // Create my side
             
             long random = r.nextLong();
-            double myLoc = loc.getValue();
+            double myLoc = getLocation();
             LocationUIDPair[] friendLocsAndUIDs = node.peers.getPeerLocationsAndUIDs();
             double[] friendLocs = extractLocs(friendLocsAndUIDs);
             long[] myValueLong = new long[1+1+friendLocs.length];
@@ -326,7 +326,7 @@
             // Send our SwapComplete
             
             Message confirm = DMT.createFNPSwapComplete(uid, myValue);
-            confirm.addSubMessage(DMT.createFNPSwapLocations(Fields.longsToBytes(extractUIDs(friendLocsAndUIDs))));
+            confirm.addSubMessage(DMT.createFNPSwapLocations(extractUIDs(friendLocsAndUIDs)));
             
             node.usm.send(pn, confirm, null);
             
@@ -338,7 +338,7 @@
                 timeLastSuccessfullySwapped = System.currentTimeMillis();
                 // Swap
                 updateLocationChangeSession(hisLoc);
-                loc.setValue(hisLoc);
+                setLocation(hisLoc);
                 if(logMINOR) Logger.minor(this, "Swapped: "+myLoc+" <-> "+hisLoc+" - "+uid);
                 swaps++;
                 announceLocChange();
@@ -376,7 +376,7 @@
                 // We can't lock friends_locations, so lets just
                 // pretend that they're locked
                 long random = r.nextLong();
-                double myLoc = loc.getValue();
+                double myLoc = getLocation();
                 LocationUIDPair[] friendLocsAndUIDs = node.peers.getPeerLocationsAndUIDs();
                 double[] friendLocs = extractLocs(friendLocsAndUIDs);
                 long[] myValueLong = new long[1+1+friendLocs.length];
@@ -438,7 +438,7 @@
                 byte[] hisHash = ((ShortBuffer)reply.getObject(DMT.HASH)).getData();
                 
                 Message confirm = DMT.createFNPSwapCommit(uid, myValue);
-                confirm.addSubMessage(DMT.createFNPSwapLocations(Fields.longsToBytes(extractUIDs(friendLocsAndUIDs))));
+                confirm.addSubMessage(DMT.createFNPSwapLocations(extractUIDs(friendLocsAndUIDs)));
 
                 filter1.clearOr();
                 MessageFilter filter3 = MessageFilter.create().setField(DMT.UID, uid).setType(DMT.FNPSwapComplete).setTimeout(TIMEOUT).setSource(pn);
@@ -518,7 +518,7 @@
                     timeLastSuccessfullySwapped = System.currentTimeMillis();
                     // Swap
                     updateLocationChangeSession(hisLoc);
-                    loc.setValue(hisLoc);
+                    setLocation(hisLoc);
                     if(logMINOR) Logger.minor(this, "Swapped: "+myLoc+" <-> "+hisLoc+" - "+uid);
                     swaps++;
                     announceLocChange();
@@ -542,8 +542,8 @@
      * Tell all connected peers that our location has changed
      */
     private void announceLocChange() {
-        Message msg = DMT.createFNPLocChangeNotification(loc.getValue());
-        node.peers.localBroadcast(msg);
+        Message msg = DMT.createFNPLocChangeNotification(getLocation());
+        node.peers.localBroadcast(msg, false);
     }
     
     private boolean locked;
@@ -569,7 +569,7 @@
         	if(logMINOR) Logger.minor(this, "Already locked");
         	return false;
         }
-        if(logMINOR) Logger.minor(this, "Locking on port "+node.portNumber);
+        if(logMINOR) Logger.minor(this, "Locking on port "+node.getDarknetPortNumber());
         locked = true;
         lockedTime = System.currentTimeMillis();
         return true;
@@ -581,7 +581,7 @@
         locked = false;
         long lockTime = System.currentTimeMillis() - lockedTime;
         if(logMINOR) {
-        	Logger.minor(this, "Unlocking on port "+node.portNumber);
+        	Logger.minor(this, "Unlocking on port "+node.getDarknetPortNumber());
         	Logger.minor(this, "lockTime: "+lockTime);
         }
     }
@@ -640,22 +640,22 @@
         double A = 1.0;
         for(int i=0;i<friendLocs.length;i++) {
             if(Math.abs(friendLocs[i] - myLoc) <= Double.MIN_VALUE) continue;
-            A *= PeerManager.distance(friendLocs[i], myLoc);
+            A *= Location.distance(friendLocs[i], myLoc);
         }
         for(int i=0;i<hisFriendLocs.length;i++) {
             if(Math.abs(hisFriendLocs[i] - hisLoc) <= Double.MIN_VALUE) continue;
-            A *= PeerManager.distance(hisFriendLocs[i], hisLoc);
+            A *= Location.distance(hisFriendLocs[i], hisLoc);
         }
         
         // B = the same, with our two values swapped
         double B = 1.0;
         for(int i=0;i<friendLocs.length;i++) {
             if(Math.abs(friendLocs[i] - hisLoc) <= Double.MIN_VALUE) continue;
-            B *= PeerManager.distance(friendLocs[i], hisLoc);
+            B *= Location.distance(friendLocs[i], hisLoc);
         }
         for(int i=0;i<hisFriendLocs.length;i++) {
             if(Math.abs(hisFriendLocs[i] - myLoc) <= Double.MIN_VALUE) continue;
-            B *= PeerManager.distance(hisFriendLocs[i], myLoc);
+            B *= Location.distance(hisFriendLocs[i], myLoc);
         }
         
         //Logger.normal(this, "A="+A+" B="+B);
@@ -762,9 +762,7 @@
                 IncomingSwapRequestHandler isrh =
                     new IncomingSwapRequestHandler(m, pn, item);
                 if(logMINOR) Logger.minor(this, "Handling... "+uid);
-                Thread t = new Thread(isrh, "Incoming swap request handler for port "+node.portNumber);
-                t.setDaemon(true);
-                t.start();
+                node.executor.execute(isrh, "Incoming swap request handler for port "+node.getDarknetPortNumber());
                 return true;
             } catch (Error e) {
                 unlock();
@@ -961,7 +959,8 @@
     
     /** Spy on locations in somebody else's swap request. Greatly increases the
      * speed at which we can gather location data to estimate the network's size.
-     * @param swappingWithMe 
+     * @param swappingWithMe True if this node is participating in the swap, false if it is
+     * merely spying on somebody else's swap.
      */
     private void spyOnLocations(Message m, boolean ignoreIfOld, boolean swappingWithMe, double myLoc) {
     	
@@ -1060,7 +1059,7 @@
         recentlyForwardedIDs.remove(new Long(item.outgoingID));
     }
     
-    private final long MAX_AGE = 7*24*60*60*1000;
+    private static final long MAX_AGE = 7*24*60*60*1000;
     
     private final TimeSortedHashtable knownLocs = new TimeSortedHashtable();
     
@@ -1125,7 +1124,7 @@
 	public static double[] extractLocs(PeerNode[] peers, boolean indicateBackoff) {
 		double[] locs = new double[peers.length];
 		for(int i=0;i<peers.length;i++) {
-			locs[i] = peers[i].getLocation().getValue();
+			locs[i] = peers[i].getLocation();
 			if(indicateBackoff) {
 				if(peers[i].isRoutingBackedOff())
 					locs[i] += 1;
@@ -1142,4 +1141,8 @@
 			uids[i] = peers[i].swapIdentifier;
 		return uids;
 	}
+
+	public synchronized double getLocChangeSession() {
+		return locChangeSession;
+	}
 }

Modified: branches/freenet-jfk/src/freenet/node/LoggingConfigHandler.java
===================================================================
--- branches/freenet-jfk/src/freenet/node/LoggingConfigHandler.java	2007-08-20 13:57:42 UTC (rev 14802)
+++ branches/freenet-jfk/src/freenet/node/LoggingConfigHandler.java	2007-08-20 14:10:51 UTC (rev 14803)
@@ -10,6 +10,7 @@
 import freenet.config.InvalidConfigValueException;
 import freenet.config.OptionFormatException;
 import freenet.config.SubConfig;
+import freenet.support.Executor;
 import freenet.support.FileLoggerHook;
 import freenet.support.Logger;
 import freenet.support.LoggerHook;
@@ -55,9 +56,11 @@
 	private String logRotateInterval;
 	private long maxCachedLogBytes;
 	private int maxCachedLogLines;
+	private final Executor executor;
 	
-	public LoggingConfigHandler(SubConfig loggingConfig) throws InvalidConfigValueException {
+	public LoggingConfigHandler(SubConfig loggingConfig, Executor executor) throws InvalidConfigValueException {
 		this.config = loggingConfig;
+		this.executor = executor;
     	
     	loggingConfig.register("enabled", true, 1, true, false, "LogConfigHandler.enabled", "LogConfigHandler.enabledLong",
     			new BooleanCallback() {
@@ -295,9 +298,7 @@
 		}
 
 		void start() {
-			Thread t = new Thread(this, "Old log directory "+logDir+" deleter");
-			t.setDaemon(true);
-			t.start();
+			executor.execute(this, "Old log directory "+logDir+" deleter");
 		}
 		
 		public void run() {

Modified: branches/freenet-jfk/src/freenet/node/LowLevelGetException.java
===================================================================
--- branches/freenet-jfk/src/freenet/node/LowLevelGetException.java	2007-08-20 13:57:42 UTC (rev 14802)
+++ branches/freenet-jfk/src/freenet/node/LowLevelGetException.java	2007-08-20 14:10:51 UTC (rev 14803)
@@ -29,6 +29,8 @@
 	public static final int VERIFY_FAILED = 8;
 	/** Request cancelled by user */
 	public static final int CANCELLED = 9;
+	/** Ran into a failure table */
+	public static final int RECENTLY_FAILED = 10;
 	
 	static final String getMessage(int reason) {
 		switch(reason) {
@@ -50,6 +52,8 @@
 			return "Node sent us invalid data";
 		case CANCELLED:
 			return "Request cancelled";
+		case RECENTLY_FAILED:
+			return "Request killed by failure table due to recently DNFing on a downstream node";
 		default:
 			return "Unknown error code: "+reason;
 		}

Modified: branches/freenet-jfk/src/freenet/node/Node.java
===================================================================
--- branches/freenet-jfk/src/freenet/node/Node.java	2007-08-20 13:57:42 UTC (rev 14802)
+++ branches/freenet-jfk/src/freenet/node/Node.java	2007-08-20 14:10:51 UTC (rev 14803)
@@ -8,7 +8,6 @@
 
 import java.io.BufferedReader;
 import java.io.BufferedWriter;
-import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileNotFoundException;
@@ -16,10 +15,7 @@
 import java.io.IOException;
 import java.io.InputStreamReader;
 import java.io.OutputStreamWriter;
-import java.io.UnsupportedEncodingException;
-import java.math.BigInteger;
 import java.net.InetAddress;
-import java.net.MalformedURLException;
 import java.net.UnknownHostException;
 import java.security.MessageDigest;
 import java.util.Arrays;
@@ -28,10 +24,9 @@
 import java.util.Iterator;
 import java.util.Locale;
 import java.util.MissingResourceException;
-import java.util.zip.DeflaterOutputStream;
+import java.util.Random;
 
-import net.i2p.util.NativeBigInteger;
-
+import org.spaceroots.mantissa.random.MersenneTwister;
 import org.tanukisoftware.wrapper.WrapperManager;
 
 import com.sleepycat.je.DatabaseException;
@@ -48,23 +43,19 @@
 import freenet.config.LongOption;
 import freenet.config.PersistentConfig;
 import freenet.config.SubConfig;
-import freenet.crypt.DSA;
-import freenet.crypt.DSAGroup;
-import freenet.crypt.DSAPrivateKey;
 import freenet.crypt.DSAPublicKey;
-import freenet.crypt.DSASignature;
-import freenet.crypt.Global;
 import freenet.crypt.RandomSource;
 import freenet.crypt.SHA256;
+import freenet.crypt.Yarrow;
 import freenet.io.comm.DMT;
 import freenet.io.comm.DisconnectedException;
 import freenet.io.comm.FreenetInetAddress;
 import freenet.io.comm.Message;
+import freenet.io.comm.MessageCore;
 import freenet.io.comm.MessageFilter;
 import freenet.io.comm.Peer;
 import freenet.io.comm.PeerParseException;
 import freenet.io.comm.ReferenceSignatureVerificationException;
-import freenet.io.comm.UdpSocketManager;
 import freenet.io.xfer.PartiallyReceivedBlock;
 import freenet.keys.CHKBlock;
 import freenet.keys.CHKVerifyException;
@@ -74,8 +65,6 @@
 import freenet.keys.ClientKeyBlock;
 import freenet.keys.ClientSSK;
 import freenet.keys.ClientSSKBlock;
-import freenet.keys.FreenetURI;
-import freenet.keys.InsertableClientSSK;
 import freenet.keys.Key;
 import freenet.keys.KeyBlock;
 import freenet.keys.KeyVerifyException;
@@ -88,24 +77,25 @@
 import freenet.node.useralerts.BuildOldAgeUserAlert;
 import freenet.node.useralerts.ExtOldAgeUserAlert;
 import freenet.node.useralerts.MeaningfulNodeNameUserAlert;
-import freenet.node.useralerts.N2NTMUserAlert;
+import freenet.node.useralerts.OpennetUserAlert;
+import freenet.node.useralerts.TimeSkewDetectedUserAlert;
 import freenet.node.useralerts.UserAlert;
 import freenet.pluginmanager.PluginManager;
 import freenet.store.BerkeleyDBFreenetStore;
 import freenet.store.FreenetStore;
 import freenet.store.KeyCollisionException;
-import freenet.support.Base64;
 import freenet.support.DoubleTokenBucket;
+import freenet.support.Executor;
 import freenet.support.Fields;
 import freenet.support.FileLoggerHook;
 import freenet.support.HTMLEncoder;
 import freenet.support.HTMLNode;
 import freenet.support.HexUtil;
-import freenet.support.IllegalBase64Exception;
 import freenet.support.ImmutableByteArrayWrapper;
 import freenet.support.LRUHashtable;
 import freenet.support.LRUQueue;
 import freenet.support.Logger;
+import freenet.support.OOMHandler;
 import freenet.support.ShortBuffer;
 import freenet.support.SimpleFieldSet;
 import freenet.support.api.BooleanCallback;
@@ -117,34 +107,13 @@
 /**
  * @author amphibian
  */
-public class Node {
+public class Node implements TimeSkewDetectorCallback {
 
 	private static boolean logMINOR;
 	
-	static class NodeBindtoCallback implements StringCallback {
-		
-		final Node node;
-		
-		NodeBindtoCallback(Node n) {
-			this.node = n;
-		}
-		
-		public String get() {
-			if(node.getBindTo()!=null)
-				return node.getBindTo();
-			else
-				return "0.0.0.0";
-		}
-		
-		public void set(String val) throws InvalidConfigValueException {
-			if(val.equals(get())) return;
-			// FIXME why not? Can't we use freenet.io.NetworkInterface like everywhere else, just adapt it for UDP?
-			throw new InvalidConfigValueException("Cannot be updated on the fly");
-		}
-	}
-	
 	private static MeaningfulNodeNameUserAlert nodeNameUserAlert;
 	private static BuildOldAgeUserAlert buildOldAgeUserAlert;
+	private static TimeSkewDetectedUserAlert timeSkewDetectedUserAlert;
 	
 	public class NodeNameCallback implements StringCallback{
 			Node node;
@@ -162,12 +131,12 @@
 			}
 
 			public void set(String val) throws InvalidConfigValueException {
+				if(get().equals(val)) return;
+				else if(val.length() > 128)
+					throw new InvalidConfigValueException("The given node name is too long ("+val+')');
+				else if("".equals(val))
+					val = "~none~";
 				myName = val;
-				if(myName.startsWith("Node id|")|| myName.equals("MyFirstFreenetNode")){
-					clientCore.alerts.register(nodeNameUserAlert);
-				}else{
-					clientCore.alerts.unregister(nodeNameUserAlert);
-				}
 			}
 	}
 	
@@ -256,9 +225,6 @@
 	static final long TESTNET_MIN_MAX_ZIPPED_LOGFILES = 512*1024*1024;
 	static final String TESTNET_MIN_MAX_ZIPPED_LOGFILES_STRING = "512M";
 	
-	// FIXME: abstract out address stuff? Possibly to something like NodeReference?
-	final int portNumber;
-
 	/** Datastore directory */
 	private final File storeDir;
 
@@ -311,22 +277,6 @@
 	private final HashSet transferringRequestHandlers;
 	/** CHKInsertSender's currently running, by KeyHTLPair */
 	private final HashMap insertSenders;
-	/** My crypto group */
-	private DSAGroup myCryptoGroup;
-	/** My private key */
-	private DSAPrivateKey myPrivKey;
-	/** My public key */
-	private DSAPublicKey myPubKey;
-	
-	/** My ARK SSK private key */
-	InsertableClientSSK myARK;
-	/** My ARK sequence number */
-	long myARKNumber;
-	// FIXME remove old ARK support
-	/** My old ARK SSK private key */
-	InsertableClientSSK myOldARK;
-	/** My old ARK sequence number */
-	long myOldARKNumber;
 	/** FetchContext for ARKs */
 	public final FetchContext arkFetcherContext;
 	
@@ -345,20 +295,9 @@
 	private final HashSet runningCHKPutUIDs;
 	private final HashSet runningSSKPutUIDs;
 	
-	byte[] myIdentity; // FIXME: simple identity block; should be unique
-	/** Hash of identity. Used as setup key. */
-	byte[] identityHash;
-	/** Hash of hash of identity i.e. hash of setup key. */
-	byte[] identityHashHash;
 	/** Semi-unique ID for swap requests. Used to identify us so that the
 	 * topology can be reconstructed. */
 	public long swapIdentifier;
-	/** The signature of the above fieldset */
-	private DSASignature myReferenceSignature = null;
-	/** A synchronization object used while signing the reference fiedlset */
-	private volatile Object referenceSync = new Object();
-	/** An ordered version of the FieldSet, without the signature */
-	private String mySignedReference = null;
 	private String myName;
 	final LocationManager lm;
 	/** My peers */
@@ -369,10 +308,26 @@
 	final File extraPeerDataDir;
 	/** Strong RNG */
 	public final RandomSource random;
-	final UdpSocketManager usm;
-	final FNPPacketMangler packetMangler;
-	final DNSRequester dnsr;
+	/** Weak but fast RNG */
+	public fin