package peergos.shared.user; import peergos.shared.crypto.*; import peergos.shared.crypto.asymmetric.*; import peergos.shared.io.ipfs.multihash.*; import peergos.shared.merklebtree.MaybeMultihash; import peergos.shared.merklebtree.MerkleBTree; import peergos.shared.mutable.*; import peergos.shared.storage.ContentAddressedStorage; import peergos.shared.util.*; import java.util.*; import java.util.concurrent.*; public class BtreeImpl implements Btree { private final MutablePointers mutable; private final ContentAddressedStorage dht; private static final boolean LOGGING = false; private final Map<PublicSigningKey, CompletableFuture<CommittedWriterData>> pending = new HashMap<>(); public BtreeImpl(MutablePointers mutable, ContentAddressedStorage dht) { this.mutable = mutable; this.dht = dht; } private <T> T log(T result, String toPrint) { if (LOGGING) System.out.println(toPrint); return result; } private CompletableFuture<CommittedWriterData> getWriterData(MaybeMultihash hash) { if (!hash.isPresent()) return CompletableFuture.completedFuture(new CommittedWriterData(MaybeMultihash.EMPTY(), WriterData.createEmpty())); return dht.get(hash.get()) .thenApply(cborOpt -> { if (! cborOpt.isPresent()) throw new IllegalStateException("Couldn't retrieve WriterData from dht! " + hash); return new CommittedWriterData(hash, WriterData.fromCbor(cborOpt.get(), null)); }); } private CompletableFuture<CommittedWriterData> getWriterData(PublicSigningKey pubKey) { return mutable.getPointer(pubKey) .thenCompose(this::getWriterData); } private CompletableFuture<CommittedWriterData> addToQueue(PublicSigningKey pubKey, CompletableFuture<CommittedWriterData> lock) { synchronized (pending) { // This is subtle, but we need to ensure that there is only ever 1 thenAble waiting on the future for a given key // otherwise when the future completes, then the two or more waiters will both proceed with the existing hash, // and whoever commits first will win. We also need to retrieve the writer data again from the network after // a previous transaction has completed (another node/user may have updated the mapping) if (pending.containsKey(pubKey)) { return pending.put(pubKey, lock).thenCompose(x -> getWriterData(pubKey)); } pending.put(pubKey, lock); return getWriterData(pubKey); } } @Override public CompletableFuture<Boolean> put(SigningKeyPair writer, byte[] mapKey, Multihash value) { PublicSigningKey publicWriterKey = writer.publicSigningKey; CompletableFuture<CommittedWriterData> lock = new CompletableFuture<>(); return addToQueue(publicWriterKey, lock) .thenCompose(committed -> { WriterData holder = committed.props; MaybeMultihash btreeRootHash = holder.btree.isPresent() ? MaybeMultihash.of(holder.btree.get()) : MaybeMultihash.EMPTY(); return MerkleBTree.create(publicWriterKey, btreeRootHash, dht) .thenCompose(btree -> btree.put(publicWriterKey, mapKey, value)) .thenApply(newRoot -> LOGGING ? log(newRoot, "BTREE.put (" + ArrayOps.bytesToHex(mapKey) + ", " + value + ") => CAS(" + btreeRootHash + ", " + newRoot + ")") : newRoot) .thenCompose(newBtreeRoot -> holder.withBtree(newBtreeRoot) .commit(writer, committed.hash, mutable, dht, lock::complete)) .thenApply(x -> true); }); } @Override public CompletableFuture<MaybeMultihash> get(PublicSigningKey writer, byte[] mapKey) { CompletableFuture<CommittedWriterData> lock = new CompletableFuture<>(); return addToQueue(writer, lock) .thenCompose(committed -> { lock.complete(committed); WriterData holder = committed.props; MaybeMultihash btreeRootHash = holder.btree.isPresent() ? MaybeMultihash.of(holder.btree.get()) : MaybeMultihash.EMPTY(); return MerkleBTree.create(writer, btreeRootHash, dht) .thenCompose(btree -> btree.get(mapKey)) .thenApply(maybe -> LOGGING ? log(maybe, "BTREE.get (" + ArrayOps.bytesToHex(mapKey) + ", root="+btreeRootHash+" => " + maybe) : maybe); }); } @Override public CompletableFuture<Boolean> remove(SigningKeyPair writer, byte[] mapKey) { PublicSigningKey publicWriter = writer.publicSigningKey; CompletableFuture<CommittedWriterData> future = new CompletableFuture<>(); return addToQueue(publicWriter, future) .thenCompose(committed -> { WriterData holder = committed.props; MaybeMultihash btreeRootHash = holder.btree.isPresent() ? MaybeMultihash.of(holder.btree.get()) : MaybeMultihash.EMPTY(); return MerkleBTree.create(publicWriter, btreeRootHash, dht) .thenCompose(btree -> btree.delete(publicWriter, mapKey)) .thenApply(pair -> LOGGING ? log(pair, "BTREE.rm (" + ArrayOps.bytesToHex(mapKey) + " => " + pair) : pair) .thenCompose(newBtreeRoot -> holder.withBtree(newBtreeRoot) .commit(writer, committed.hash, mutable, dht, future::complete)) .thenApply(x -> true); }); } }