mirror of
https://github.com/bisq-network/bisq.git
synced 2025-02-23 15:00:30 +01:00
Use NonMMappedSPVBlockStore on windows.
Fixes https://github.com/bisq-network/bisq/issues/2402
This commit is contained in:
parent
962ef6e388
commit
9f9de57251
2 changed files with 346 additions and 1 deletions
|
@ -0,0 +1,341 @@
|
|||
/*
|
||||
* Copyright 2013 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* This file is part of Bisq.
|
||||
*
|
||||
* Bisq is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Bisq is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.core.btc.setup;
|
||||
|
||||
import org.bitcoinj.core.*;
|
||||
import org.bitcoinj.store.BlockStore;
|
||||
import org.bitcoinj.store.BlockStoreException;
|
||||
import org.bitcoinj.store.ChainFileLockedException;
|
||||
import org.bitcoinj.utils.*;
|
||||
import org.slf4j.*;
|
||||
|
||||
import javax.annotation.*;
|
||||
import java.io.*;
|
||||
import java.nio.*;
|
||||
import java.nio.channels.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.locks.*;
|
||||
|
||||
import static com.google.common.base.Preconditions.*;
|
||||
|
||||
/**
|
||||
* NonMMappedSPVBlockStore is like SPVBlockStore but it does not use a memory mapped file
|
||||
* to access the file systen. Memory mapped file has problems on windows on restore from
|
||||
* seed because of problems releasing control of mmapped files.
|
||||
* We proposed a similar solution on bitcoinj upstream, so we hopefully will be able to
|
||||
* remove this class and use the upstream version.
|
||||
* See:
|
||||
* <a href="http://www.mapdb.org/blog/mmap_files_alloc_and_jvm_crash/">Mapdb on mmapped files</a>
|
||||
* <a href="https://bugs.java.com/bugdatabase/view_bug.do?bug_id=4715154">Java bug 1</a>
|
||||
* <a href="https://bugs.java.com/bugdatabase/view_bug.do?bug_id=4724038">Java bug 2</a>
|
||||
* <a href="https://bugs.java.com/bugdatabase/view_bug.do?bug_id=6359560">Java bug 3</a>
|
||||
*/
|
||||
public class NonMMappedSPVBlockStore implements BlockStore {
|
||||
private static final Logger log = LoggerFactory.getLogger(NonMMappedSPVBlockStore.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 int numHeaders;
|
||||
protected NetworkParameters params;
|
||||
|
||||
protected ReentrantLock lock = Threading.lock("SPVBlockStore");
|
||||
|
||||
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;
|
||||
private final String fileAbsolutePath;
|
||||
|
||||
/**
|
||||
* Creates and initializes an SPV block store. Will create the given file if it's missing. This operation
|
||||
* will block on disk.
|
||||
*/
|
||||
public NonMMappedSPVBlockStore(NetworkParameters params, File file) throws BlockStoreException {
|
||||
checkNotNull(file);
|
||||
fileAbsolutePath = file.getAbsolutePath();
|
||||
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 ChainFileLockedException("Store file is already locked by another process");
|
||||
|
||||
// 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];
|
||||
randomAccessFile.read(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");
|
||||
randomAccessFile.write(header);
|
||||
// Insert the genesis block.
|
||||
lock.lock();
|
||||
try {
|
||||
setRingCursor(randomAccessFile, 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 final int getFileSize() {
|
||||
return RECORD_SIZE * numHeaders + FILE_PROLOGUE_BYTES /* extra kilobyte for stuff */;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void put(StoredBlock block) throws BlockStoreException {
|
||||
final RandomAccessFile randomAccessFile = this.randomAccessFile;
|
||||
if (randomAccessFile == null) throw new BlockStoreException("Store closed");
|
||||
|
||||
lock.lock();
|
||||
try {
|
||||
int cursor = getRingCursor(randomAccessFile);
|
||||
if (cursor == getFileSize()) {
|
||||
// Wrapped around.
|
||||
cursor = FILE_PROLOGUE_BYTES;
|
||||
}
|
||||
randomAccessFile.seek(cursor);
|
||||
Sha256Hash hash = block.getHeader().getHash();
|
||||
notFoundCache.remove(hash);
|
||||
randomAccessFile.write(hash.getBytes());
|
||||
ByteBuffer buffer = ByteBuffer.allocate(StoredBlock.COMPACT_SERIALIZED_SIZE);
|
||||
block.serializeCompact(buffer);
|
||||
buffer.flip();
|
||||
FileChannel channel = randomAccessFile.getChannel();
|
||||
channel.write(buffer);
|
||||
setRingCursor(randomAccessFile, (int) randomAccessFile.getFilePointer());
|
||||
blockCache.put(hash, block);
|
||||
} catch (IOException ioException) {
|
||||
throw new BlockStoreException(ioException);
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public StoredBlock get(Sha256Hash hash) throws BlockStoreException {
|
||||
final RandomAccessFile randomAccessFile = this.randomAccessFile;
|
||||
if (randomAccessFile == 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(randomAccessFile);
|
||||
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.
|
||||
randomAccessFile.seek(cursor);
|
||||
randomAccessFile.read(scratch);
|
||||
if (Arrays.equals(scratch, targetHashBytes)) {
|
||||
// Found the target.
|
||||
ByteBuffer buffer = ByteBuffer.allocate(StoredBlock.COMPACT_SERIALIZED_SIZE);
|
||||
FileChannel channel = randomAccessFile.getChannel();
|
||||
channel.read(buffer);
|
||||
buffer.flip();
|
||||
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.
|
||||
} catch (IOException ioException) {
|
||||
throw new BlockStoreException(ioException);
|
||||
} finally { lock.unlock(); }
|
||||
}
|
||||
|
||||
protected StoredBlock lastChainHead = null;
|
||||
|
||||
@Override
|
||||
public StoredBlock getChainHead() throws BlockStoreException {
|
||||
final RandomAccessFile randomAccessFile = this.randomAccessFile;
|
||||
if (randomAccessFile == null) throw new BlockStoreException("Store closed");
|
||||
|
||||
lock.lock();
|
||||
try {
|
||||
if (lastChainHead == null) {
|
||||
byte[] headHash = new byte[32];
|
||||
randomAccessFile.seek(8);
|
||||
randomAccessFile.read(headHash);
|
||||
Sha256Hash hash = Sha256Hash.wrap(headHash);
|
||||
StoredBlock block = get(hash);
|
||||
if (block == null)
|
||||
throw new BlockStoreException("Corrupted block store: could not find chain head: " + hash
|
||||
+"\nFile path: "+ fileAbsolutePath);
|
||||
lastChainHead = block;
|
||||
}
|
||||
return lastChainHead;
|
||||
} catch (IOException ioException) {
|
||||
throw new BlockStoreException(ioException);
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setChainHead(StoredBlock chainHead) throws BlockStoreException {
|
||||
final RandomAccessFile randomAccessFile = this.randomAccessFile;
|
||||
if (randomAccessFile == null) throw new BlockStoreException("Store closed");
|
||||
|
||||
lock.lock();
|
||||
try {
|
||||
lastChainHead = chainHead;
|
||||
byte[] headHash = chainHead.getHeader().getHash().getBytes();
|
||||
randomAccessFile.seek(8);
|
||||
randomAccessFile.write(headHash);
|
||||
} catch (IOException ioException) {
|
||||
throw new BlockStoreException(ioException);
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws BlockStoreException {
|
||||
try {
|
||||
randomAccessFile.close();
|
||||
randomAccessFile = null; // Set to null to avoid trying to use a closed file.
|
||||
} 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(RandomAccessFile randomAccessFile) throws IOException {
|
||||
long filePointer = randomAccessFile.getFilePointer();
|
||||
randomAccessFile.seek(4);
|
||||
int c = randomAccessFile.readInt();
|
||||
randomAccessFile.seek(filePointer);
|
||||
checkState(c >= FILE_PROLOGUE_BYTES, "Integer overflow");
|
||||
return c;
|
||||
}
|
||||
|
||||
private void setRingCursor(RandomAccessFile randomAccessFile, int newCursor) throws IOException {
|
||||
checkArgument(newCursor >= 0);
|
||||
long filePointer = randomAccessFile.getFilePointer();
|
||||
randomAccessFile.seek(4);
|
||||
randomAccessFile.writeInt(newCursor);
|
||||
randomAccessFile.seek(filePointer);
|
||||
}
|
||||
|
||||
}
|
|
@ -326,7 +326,11 @@ public class WalletConfig extends AbstractIdleService {
|
|||
* Override this to use a {@link BlockStore} that isn't the default of {@link SPVBlockStore}.
|
||||
*/
|
||||
private BlockStore provideBlockStore(File file) throws BlockStoreException {
|
||||
return new SPVBlockStore(params, file);
|
||||
if (System.getProperty("os.name").toLowerCase().contains("win")) {
|
||||
return new NonMMappedSPVBlockStore(params, file);
|
||||
} else {
|
||||
return new SPVBlockStore(params, file);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
Loading…
Add table
Reference in a new issue