/**
 * An SPVBlockStore holds a limited number of block headers in a memory mapped ring buffer. With
 * such a store, you may not be able to process very deep re-orgs and could be disconnected from the
 * chain (requiring a replay), but as they are virtually unheard of this is not a significant risk.
 */
public class SPVBlockStore implements BlockStore {
  private static final Logger log = LoggerFactory.getLogger(SPVBlockStore.class);

  /** The default number of headers that will be stored in the ring buffer. */
  public static final int DEFAULT_NUM_HEADERS = 5000;

  public static final String HEADER_MAGIC = "SPVB";

  protected volatile MappedByteBuffer buffer;
  protected int numHeaders;
  protected NetworkParameters params;

  protected ReentrantLock lock = Threading.lock("SPVBlockStore");

  // The entire ring-buffer is mmapped and accessing it should be as fast as accessing regular
  // memory once it's
  // faulted in. Unfortunately, in theory practice and theory are the same. In practice they aren't.
  //
  // MMapping a file in Java does not give us a byte[] as you may expect but rather a ByteBuffer,
  // and whilst on
  // the OpenJDK/Oracle JVM calls into the get() methods are compiled down to inlined native code on
  // Android each
  // get() call is actually a full-blown JNI method under the hood, meaning it's unbelievably slow.
  // The caches
  // below let us stay in the JIT-compiled Java world without expensive JNI transitions and make a
  // 10x difference!
  protected LinkedHashMap<Sha256Hash, StoredBlock> blockCache =
      new LinkedHashMap<Sha256Hash, StoredBlock>() {
        @Override
        protected boolean removeEldestEntry(Map.Entry<Sha256Hash, StoredBlock> entry) {
          return size() > 2050; // Slightly more than the difficulty transition period.
        }
      };
  // Use a separate cache to track get() misses. This is to efficiently handle the case of an
  // unconnected block
  // during chain download. Each new block will do a get() on the unconnected block so if we haven't
  // seen it yet we
  // must efficiently respond.
  //
  // We don't care about the value in this cache. It is always notFoundMarker. Unfortunately
  // LinkedHashSet does not
  // provide the removeEldestEntry control.
  protected static final Object notFoundMarker = new Object();
  protected LinkedHashMap<Sha256Hash, Object> notFoundCache =
      new LinkedHashMap<Sha256Hash, Object>() {
        @Override
        protected boolean removeEldestEntry(Map.Entry<Sha256Hash, Object> entry) {
          return size() > 100; // This was chosen arbitrarily.
        }
      };
  // Used to stop other applications/processes from opening the store.
  protected FileLock fileLock = null;
  protected RandomAccessFile randomAccessFile = null;

