convex.core.Peer Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of convex-core Show documentation
Show all versions of convex-core Show documentation
Convex core libraries and common utilities
The newest version!
package convex.core;
import java.io.IOException;
import java.util.function.Consumer;
import java.util.stream.Stream;
import convex.core.crypto.AKeyPair;
import convex.core.data.ACell;
import convex.core.data.AMap;
import convex.core.data.AVector;
import convex.core.data.AccountKey;
import convex.core.data.AccountStatus;
import convex.core.data.Address;
import convex.core.data.Hash;
import convex.core.data.Keyword;
import convex.core.data.Keywords;
import convex.core.data.Maps;
import convex.core.data.PeerStatus;
import convex.core.data.Ref;
import convex.core.data.SignedData;
import convex.core.data.Vectors;
import convex.core.data.prim.CVMLong;
import convex.core.exceptions.InvalidDataException;
import convex.core.init.Init;
import convex.core.lang.Context;
import convex.core.store.AStore;
import convex.core.store.Stores;
import convex.core.transactions.ATransaction;
import convex.core.util.Utils;
/**
*
* Immutable class representing the encapsulated state of a Peer
*
*
* SECURITY:
*
* - Needs to contain the Peer's unlocked private key for online signing.
* - Manages Peer state transitions given external events. Must do so
* correctly.
*
*
*
* Must have at least one state, the initial state. New states will be added as
* consensus updates happen.
*
*
*
* "Don't worry about what anybody else is going to do. The best way to predict
* the future is to invent it." - Alan Kay
*/
public class Peer {
/** This Peer's key */
private final AccountKey peerKey;
/**
* This Peer's key pair
*
* Make transient to mark that this should never be Persisted by accident
*/
private transient final AKeyPair keyPair;
/** The latest merged belief */
private final Belief belief;
/**
* The latest observed timestamp. This is increased by the Server polling the
* local clock.
*/
private final long timestamp;
/**
* Current state index
*/
private final long statePosition;
/**
* Current consensus Order
*/
private final Order consensusOrder;
/**
* Current base history position, from which results are stored
*/
private final long historyPosition;
/**
* Current consensus state
*/
private final State state;
/**
* Current consensus state
*/
private final State genesis;
/**
* Vector of results
*/
private final AVector blockResults;
private Peer(AKeyPair kp, Belief belief, Order consensusOrder, long pos, State state, State genesis, long history, AVector results,
long timeStamp) {
this.keyPair = kp;
this.peerKey = kp.getAccountKey();
this.belief = belief;
this.state = state;
this.genesis=genesis;
this.timestamp = timeStamp;
this.consensusOrder=consensusOrder;
this.statePosition=pos;
this.historyPosition=history;
this.blockResults = results;
}
/**
* Constructs a Peer instance from persisted PEer Data
* @param keyPair Key Pair for Peer
* @param peerData Peer data map
* @return New Peer instance
*/
@SuppressWarnings("unchecked")
public static Peer fromData(AKeyPair keyPair,AMap peerData) {
Belief belief=(Belief) peerData.get(Keywords.BELIEF);
AVector results=(AVector) peerData.get(Keywords.RESULTS);
State state=(State) peerData.get(Keywords.STATE);
State genesis=(State) peerData.get(Keywords.GENESIS);
long pos=((CVMLong) peerData.get(Keywords.POSITION)).longValue();
Order co=((Order) peerData.get(Keywords.ORDER));
long hpos=((CVMLong) peerData.get(Keywords.HISTORY)).longValue();
long timestamp=((CVMLong) peerData.get(Keywords.TIMESTAMP)).longValue();
return new Peer(keyPair,belief,co,pos,state,genesis,hpos,results,timestamp);
}
/**
* Gets the Peer Data map for this Peer
* @return Peer data
*/
public AMap toData() {
return Maps.of(
Keywords.BELIEF,belief,
Keywords.HISTORY,CVMLong.create(historyPosition),
Keywords.ORDER,consensusOrder,
Keywords.RESULTS,blockResults,
Keywords.POSITION,CVMLong.create(statePosition),
Keywords.STATE,state,
Keywords.GENESIS,genesis,
Keywords.TIMESTAMP,timestamp
);
}
/**
* Creates a Peer
* @param peerKP Key Pair
* @param genesis Genesis State
* @return New Peer instance
*/
public static Peer create(AKeyPair peerKP, State genesis) {
Belief belief = Belief.createSingleOrder(peerKP);
return new Peer(peerKP, belief, Order.create(),0L,genesis,genesis, 0,Vectors.empty(),genesis.getTimestamp().longValue());
}
/**
* Create a Peer instance from a remotely acquired Belief
* @param peerKP Peer KeyPair
* @param genesisState Initial genesis State of the Network
* @param remoteBelief Remote belief to sync with
* @return New Peer instance
* @throws InvalidDataException if invalid data was found in merged belief
*/
public static Peer create(AKeyPair peerKP, State genesisState, Belief remoteBelief) throws InvalidDataException {
Peer peer=create(peerKP,genesisState);
peer=peer.updateTimestamp(Utils.getCurrentTimestamp());
peer=peer.mergeBeliefs(remoteBelief);
return peer;
}
/**
* Create a Peer instance from a remotely acquired State and Order
* @param peerKP Peer KeyPair
* @param genesisState Initial genesis State of the Network
* @param remoteBelief Remote belief to sync with
* @return New Peer instance
*/
// public static Peer create(AKeyPair peerKP, State genesisState, State consensusState, SignedData order) {
// SignedData myOrder=peerKP.signData(order.getValue());
// Belief b=Belief.create(order,myOrder); // two orders in Belief at least....
// Peer peer=create(peerKP,genesisState,consensusState,b);
// peer=peer.updateTimestamp(Utils.getCurrentTimestamp());
// peer=peer.mergeBeliefs(remoteBelief);
// return peer;
// }
/**
* Like {@link #restorePeer(AStore, AKeyPair, ACell)} but uses a null root key.
* @param store Store to restore from
* @param keyPair Key Pair to use for restored Peer
* @return Restored Peer instance
* @throws IOException In case of IO error
*/
public static Peer restorePeer(AStore store, AKeyPair keyPair) throws IOException {
return restorePeer(store, keyPair, null);
}
/**
* Restores a Peer from the Etch database specified in Config
* @param store Store to restore from
* @param keyPair Key Pair to use for restored Peer
* @param rootKey When not null, assumes the root data is a map and peer data is under that key
* @return Peer instance, or null if root hash was not found
* @throws IOException If store reading failed
*/
public static Peer restorePeer(AStore store, AKeyPair keyPair, ACell rootKey) throws IOException {
AMap peerData=getPeerData(store, rootKey);
if (peerData==null) return null;
Peer peer=Peer.fromData(keyPair,peerData);
return peer;
}
/**
* Like {@link #getPeerData(AStore, ACell)} but uses a null root key.
* @param store store from which to load Peer data
* @return Peer data map
* @throws IOException In case of IOException
*/
public static AMap getPeerData(AStore store) throws IOException {
return getPeerData(store, null);
}
/**
* Gets Peer Data from a Store.
*
* @param store Store to retrieve Peer Data from
* @param rootKey When not null, assumes the root data is a map and peer data is under that key
* @return Peer data map, or null if not available
* @throws IOException If a store IO error occurs
*/
@SuppressWarnings("unchecked")
public static AMap getPeerData(AStore store, ACell rootKey) throws IOException {
Stores.setCurrent(store);
Hash root = store.getRootHash();
Ref ref=store.refForHash(root);
if (ref==null) return null; // not found case
if (ref.getStatus())ref.getValue();
else return (AMap)((AMap)ref.getValue()).get(rootKey);
}
/**
* Creates a new Peer instance at server startup using the provided
* configuration. Current store must be set to store for server.
*
* @param keyPair Key pair for genesis peer
* @param genesisState Genesis state, or null to generate fresh state
* @return A new Peer instance
*/
public static Peer createGenesisPeer(AKeyPair keyPair, State genesisState) {
if (keyPair == null) throw new IllegalArgumentException("Peer initialisation requires a keypair");
if (genesisState == null) {
genesisState=Init.createState(Utils.listOf(keyPair.getAccountKey()));
genesisState=genesisState.withTimestamp(Utils.getCurrentTimestamp());
}
return create(keyPair, genesisState);
}
/**
* Updates the timestamp to the specified time, going forwards only
*
* @param newTimestamp New Peer timestamp
* @return This peer updated with the given timestamp
*/
public Peer updateTimestamp(long newTimestamp) {
if (newTimestamp <= timestamp) return this;
return new Peer(keyPair, belief, consensusOrder,statePosition,state,genesis, historyPosition,blockResults, newTimestamp);
}
/**
* Compiles and executes a query on the current consensus state of this Peer.
*
* @param form Form to compile and execute.
* @param address Address to use for query execution. If null, core address will be used
* @return The Context containing the query results. Will be NOBODY error if address / account does not exist
*/
public ResultContext executeQuery(ACell form, Address address) {
State state=getConsensusState();
if (form instanceof ATransaction) {
return executeDetached((ATransaction)form);
}
if (form instanceof SignedData) {
SignedData> sc=(SignedData>)form;
ACell val=sc.getValue();
if (form instanceof ATransaction) {
return executeDetached((ATransaction)val);
}
}
if (address==null) {
address=Init.getGenesisAddress();
//return Context.createFake(state).withError(ErrorCodes.NOBODY,"Null Address provided for query");
} else if (!state.hasAccount(address)) {
return ResultContext.error(state,ErrorCodes.NOBODY,"Query for non-existant account");
}
// Run query in a fake context
Context ctx=Context.create(state, address, Constants.MAX_TRANSACTION_JUICE);
ctx=ctx.run(form);
ResultContext rctx=ResultContext.fromContext(ctx);
return rctx;
}
/**
* Executes a "detached" transaction on the current consensus state of this Peer, but without any effect on current CVM state.
* This can be used for query, estimating potential fees etc.
*
* @param transaction Transaction to execute
* @return The Context containing the transaction results.
*/
public ResultContext executeDetached(ATransaction transaction) {
ResultContext ctx=getConsensusState().applyTransaction(transaction);
return ctx;
}
/**
* Executes a query in this Peer's current Consensus State, using a default address
* @param form Form to execute as a Query
* @return Context after executing query
*/
public ResultContext executeQuery(ACell form) {
return executeQuery(form,Init.getGenesisAddress());
}
/**
* Gets the timestamp of this Peer
* @return Timestamp
*/
public long getTimestamp() {
return timestamp;
}
/**
* Gets the Peer Public Key of this Peer.
* @return Peer Key of Peer.
*/
public AccountKey getPeerKey() {
return peerKey;
}
/**
* Gets the controller Address for this Peer
* @return Address of Peer controller Account, or null if does not exist
*/
public Address getController() {
PeerStatus ps= getConsensusState().getPeer(peerKey);
if (ps==null) return null;
return ps.getController();
}
/**
* Gets the Peer Key of this Peer.
* @return Address of Peer.
*/
public AKeyPair getKeyPair() {
return keyPair;
}
/**
* Get the current Belief of this Peer
* @return Belief
*/
public Belief getBelief() {
return belief;
}
/**
* Signs a value with the keypair of this Peer
* @param Type of value to sign
* @param value Value to sign
* @return Signed data value
*/
public SignedData sign(T value) {
return keyPair.signData(value);
}
/**
* Gets the current consensus state for this Peer
*
* @return Consensus state for this chain (genesis state if no block consensus)
*/
public State getConsensusState() {
return state;
}
/**
* Merges a set of new Beliefs into this Peer's belief. Beliefs may be null, in
* which case they are ignored. Should call updateState() at some point after this to
* update the global consensus state
*
* @param beliefs An array of Beliefs. May contain nulls, which will be ignored.
* @return Updated Peer after Belief Merge
* @throws InvalidDataException if
*
*/
public Peer mergeBeliefs(Belief... beliefs) throws InvalidDataException {
Belief belief=getBelief();
BeliefMerge mc = BeliefMerge.create(belief,keyPair, timestamp, getConsensusState());
Belief newBelief = mc.merge(beliefs);
long ocp=getFinalityPoint();
Order newOrder=newBelief.getOrder(peerKey);
long ncp=newOrder.getConsensusPoint(Constants.CONSENSUS_LEVEL_FINALITY);
if (ocp>ncp) {
// This probably shouldn't happen, but just in case.....
System.err.println("Receding consensus? Old CP="+ocp +", New CP="+ncp);
}
Peer p= updateBelief(newBelief);
if (p==this) return this;
return p;
}
/**
* Prunes History before the given timestamp
* @param ts Timestamp from which to to keep History
* @return Updated Peer with pruned History
*/
public Peer pruneHistory(long ts) {
// Return this if we don't possibly have anything to prune
if (blockResults.count()==0) return this;
// Exit without change if there is nothing before the given timestamp to prune
long firstTs=blockResults.get(0).getState().getTimestamp().longValue();
if (tsbr.getState().getTimestamp(), (a,b)->a.compareTo(b), CVMLong.create(ts));
// TODO: complete pruning
return this;
}
/**
* Update this Peer with a new consensus Belief
*
* @param newBelief Belief to apply
* @return Updated Peer
*/
public Peer updateBelief(Belief newBelief) {
if (belief == newBelief) return this;
return new Peer(keyPair, newBelief, consensusOrder,statePosition,state, genesis, historyPosition,blockResults, timestamp);
}
/**
* Updates the state of the Peer based on latest consensus Belief
* @return Updated Peer
*/
public Peer updateState() {
Order myOrder = belief.getOrder(peerKey); // this peer's Order from latest belief
long consensusPoint = myOrder.getConsensusPoint(Constants.CONSENSUS_LEVEL_FINALITY);
AVector> blocks = myOrder.getBlocks();
AVector> consensusBlocks= consensusOrder.getBlocks();
State s = this.state;
AVector newResults = this.blockResults;
long stateIndex=statePosition;
long consensusMatch=blocks.commonPrefixLength(consensusBlocks);
if (consensusMatch=consensusPoint) return this;
// Kick off parallel signature validation
validateSignatures(s,blocks,stateIndex,consensusPoint);
// We need to compute at least one new state update
while (stateIndex < consensusPoint) { // add states until last state is at consensus point
SignedData block = blocks.get(stateIndex);
BlockResult br = s.applyBlock(block);
State newState=br.getState();
newResults = newResults.append(br);
if (newState.equals(s)) {
// Nothing changes in CVM state, so the Block must have been invalid!
if (peerKey.equals(block.getAccountKey())) {
// We messed up, or someone was messing with us in a serious way.....
}
}
s=newState;
stateIndex++;
}
return new Peer(keyPair, belief, myOrder,stateIndex,s, genesis, historyPosition,newResults, timestamp);
}
private void validateSignatures(State s, AVector> blocks, long start, long end) {
Consumer> transactionValidator=st->{
ATransaction t=st.getValue();
Address origin=t.getOrigin();
AccountStatus as=s.getAccount(origin);
if (as==null) return; // ignore, will probably fail with :NOBODY
AccountKey pk=as.getAccountKey();
if (pk==null) return; // ignore, will probably fail as an actor account
st.checkSignature(pk);
};
Consumer> blockValidator=sb->{
AVector> transactions = sb.getValue().getTransactions();
Stream> tstream=transactions.parallelStream();
tstream.forEach(transactionValidator);
};
Stream> stream=blocks.parallelStream().skip(start).limit(end-start);
stream.forEach(blockValidator);
}
/**
* Gets a historical State for the specified position
* @param consensusMatch
* @return Historical state, or null if not available
*/
private State getHistoricalState(long pos) {
if (pos==0) return genesis;
long hpos=pos-historyPosition;
if (hpos<1) return null;
if (hpos>blockResults.count()) return null;
BlockResult br=blockResults.get(hpos-1);
return br.getState();
}
/**
* Gets the Result of a specific transaction
* @param blockIndex Index of Block in Order
* @param txIndex Index of transaction in block
* @return Result from transaction, or null if the transaction does not exist or is no longer stored
*/
public Result getResult(long blockIndex, long txIndex) {
BlockResult br=getBlockResult(blockIndex);
if (br==null) return null;
return br.getResult(txIndex);
}
/**
* Gets the BlockResult of a specific block index
* @param i Index of Block
* @return BlockResult, or null if the BlockResult is not stired
*/
public BlockResult getBlockResult(long i) {
if (i=blockResults.count()) return null;
return blockResults.get(brix);
}
/**
* Propose a new Block. Adds the Block to the current proposed Order for this
* Peer. Also increments Peer timestamp if necessary for new Block
*
* @param block Block to publish
* @return Peer after proposing new Block in Peer's own Order
*/
public Peer proposeBlock(Block block) {
SignedData signedBlock=sign(block);
Belief newBelief=belief.proposeBlock(keyPair, signedBlock);
Peer result=this;
// Update timestamp if necessary to accommodate Block
long blockTimeStamp=block.getTimeStamp();
if (blockTimeStamp>result.getTimestamp()) {
result=result.updateTimestamp(blockTimeStamp);
}
result=result.updateBelief(newBelief);
result=result.updateState();
return result;
}
/**
* Gets the Final Point for this Peer
* @return Consensus Point value
*/
public long getFinalityPoint() {
Order order=getPeerOrder();
if (order==null) return 0;
return order.getConsensusPoint(Constants.CONSENSUS_LEVEL_FINALITY);
}
/**
* Gets the current Order for this Peer
*
* @return The Order for this peer in its current Belief. Will return null if the Peer is not a peer in the current consensus state
*
*/
public Order getPeerOrder() {
return getBelief().getOrder(peerKey);
}
/**
* Gets the current chain this Peer sees for a given peer address
*
* @param peerKey Peer Key
* @return The current Order for the specified peer
*/
public Order getOrder(AccountKey peerKey) {
return getBelief().getOrder(peerKey);
}
/**
* Get the Network ID for this PEer
* @return Network ID
*/
public Hash getNetworkID() {
return genesis.getHash();
}
/**
* Gets the state position of this Peer, which is equal to the number of state transitions executed.
* @return Position
*/
public long getStatePosition() {
return statePosition;
}
/**
* Gets the genesis State of this Peer
* @return Genesis State
*/
public State getGenesisState() {
return genesis;
}
}