  /**
   * Creates and initializes an SPV block store. Will create the given file if it's missing. This
   * operation will block on disk.
   */
  public SPVBlockStore(NetworkParameters params, File file) throws BlockStoreException {
    checkNotNull(file);
    this.params = checkNotNull(params);
    try {
      this.numHeaders = DEFAULT_NUM_HEADERS;
      boolean exists = file.exists();
      // Set up the backing file.
      randomAccessFile = new RandomAccessFile(file, "rw");
      long fileSize = getFileSize();
      if (!exists) {
        log.info("Creating new SPV block chain file " + file);
        randomAccessFile.setLength(fileSize);
      } else if (randomAccessFile.length() != fileSize) {
        throw new BlockStoreException(
            "File size on disk does not match expected size: "
                + randomAccessFile.length()
                + " vs "
                + fileSize);
      }

      FileChannel channel = randomAccessFile.getChannel();
      fileLock = channel.tryLock();
      if (fileLock == null)
        throw new BlockStoreException("Store file is already locked by another process");

      // Map it into memory read/write. The kernel will take care of flushing writes to disk at the
      // most
      // efficient times, which may mean that until the map is deallocated the data on disk is
      // randomly
      // inconsistent. However the only process accessing it is us, via this mapping, so our own
      // view will
      // always be correct. Once we establish the mmap the underlying file and channel can go away.
      // Note that
      // the details of mmapping vary between platforms.
      buffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, fileSize);

      // Check or initialize the header bytes to ensure we don't try to open some random file.
      byte[] header;
      if (exists) {
        header = new byte[4];
        buffer.get(header);
        if (!new String(header, "US-ASCII").equals(HEADER_MAGIC))
          throw new BlockStoreException("Header bytes do not equal " + HEADER_MAGIC);
      } else {
        initNewStore(params);
      }
    } catch (Exception e) {
      try {
        if (randomAccessFile != null) randomAccessFile.close();
      } catch (IOException e2) {
        throw new BlockStoreException(e2);
      }
      throw new BlockStoreException(e);
    }
  }

  private void initNewStore(NetworkParameters params) throws Exception {
    byte[] header;
    header = HEADER_MAGIC.getBytes("US-ASCII");
    buffer.put(header);
    // Insert the genesis block.
    lock.lock();
    try {
      setRingCursor(buffer, FILE_PROLOGUE_BYTES);
    } finally {
      lock.unlock();
    }
    Block genesis = params.getGenesisBlock().cloneAsHeader();
    StoredBlock storedGenesis = new StoredBlock(genesis, genesis.getWork(), 0);
    put(storedGenesis);
    setChainHead(storedGenesis);
  }

  /**
   * Returns the size in bytes of the file that is used to store the chain with the current
   * parameters.
   */
  public int getFileSize() {
    return RECORD_SIZE * numHeaders + FILE_PROLOGUE_BYTES /* extra kilobyte for stuff */;
  }

  @Override
  public void put(StoredBlock block) throws BlockStoreException {
    final MappedByteBuffer buffer = this.buffer;
    if (buffer == null) throw new BlockStoreException("Store closed");

    lock.lock();
    try {
      int cursor = getRingCursor(buffer);
      if (cursor == getFileSize()) {
        // Wrapped around.
        cursor = FILE_PROLOGUE_BYTES;
      }
      buffer.position(cursor);
      Sha256Hash hash = block.getHeader().getHash();
      notFoundCache.remove(hash);
      buffer.put(hash.getBytes());
      block.serializeCompact(buffer);
      setRingCursor(buffer, buffer.position());
      blockCache.put(hash, block);
    } finally {
      lock.unlock();
    }
  }

  @Override
  @Nullable
  public StoredBlock get(Sha256Hash hash) throws BlockStoreException {
    final MappedByteBuffer buffer = this.buffer;
    if (buffer == null) throw new BlockStoreException("Store closed");

    lock.lock();
    try {
      StoredBlock cacheHit = blockCache.get(hash);
      if (cacheHit != null) return cacheHit;
      if (notFoundCache.get(hash) != null) return null;

      // Starting from the current tip of the ring work backwards until we have either found the
      // block or
      // wrapped around.
      int cursor = getRingCursor(buffer);
      final int startingPoint = cursor;
      final int fileSize = getFileSize();
      final byte[] targetHashBytes = hash.getBytes();
      byte[] scratch = new byte[32];
      do {
        cursor -= RECORD_SIZE;
        if (cursor < FILE_PROLOGUE_BYTES) {
          // We hit the start, so wrap around.
          cursor = fileSize - RECORD_SIZE;
        }
        // Cursor is now at the start of the next record to check, so read the hash and compare it.
        buffer.position(cursor);
        buffer.get(scratch);
        if (Arrays.equals(scratch, targetHashBytes)) {
          // Found the target.
          StoredBlock storedBlock = StoredBlock.deserializeCompact(params, buffer);
          blockCache.put(hash, storedBlock);
          return storedBlock;
        }
      } while (cursor != startingPoint);
      // Not found.
      notFoundCache.put(hash, notFoundMarker);
      return null;
    } catch (ProtocolException e) {
      throw new RuntimeException(e); // Cannot happen.
    } finally {
      lock.unlock();
    }
  }

  protected StoredBlock lastChainHead = null;

  @Override
  public StoredBlock getChainHead() throws BlockStoreException {
    final MappedByteBuffer buffer = this.buffer;
    if (buffer == null) throw new BlockStoreException("Store closed");

    lock.lock();
    try {
      if (lastChainHead == null) {
        byte[] headHash = new byte[32];
        buffer.position(8);
        buffer.get(headHash);
        Sha256Hash hash = new Sha256Hash(headHash);
        StoredBlock block = get(hash);
        if (block == null)
          throw new BlockStoreException(
              "Corrupted block store: could not find chain head: " + hash);
        lastChainHead = block;
      }
      return lastChainHead;
    } finally {
      lock.unlock();
    }
  }

  @Override
  public void setChainHead(StoredBlock chainHead) throws BlockStoreException {
    final MappedByteBuffer buffer = this.buffer;
    if (buffer == null) throw new BlockStoreException("Store closed");

    lock.lock();
    try {
      lastChainHead = chainHead;
      byte[] headHash = chainHead.getHeader().getHash().getBytes();
      buffer.position(8);
      buffer.put(headHash);
    } finally {
      lock.unlock();
    }
  }

  @Override
  public void close() throws BlockStoreException {
    try {
      buffer.force();
      if (System.getProperty("os.name").toLowerCase().contains("win")) {
        log.info("Windows mmap hack: Forcing buffer cleaning");
        WindowsMMapHack.forceRelease(buffer);
      }
      buffer = null; // Allow it to be GCd and the underlying file mapping to go away.
      randomAccessFile.close();
    } catch (IOException e) {
      throw new BlockStoreException(e);
    }
  }

  @Override
  public NetworkParameters getParams() {
    return params;
  }

  protected static final int RECORD_SIZE = 32 /* hash */ + StoredBlock.COMPACT_SERIALIZED_SIZE;

  // File format:
  //   4 header bytes = "SPVB"
  //   4 cursor bytes, which indicate the offset from the first kb where the next block header
  // should be written.
  //   32 bytes for the hash of the chain head
  //
  // For each header (128 bytes)
  //   32 bytes hash of the header
  //   12 bytes of chain work
  //    4 bytes of height
  //   80 bytes of block header data
  protected static final int FILE_PROLOGUE_BYTES = 1024;

  /**
   * Returns the offset from the file start where the latest block should be written (end of prev
   * block).
   */
  private int getRingCursor(ByteBuffer buffer) {
    int c = buffer.getInt(4);
    checkState(c >= FILE_PROLOGUE_BYTES, "Integer overflow");
    return c;
  }

  private void setRingCursor(ByteBuffer buffer, int newCursor) {
    checkArgument(newCursor >= 0);
    buffer.putInt(4, newCursor);
  }
}
Example #2
0
 public void destroy() {
   // FIXME: not calling event listeners .. see GLAutoDrawable spec
   if (Threading.isSingleThreaded() && !Threading.isOpenGLThread()) {
     Threading.invokeOnOpenGLThread(destroyAction);
   } else {
     destroyAction.run();
   }
 }
Example #3
0
 private void maybeDoSingleThreadedWorkaround(
     Runnable eventDispatchThreadAction, Runnable invokeGLAction, boolean isReshape) {
   if (Threading.isSingleThreaded() && !Threading.isOpenGLThread()) {
     Threading.invokeOnOpenGLThread(eventDispatchThreadAction);
   } else {
     drawableHelper.invokeGL(pbufferDrawable, context, invokeGLAction, initAction);
   }
 }
 public void startThreadingModel() {
   try {
     threadingModel.initialize();
     threadingModel.load();
     threadingModel.start();
   } catch (RuntimeException e) {
     System.err.println("Caught exception during threadingModel initialization: " + e);
     e.printStackTrace();
   }
 }