![JAR search and dependency download from the Maven repository](/logo.png)
convex.core.lang.Context Maven / Gradle / Ivy
Show all versions of convex-core Show documentation
package convex.core.lang;
import convex.core.Coin;
import convex.core.Constants;
import convex.core.ErrorCodes;
import convex.core.ResultContext;
import convex.core.SourceCodes;
import convex.core.State;
import convex.core.data.ACell;
import convex.core.data.AHashMap;
import convex.core.data.AList;
import convex.core.data.ASequence;
import convex.core.data.AString;
import convex.core.data.AVector;
import convex.core.data.AccountKey;
import convex.core.data.AccountStatus;
import convex.core.data.Address;
import convex.core.data.Index;
import convex.core.data.Hash;
import convex.core.data.Keyword;
import convex.core.data.Keywords;
import convex.core.data.MapEntry;
import convex.core.data.Maps;
import convex.core.data.PeerStatus;
import convex.core.data.Strings;
import convex.core.data.Symbol;
import convex.core.data.Syntax;
import convex.core.data.Vectors;
import convex.core.data.prim.CVMLong;
import convex.core.data.type.AType;
import convex.core.data.util.BlobBuilder;
import convex.core.init.Init;
import convex.core.lang.exception.AExceptional;
import convex.core.lang.exception.AThrowable;
import convex.core.lang.exception.ATrampoline;
import convex.core.lang.exception.ErrorValue;
import convex.core.lang.exception.Failure;
import convex.core.lang.exception.HaltValue;
import convex.core.lang.exception.RecurValue;
import convex.core.lang.exception.ReducedValue;
import convex.core.lang.exception.ReturnValue;
import convex.core.lang.exception.RollbackValue;
import convex.core.lang.exception.TailcallValue;
import convex.core.lang.impl.CoreFn;
import convex.core.util.Economics;
import convex.core.util.Errors;
/**
* Representation of CVM execution context.
*
*
* Execution context includes:
* - The current on-Chain state, including the defined execution environment for each Address
* - Local lexical bindings for the current execution position
* - The identity (as an Address) for the origin, caller and currently executing actor
* - Juice and execution depth current status for
* - Result of the last operation executed (which may be exceptional)
*
* Interestingly, this behaves like Scala's ZIO[Context-Stuff, AExceptional, T]
*
* Contexts maintain checks on execution depth and juice to control against arbitrary on-chain
* execution. Coupled with limits on total juice and limits on memory allocation
* per unit juice, this places an upper bound on execution time and space.
*
* Contexts also support returning exceptional values. Exceptional results may come
* from arbitrary nested depth (which requires a bit of complexity to reset depth when
* catching exceptional values). We avoid using Java exceptions here, because exceptionals
* are "normal" in the context of on-chain execution, and we'd like to avoid the overhead
* of exception handling - may be especially important in DoS scenarios.
*
* "If you have a procedure with 10 parameters, you probably missed some"
* - Alan Perlis
*
*/
public class Context {
private static final long INITIAL_JUICE = 0;
// Default values
private static final AVector> DEFAULT_LOG = null;
private static int ZERO_DEPTH = 0;
private static final AExceptional DEFAULT_EXCEPTION = null;
private static final long ZERO_OFFER = 0L;
public static final AVector EMPTY_BINDINGS=Vectors.empty();
private static final ACell NO_RESULT = null;
private static final ACell NULL_SCOPE = null;
private static final AExceptional NO_EXCEPTION = null;
private static final CompilerState NO_COMPILER_STATE = null;
// private static final Logger log=Logger.getLogger(Context.class.getName());
/*
* Frequently changing mutable fields during execution.
*
* While these are mutable, it is also very cheap to just fork() short-lived Contexts
* because the JVM generational GC will just sweep them up shortly afterwards.
*/
private long juice;
private long juiceLimit;
private ACell result;
private AExceptional exception;
private int depth;
private AVector localBindings;
private ChainState chainState;
/**
* Local log is an ordered [vector of [address caller scope location [values ...] ] entries
* See CAD33 for details
*/
private AVector> log;
private CompilerState compilerState;
/**
* Inner class compiler state.
*
* Maintains a mapping of Symbols to positions in a definition vector corresponding to lexical scope.
*
*/
public static final class CompilerState {
public static final CompilerState EMPTY = new CompilerState(Vectors.empty(),Maps.empty());
private AVector definitions;
private AHashMap mappings;
private CompilerState(AVector definitions, AHashMap mappings) {
this.definitions=definitions;
this.mappings=mappings;
}
public CompilerState define(Symbol sym, Syntax syn) {
long position=definitions.count();
AVector newDefs=definitions.conj(syn);
AHashMap newMaps=mappings.assoc(sym, CVMLong.create(position));
return new CompilerState(newDefs,newMaps);
}
public CVMLong getPosition(Symbol sym) {
return mappings.get(sym);
}
}
/**
* Inner class for less-frequently changing CVM state related to Actor execution
* Should save some allocation / GC on average, since it will change less
* frequently than the surrounding Context and can be cheaply copied by reference.
*
* SECURITY: security critical, since it determines the current *address* and *caller*
* which in turn controls access to most account resources and rights.
*/
protected static final class ChainState {
private final State state;
private final Address origin;
private final Address caller;
private final Address address;
private final ACell scope;
private final long offer;
/**
* Cached copy of the current environment. Avoid looking up via Address each time.
*/
private final AccountStatus account;
private ChainState(State state, Address origin,Address caller, Address address,AccountStatus account, long offer,ACell scope) {
this.state=state;
this.origin=origin;
this.caller=caller;
this.address=address;
this.account=account;
this.offer=offer;
this.scope=scope;
}
public static ChainState create(State state, Address origin, Address caller, Address address, long offer, ACell scope) {
AccountStatus as=state.getAccount(address);
if (as==null) return null;
return new ChainState(state,origin,caller,address,as,offer,scope);
}
public ChainState withStateOffer(State newState,long newOffer) {
if ((state==newState)&&(offer==newOffer)) return this;
return create(newState,origin,caller,address,newOffer,scope);
}
private ChainState withState(State newState) {
if (state==newState) return this;
return create(newState,origin,caller,address,offer,scope);
}
protected long getOffer() {
return offer;
}
protected ACell getScope() {
return scope;
}
/**
* Gets the current defined environment
* @return
*/
private AHashMap getEnvironment() {
return account.getEnvironment();
}
private ChainState withEnvironment(AHashMap newEnvironment) {
if (account.getEnvironment()==newEnvironment) return this;
AccountStatus nas=account.withEnvironment(newEnvironment);
State newState=state.putAccount(address,nas);
return withState(newState);
}
public ChainState withEnvironment(AHashMap newEnvironment,
AHashMap> newMeta) {
if ((account.getEnvironment()==newEnvironment)&&(account.getMetadata()==newMeta)) return this;
AccountStatus nas=account.withEnvironment(newEnvironment).withMetadata(newMeta);
State newState=state.putAccount(address,nas);
return withState(newState);
}
private ChainState withAccounts(AVector newAccounts) {
return withState(state.withAccounts(newAccounts));
}
public AHashMap> getMetadata() {
return account.getMetadata();
}
public ChainState withScope(ACell newScope) {
if (scope==newScope) return this;
return create(state,origin,caller,address,offer,newScope);
}
public AccountStatus getAccount() {
return account;
}
public AccountStatus getOriginAccount() {
if (address.equals(origin)) return account;
return state.getAccount(origin);
}
}
protected Context(ChainState chainState, long juice, long juiceLimit, AVector localBindings2,ACell result, int depth, AExceptional exception, AVector> log, CompilerState comp) {
this.chainState=chainState;
this.juice=juice;
this.juiceLimit=juiceLimit;
this.localBindings=localBindings2;
this.result=result;
this.depth=depth;
this.exception=exception;
this.log=log;
this.compilerState=comp;
}
@SuppressWarnings("unchecked")
private static Context create(ChainState cs, long juice,long juiceLimit, AVector localBindings, ACell result, int depth,AVector> log, CompilerState comp) {
if (juice<0) throw new IllegalArgumentException("Negative juice! "+juice);
Context ctx= new Context(cs,juice,juiceLimit,localBindings,(T)result,depth,DEFAULT_EXCEPTION,log, comp);
return ctx;
}
private static Context create(State state, long juice,long juiceLimit,AVector localBindings, T result, int depth, Address origin,Address caller, Address address, long offer, AVector> log, CompilerState comp) {
ChainState chainState=ChainState.create(state,origin,caller,address,offer,NULL_SCOPE);
if (chainState==null) throw new Error("Attempting to create context with invalid Address");
return create(chainState,juice,juiceLimit,localBindings,result,depth,log,comp);
}
/**
* Creates an CVM execution context
*
* Useful for Testing or local execution
*
* @param state State to use for this Context
* @return Fake context
*/
public static Context create(State state) {
return create(state,Address.ZERO);
}
/**
* Creates a execution context for the given address.
*
* Not valid for use in real transactions, but can be used to
* compute stuff off-chain "as-if" the actor made the call.
*
* @param state State to use for this Context
* @param origin Origin address to use
* @return Fake context
*/
public static Context create(State state, Address origin) {
if (origin==null) throw new IllegalArgumentException("Null address!");
return create(state,0,Constants.MAX_TRANSACTION_JUICE,EMPTY_BINDINGS,NO_RESULT,ZERO_DEPTH,origin,null,origin, ZERO_OFFER, DEFAULT_LOG,null);
}
/**
* Creates an execution context with the specified actor as origin, and reserving the appropriate
* amount of juice.
*
* Juice reserve is extracted from the actor's current balance.
*
* @param state Initial State for Context
* @param origin Origin Address for Context
* @param juiceLimit Initial juice requested for Context
* @return Initial execution context with reserved juice.
*/
public static Context create(State state, Address origin,long juiceLimit) {
AccountStatus as=state.getAccount(origin);
if (as==null) {
// no account
return Context.create(state).withError(ErrorCodes.NOBODY);
}
return create(state,0,juiceLimit,EMPTY_BINDINGS,NO_RESULT,ZERO_DEPTH,origin,null,origin,INITIAL_JUICE,DEFAULT_LOG,null);
}
/**
* Performs key actions at the end of a transaction:
*
* - Refunds juice
* - Accumulates used juice fees in globals
* - Increments sequence number
*
*
* @param initialState State before transaction execution (after prepare)
* @param rc Juice price of current execution
* @return Updated context
*/
public Context completeTransaction(State initialState, ResultContext rc) {
// get state at end of transaction application
State state=getState();
long executionJuice=this.juice;
rc.juiceUsed=executionJuice;
// Base fixed juice cost per transaction
long trxJuice=Juice.priceTransaction(rc.tx);
long totalJuice=executionJuice+trxJuice;
long juicePrice=rc.juicePrice;
long juiceFees=Juice.addMul(0,totalJuice,juicePrice);
Address address=getAddress();
AccountStatus account=state.getAccount(address);
long balance=account.getBalance();
boolean juiceFailure=juiceFees>balance;
boolean memoryFailure=false;
long memorySpend=0L; // usually zero
if (juiceFailure) {
// consume whole balance, reset state
juiceFees=balance;
state=initialState;
} else if (!rc.context.isExceptional()) {
// Transaction appears to have succeeded, and will do unless memory accounting fails
// do memory accounting as long as we didn't fail for any other reason
// compute memory delta (memUsed) and store in ResultContext
long memUsed=state.getMemorySize()-initialState.getMemorySize();
rc.memUsed=memUsed;
long allowance=account.getMemory();
if (memUsed>0) {
long allowanceUsed=Math.min(allowance, memUsed);
if (allowanceUsed>0) {
account=account.withMemory(allowance-allowanceUsed);
}
// compute additional memory purchase requirement beyond allowance
long purchaseNeeded=memUsed-allowanceUsed;
if (purchaseNeeded>0) {
long poolBalance=state.getGlobalMemoryValue().longValue();
long poolAllowance=state.getGlobalMemoryPool().longValue();
memorySpend=(long)Economics.swapPrice(purchaseNeeded, poolAllowance, poolBalance);
if ((balance-juiceFees)>=memorySpend) {
// enough to cover memory price, so automatically buy from pool
// System.out.println("Buying "+purchaseNeeded+" memory for: "+price);
state=state.updateMemoryPool(poolBalance+memorySpend, poolAllowance-purchaseNeeded);
} else {
// Insufficient memory, so need to roll back state to before transaction
// origin should still pay transaction fees, but no memory costs
memorySpend=0L;
state=initialState;
account=state.getAccount(address);
memoryFailure=true;
}
}
} else {
// credit any unused memory back to allowance (may be zero)
long allowanceCredit=-memUsed;
account=account.withMemory(allowance+allowanceCredit);
}
} else {
// Transaction failed for reason other than juice usage exceeding balance
AExceptional ex=rc.context.getExceptional();
// It's user :CODE that caused the error if catchable, otherwise :CVM source
rc.source=(ex.isCatchable())?SourceCodes.CODE:SourceCodes.CVM;
}
// Compute total fees
long fees=juiceFees+memorySpend;
rc.totalFees=fees;
// Make balance changes if needed for refund and memory purchase
// Also increment sequence number
account=account.addBalanceAndSequence(-fees);
// update Account
state=state.putAccount(address,account);
// maybe add used juice to peer fees
if (juiceFees>0L) {
long oldFees=state.getGlobalFees().longValue();
long newFees=oldFees+juiceFees;
state=state.withGlobalFees(CVMLong.create(newFees));
}
// final state update and result reporting
Context rctx=this.withState(state);
if (juiceFailure) {
rctx=rctx.withError(ErrorCodes.JUICE, "Insuffienct balance to cover juice fees of "+rc.getJuiceFees());
rc.source=SourceCodes.CVM;
} else if (memoryFailure) {
rctx=rctx.withError(ErrorCodes.MEMORY, "Unable to allocate additional memory required for transaction ("+rc.memUsed+" bytes)");
rc.source=SourceCodes.CVM;
}
return rctx;
}
public Context withState(State newState) {
return this.withChainState(chainState.withState(newState));
}
/**
* Get the latest state from this Context
* @return State instance
*/
public State getState() {
return chainState.state;
}
/**
* Get the juice used so far in this Context
* @return Juice used
*/
public long getJuiceUsed() {
return juice;
}
/**
* Get the juice available in this Context
* @return Juice available
*/
public long getJuiceAvailable() {
return juiceLimit-juice;
}
/**
* Get the juice limit in this Context
* @return Juice limit
*/
public long getJuiceLimit() {
return juiceLimit;
}
/**
* Get the current offer from this Context
* @return Offered amount in Convex copper
*/
public long getOffer() {
return chainState.getOffer();
}
/**
* Gets the current Environment
* @return Environment map
*/
public AHashMap getEnvironment() {
return chainState.getEnvironment();
}
/**
* Gets the compiler state
* @return CompilerState instance
*/
public CompilerState getCompilerState() {
return compilerState;
}
/**
* Gets the metadata for the current Account
* @return Metadata map
*/
public AHashMap> getMetadata() {
return chainState.getMetadata();
}
/**
* Consumes juice, returning an updated context if sufficient juice remains or an exceptional JUICE error.
* @param gulp Amount of juice to consume
* @return Updated context with juice consumed
*/
public Context consumeJuice(long gulp) {
if (gulp<=0) throw new Error("Juice gulp must be positive!");
if(!checkJuice(gulp)) return withJuiceError();
juice=juice+gulp;
return this;
// return new Context(chainState,newJuice,localBindings,(R) result,depth,isExceptional);
}
/**
* Checks if there is sufficient juice for a given gulp of consumption. Does not alter context in any way.
*
* @param gulp Amount of juice to be consumed.
* @return true if juice is sufficient, false otherwise.
*/
public boolean checkJuice(long gulp) {
long juiceUsed = juice + gulp;
return (juiceUsed >= 0 && juiceUsed <= juiceLimit);
}
/**
* Looks up a symbol's value in the current execution context, without any effect on the Context (no juice consumed etc.)
*
* @param symbol Symbol to look up. May be qualified
* @return Context with the result of the lookup (may be an undeclared exception)
*/
public Context lookup(Symbol symbol) {
// try lookup in dynamic environment
return lookupDynamic(symbol);
}
/**
* Looks up a value in the dynamic environment. Consumes no juice.
*
* Returns an UNDECLARED exception if the symbol cannot be resolved.
*
* @param symbol Symbol to look up
* @return Updated Context
*/
public Context lookupDynamic(Symbol symbol) {
Address address=getAddress();
return lookupDynamic(address,symbol);
}
/**
* Looks up a value in the dynamic environment. Consumes no juice.
* Returns an UNDECLARED exception if the symbol cannot be resolved.
* Returns a NOBODY exception if the specified Account does not exist
*
* @param address Address of account in which to look up value
* @param symbol Symbol to look up
* @return Updated Context
*/
public Context lookupDynamic(Address address, Symbol symbol) {
AccountStatus as=getAccountStatus(address);
if (as==null) return withError(ErrorCodes.NOBODY,"No account found for: "+address+"/"+symbol.toString());
MapEntry envEntry=lookupDynamicEntry(as,symbol);
// if not found, return UNDECLARED error
if (envEntry==null) {
return withUndeclaredError(symbol);
}
// Result is whatever is defined as the datum value in the environment entry
ACell result = envEntry.getValue();
return withResult(result);
}
/**
* Looks up Metadata for the given symbol in this context
* @param sym Symbol to look up
* @return Metadata for given symbol (may be empty) or null if undeclared
*/
public AHashMap lookupMeta(Symbol sym) {
AHashMap env=getEnvironment();
if ((env!=null)&&env.containsKey(sym)) {
return getMetadata().get(sym,Maps.empty());
}
AccountStatus as = getAliasedAccount(env);
if (as==null) return null;
env=as.getEnvironment();
if (env.containsKey(sym)) {
return as.getMetadata().get(sym,Maps.empty());
}
return null;
}
/**
* Looks up Metadata for the given symbol in this context
* @param address Address to use for lookup (may pass null for current environment)
* @param sym Symbol to look up
* @return Metadata for given symbol (may be empty) or null if undeclared
*/
public AHashMap lookupMeta(Address address,Symbol sym) {
if (address==null) return lookupMeta(sym);
AccountStatus as=getAccountStatus(address);
if (as==null) return null;
AHashMap env=as.getEnvironment();
if (env.containsKey(sym)) {
return as.getMetadata().get(sym,Maps.empty());
}
return null;
}
/**
* Looks up the address of the account that defines a given Symbol
* @param sym Symbol to look up
* @param address Address to look up in first instance (null for current address).
* @return Context with result as the address defining the given symbol (or null if undeclared)
*/
public Context lookupDefiningAddress(Address address,Symbol sym) {
Context ctx=this;
Address addr=(address==null)?getAddress():address;
while (addr!=null) {
AccountStatus as=getAccountStatus(addr);
if (as==null) return ctx.withResult(Juice.LOOKUP, null);
AHashMap env=as.getEnvironment();
MapEntry entry = env.getEntry(sym);
if (entry!=null) {
return ctx.withResult(Juice.LOOKUP, addr);
}
ctx=ctx.consumeJuice(Juice.LOOKUP);
if (ctx.isExceptional()) return ctx;
if (addr.equals(Core.CORE_ADDRESS)) break;
addr=getParentAddress(as);
}
return ctx.withResult(Juice.LOOKUP, null);
}
private Address getParentAddress(AccountStatus as) {
Address ba=as.getParent();
if (ba==null) return Core.CORE_ADDRESS;
return ba;
}
/**
* Looks up value for the given symbol in this context
* @param symName Symbol name to look up, as a Java String for convenience
* @return Value for the given symbol or null if undeclared
*/
@SuppressWarnings("unchecked")
public T lookupValue(String symName) {
return (T) lookupValue(Symbol.create(symName));
}
/**
* Looks up value for the given symbol in this context
* @param sym Symbol to look up
* @return Value for the given symbol or null if undeclared
*/
public ACell lookupValue(Symbol sym) {
AHashMap env=getEnvironment();
// Lookup in current environment first
MapEntry me=env.getEntry(sym);
if (me!=null) {
return me.getValue();
}
AccountStatus as = getAliasedAccount(env);
if (as==null) return null;
return as.getEnvironment().get(sym);
}
/**
* Looks up value for the given symbol in this context
* @param address Address to look up in (may be null for current environment)
* @param sym Symbol to look up
* @return Value for the given symbol or null if undeclared
*/
public ACell lookupValue(Address address,Symbol sym) {
if (address==null) return lookupValue(sym);
AccountStatus as=getAccountStatus(address);
if (as==null) return null;
AHashMap env=as.getEnvironment();
return env.get(sym);
}
/**
* Looks up an environment entry for a specific address without consuming juice.
*
* @param address Address of Account in which to look up entry
* @param sym Symbol to look up
* @return Environment entry
*/
public MapEntry lookupDynamicEntry(Address address,Symbol sym) {
AccountStatus as=getAccountStatus(address);
if (as==null) return null;
return lookupDynamicEntry(as,sym);
}
private MapEntry lookupDynamicEntry(AccountStatus as,Symbol sym) {
// Get environment for Address, or default to initial environment
AHashMap env = (as==null)?Core.ENVIRONMENT:as.getEnvironment();
MapEntry result=env.getEntry(sym);
if (result==null) {
AccountStatus aliasAccount=getAliasedAccount(env);
result = lookupAliasedEntry(aliasAccount,sym);
}
return result;
}
private MapEntry lookupAliasedEntry(AccountStatus as,Symbol sym) {
if (as==null) return null;
AHashMap env = as.getEnvironment();
return env.getEntry(sym);
}
/**
* Gets the account status for the current Address
*
* @return AccountStatus object, or null if not found
*/
public AccountStatus getAccountStatus() {
Address a=getAddress();
// Possible we don't have an Address (e.g. in a Query)
if (a==null) return null;
return chainState.state.getAccount(a);
}
/**
* Looks up the account for an Symbol alias in the given environment.
* @param env
* @param path An alias path
* @return AccountStatus for the alias, or null if not present
*/
private AccountStatus getAliasedAccount(AHashMap env) {
// TODO: alternative core accounts
return getCoreAccount();
}
private AccountStatus getCoreAccount() {
return getState().getAccount(Core.CORE_ADDRESS);
}
/**
* Gets the holdings map for the current account.
* @return Map of holdings, or null if the current account does not exist.
*/
public Index getHoldings() {
AccountStatus as=getAccountStatus(getAddress());
if (as==null) return null;
return as.getHoldings();
}
/**
* Gets the balance for the current Address
* @return Balance in Convex Coins
*/
public long getBalance() {
return getBalance(getAddress());
}
/**
* Gets the balance for the specified Address
* @param address Address to check balance for
* @return Balance in Convex Coins
*/
public long getBalance(Address address) {
AccountStatus as=getAccountStatus(address);
if (as==null) return 0L;
return as.getBalance();
}
/**
* Gets the caller of the currently executing context.
*
* Will be null if this context was not called from elsewhere (e.g. is an origin context)
* @return Caller of the currently executing context
*/
public Address getCaller() {
return chainState.caller;
}
/**
* Gets the scope of the currently executing context.
*
* Will be null if no scope was set
* @return Caller of the currently executing context
*/
public ACell getScope() {
return chainState.scope;
}
/**
* Gets the address of the currently executing Account. May be the current actor, or the address of the
* account that executed this transaction if no Actors have been called.
*
* @return Address of the current account, cannot be null, must be a valid existing account
*/
public Address getAddress() {
return chainState.address;
}
/**
* Gets the result from this context. Throws an Error if the context return value is exceptional.
*
* @return Result value from this Context.
*/
@SuppressWarnings("unchecked")
public R getResult() {
if (exception!=null) {
String msg = "Can't get result with exceptional value: "+exception;
if (exception instanceof ErrorValue) {
ErrorValue ev=(ErrorValue)exception;
msg=msg+"\n"+ev.getTrace();
}
throw new IllegalStateException(msg);
}
return (R) result;
}
/**
* Gets the resulting value from this context. May be either exceptional or a normal result.
* @return Either the normal result, or an AExceptional instance
*/
public Object getValue() {
if (exception!=null) return exception;
return result;
}
/**
* Gets the exceptional value from this context. Throws an Error is the context return value is normal.
* @return an AExceptional instance
*/
public AExceptional getExceptional() {
if (exception==null) throw new Error("Can't get exceptional value for context with result: "+exception);
return exception;
}
/**
* Returns a context updated with the specified result.
*
* Context may become exceptional depending on the result type.
*
* @param value Value
* @return Context updated with the specified result.
*/
public Context withResult(ACell value) {
result=value;
exception=null;
return this;
}
/**
* Updates this context with a given value, which may either be a normal result or exceptional value
* @param value Value
* @return Context updated with the specified result value.
*/
public Context withValue(Object value) {
if (value instanceof AExceptional) {
exception=(AExceptional)value;
result=null;
} else {
result = (ACell)value;
exception=null;
}
return this;
}
public Context withResult(long gulp,ACell value) {
if (!checkJuice(gulp)) return withJuiceError();
juice=juice+gulp;
return withResult(value);
}
/**
* Returns this context with a JUICE error, consuming all juice.
* @return Exceptional Context signalling JUICE error.
*/
public Context withJuiceError() {
// set juice to zero. Can't consume more that we have!
this.juice=juiceLimit;
return withException(Failure.juice());
}
public Context withException(AExceptional exception) {
//return (Context) new Context(chainState,juice,localBindings,exception,depth,true);
this.exception=exception;
this.result=null;
return this;
}
public Context withException(long gulp,AExceptional value) {
if (!checkJuice(gulp)) return withJuiceError();
juice=juice+gulp;
return withException(value);
//if ((this.result==value)&&(this.juice==newJuice)) return (Context) this;
//return (Context) new Context(chainState,newJuice,localBindings,value,depth,true);
}
/**
* Updates the environment of this execution context. This changes the environment stored in the
* state for the current Address.
*
* @param newEnvironment
* @return Updated Context with the given dynamic environment
*/
private Context withEnvironment(AHashMap newEnvironment) {
ChainState cs=chainState.withEnvironment(newEnvironment);
return withChainState(cs);
}
/**
* Updates the environment of this execution context. This changes the environment stored in the
* state for the current Address.
*
* @param newEnvironment
* @return Updated Context with the given dynamic environment
*/
private Context withEnvironment(AHashMap newEnvironment, AHashMap> newMeta) {
ChainState cs=chainState.withEnvironment(newEnvironment,newMeta);
return withChainState(cs);
}
private Context withChainState(ChainState newChainState) {
if (chainState==newChainState) return this;
long oldBalance=chainState.getOriginAccount().getBalance();
chainState=newChainState;
long newBalance=newChainState.getOriginAccount().getBalance();
if (newBalance op) {
// execute op with adjusted depth
int savedDepth=getDepth();
Context ctx =this.withDepth(savedDepth+1);
if (ctx.isExceptional()) return ctx; // depth error, won't have modified depth
Context rctx=op.execute(ctx);
// reset depth after execution.
rctx=rctx.withDepth(savedDepth);
return rctx;
}
/**
* Executes an Op at the top level in a new forked Context. Handles top level halt, recur and return.
*
* Returning an updated context containing the result or an exceptional error.
*
* @param op Op to execute
* @return Updated Context
*/
public Context run(AOp> op) {
// Security: run in fork
Context ctx=fork().exec(op);
// must handle state results like halt, rollback etc.
return handleStateResults(ctx,false);
}
/**
* Executes a form at the top level in the current account. Handles top level halt, recur and return.
*
* Returning an updated context containing the result or an exceptional error.
*
* @param code Code to execute
* @return Updated Context
*/
public Context run(ACell code) {
Context ctx=fork(); // for so we can handle rollback etc.
ctx=ctx.eval(code);
// must handle state results like halt, rollback etc.
return handleStateResults(ctx,false);
}
/**
* Invokes a function within this context, returning an updated context.
*
* Handles function recur and return values.
*
* Keeps depth constant upon return.
*
* @param fn Function to execute
* @param args Arguments for function
* @return Updated Context
*/
public Context invoke(AFn> fn, ACell... args) {
// Note: we don't adjust depth here because execute(...) does it for us in the function body
Context ctx = fn.invoke(this,args);
if (ctx.isExceptional()) {
// Need an Object because maybe mutating later
Object v=ctx.getExceptional();
// recur as many times as needed
while (v instanceof ATrampoline) {
// don't recur if this is the recur function itself
if (v instanceof RecurValue) {
if (fn==Core.RECUR) break;
RecurValue rv = (RecurValue) v;
ACell[] newArgs = rv.getValues();
ctx=ctx.withValue(null); // clear value
ctx = fn.invoke(ctx,newArgs);
v = ctx.getValue();
} else if (v instanceof TailcallValue) {
if (fn==Core.TAILCALL_STAR) break;
TailcallValue rv=(TailcallValue)v;
ACell[] newArgs = rv.getValues();
// redirect function and invoke
fn = (AFn>) rv.getFunction();
ctx=ctx.withValue(null); // clear value
ctx = fn.invoke(ctx,newArgs);
v = ctx.getValue();
}
}
// unwrap return value if necessary
if ((v instanceof ReturnValue)&&(!(fn==Core.RETURN))) {
ACell val = ((ReturnValue>) v).getValue();
// unwrap result
return ctx.withResult(val);
}
if (v instanceof AThrowable) {
if (fn instanceof CoreFn) {
AThrowable ev=(AThrowable)v;
ev.addTrace("In core function: "+RT.str(fn)); // TODO: Core.getCoreName() ?
}
}
}
return ctx;
}
/**
* Execute an op, and bind the result to the given binding form in the lexical environment
*
* Binding form may be a destructuring form
* @param bindingForm Binding form
* @param op Op to execute to get binding values
*
* @return Context with local bindings updated
*/
public Context executeLocalBinding(ACell bindingForm, AOp> op) {
Context ctx=this.execute(op);
if (ctx.isExceptional()) return ctx;
return ctx.updateBindings(bindingForm, ctx.getResult());
}
/**
* Updates local bindings with a given binding form
*
* @param bindingForm Binding form
* @param args Arguments to bind
* @return Non-exceptional Context with local bindings updated, or an exceptional result if bindings fail
*/
@SuppressWarnings("unchecked")
public Context updateBindings(ACell bindingForm, Object args) {
// Clear any exceptional status
Context ctx=this.withValue(null);
if (bindingForm instanceof Symbol) {
Symbol sym=(Symbol)bindingForm;
if (sym.equals(Symbols.UNDERSCORE)) return ctx;
// TODO: confirm must be an ACell at this point?
return withLocalBindings(localBindings.conj((ACell)args));
} else if (bindingForm instanceof AVector) {
AVector v=(AVector)bindingForm;
long vcount=v.count(); // count of binding form symbols (may include & etc.)
// Count the arguments, exit with a CAST error if args are not sequential
Long argCount=RT.argumentCount(args);
if (argCount==null) return ctx.withError(ErrorCodes.CAST, "Trying to destructure an argument that is not a sequential collection");
boolean foundAmpersand=false;
for (long i=0; i rest=RT.vec(args).slice(i,i+consumeCount); // TODO: cost of this?
ctx= ctx.updateBindings(v.get(i+1), rest);
if(ctx.isExceptional()) return ctx;
// mark ampersand as found, and skip to next binding form (i.e. past the variadic symbol following &)
foundAmpersand=true;
i++;
} else {
// just a regular binding
long argIndex=foundAmpersand?(argCount-(vcount-i)):i;
if (argIndex>=argCount) return ctx.withArityError("Insufficient arguments ("+argCount+") for binding form: "+bindingForm);
ctx=ctx.updateBindings(bf,RT.nth(args, argIndex));
if(ctx.isExceptional()) return ctx;
}
}
// at this point, should have consumed all bindings
if (!foundAmpersand) {
if (vcount!=argCount) {
return ctx.withArityError("Expected "+vcount+" arguments but got "+argCount+" for binding form: "+bindingForm);
}
}
} else {
return ctx.withCompileError("Don't understand binding form of type: "+RT.getType(bindingForm));
}
// return
return ctx;
}
public boolean print(BlobBuilder bb, long limit) {
bb.append("{");
bb.append(":juice "+juice);
bb.append(',');
bb.append(":juice-limit "+juiceLimit);
bb.append(',');
bb.append(":result ");
if (!RT.print(bb,result,limit)) return false;
bb.append(',');
bb.append(":state ");
if (!getState().print(bb,limit)) return false;
bb.append("}");
return bb.check(limit);
}
@Override
public String toString() {
BlobBuilder bb=new BlobBuilder();
long LIMIT=1000;
print(bb,LIMIT);
return bb.toBlob().toCVMString(LIMIT).toString();
}
public AVector getLocalBindings() {
return localBindings;
}
/**
* Updates this Context with new local bindings. Doesn't affect result state (exceptional or otherwise)
* @param newBindings New local bindings map to use.
* @return Updated context
*/
public Context withLocalBindings(AVector newBindings) {
localBindings=newBindings;
return this;
}
/**
* Gets the account status record, or null if not found
*
* @param address Address of account
* @return AccountStatus for the specified address, or null if the account does not exist
*/
public AccountStatus getAccountStatus(Address address) {
return getState().getAccount(address);
}
public int getDepth() {
return depth;
}
public Address getOrigin() {
return chainState.origin;
}
/**
* Defines a value in the environment of the current address
* @param key Symbol of the mapping to create
* @param value Value to define
* @return Updated context with symbol defined in environment
*/
public Context define(Symbol key, ACell value) {
AHashMap env = getEnvironment();
AHashMap newEnvironment = env.assoc(key, value);
return withEnvironment(newEnvironment);
}
/**
* Defines a value in the environment of the current address, updating the metadata
*
* @param syn Syntax Object to define, containing a Symbol value
* @param value Value to set of the given Symbol
* @return Updated context with symbol defined in environment
*/
public Context defineWithSyntax(Syntax syn, ACell value) {
Symbol key=syn.getValue();
AHashMap env = getEnvironment();
AHashMap newEnvironment = env.assoc(key, value);
AHashMap> newMeta = getMetadata().assoc(key, syn.getMeta());
return withEnvironment(newEnvironment,newMeta);
}
/**
* Removes a definition mapping in the environment of the current address
* @param key Symbol of the environment mapping to remove
* @return Updated context with symbol definition removed from the environment, or this context if unchanged
*/
public Context undefine(Symbol key) {
AHashMap m = getEnvironment();
AHashMap newEnvironment = m.dissoc(key);
AHashMap> newMeta = getMetadata().dissoc(key);
return withEnvironment(newEnvironment,newMeta);
}
/**
* Expand and compile a form in this Context.
*
* @param form Form to expand and compile
* @return Updated Context with compiled Op as result
*/
public Context expandCompile(ACell form) {
// run compiler with adjusted depth
int saveDepth=getDepth();
Context rctx =this.withDepth(saveDepth+1);
if (rctx.isExceptional()) return rctx; // depth error, won't have modified depth
// EXPAND AND COMPILE
rctx = Compiler.expandCompile(form, rctx);
// reset depth after expansion and compilation, unless there is an error
rctx=rctx.withDepth(saveDepth);
return rctx;
}
/**
* Compile a form in this Context. Form must already be fully expanded to a Syntax Object
*
* @param expandedForm Form to compile
* @return Updated Context with compiled Op as result
*/
public Context compile(ACell expandedForm) {
// Save an adjust depth
int saveDepth=getDepth();
Context rctx =this.withDepth(saveDepth+1);
if (rctx.isExceptional()) return rctx; // depth error
// Save Compiler state
CompilerState savedCompilerState=getCompilerState();
// COMPILE
rctx = Compiler.compile(expandedForm, rctx);
if (rctx.isExceptional()) {
AExceptional ex=rctx.getExceptional();
if (ex instanceof ErrorValue) {
ErrorValue ev=(ErrorValue)ex;
// TODO: SECURITY: DoS limits
//String msg = "Compiling: Syntax Object with datum of type "+Utils.getClassName(expandedForm);
String msg = "Compiling:"+ expandedForm;
//String msg = "Compiling: "+expandedForm;
ev.addTrace(msg);
}
}
// restore depth and return
rctx=rctx.withDepth(saveDepth);
rctx=rctx.withCompilerState(savedCompilerState);
return rctx;
}
/**
* Executes a form in the current context.
*
* Ops are executed directly.
* Other forms will be expanded and compiled before execution, according to `compile` as defined in the current environment
*
* @param form Form to evaluate
* @return Context containing the result of evaluating the specified form
*/
public Context eval(ACell form) {
Context ctx= this;
AOp> op;
if (form instanceof AOp) {
op=(AOp>)form;
} else {
ctx=ctx.lookup(Symbols.STAR_LANG);
if (ctx.isExceptional()) return ctx;
AFn> cfn=RT.ensureFunction(ctx.getResult());
if (cfn==null) cfn=Core.COMPILE;
ctx=ctx.invoke(cfn, form);
if (ctx.isExceptional()) return ctx;
ACell cop=ctx.getResult();
if (!(cop instanceof AOp)) return ctx.withCompileError("*lang* did not produce CVM op");
op = (AOp>)cop;
ctx=ctx.withResult(null); // clear result before execution
}
return ctx.exec(op);
}
/**
* Executes an op as a top level instruction (no local bindings)
* @param Type of Op result
* @param op Op to execute
* @return Updated Context
*/
public Context exec(AOp op) {
AVector savedBindings = getLocalBindings();
Context ctx=withLocalBindings(Vectors.empty());
ctx= ctx.execute(op);
return ctx.withLocalBindings(savedBindings);
}
/**
* Evaluates a form as another Address.
*
* Causes TRUST error if the Address is not controlled by the current address.
* @param target Address of Account in which to evaluate
* @param form Form to evaluate
* @return Updated Context
*/
public Context evalAs(Address target, ACell form) {
Address caller=getAddress();
AccountStatus as=this.getAccountStatus(target);
if (as==null) return withError(Errors.nobody(target));
// TODO should probably refactor into a checkControl function or similar
ACell controller=as.getController();
boolean canControl=false;
Context ctx=this;
if (caller.equals(target)) {
// can always control own address
canControl=true;
} else if (controller==null) {
return withError(ErrorCodes.TRUST,"Cannot control address with nil controller set: "+target);
} else if (caller.equals(controller)) {
// if we are the precisely specified controller, can control
canControl=true;
} else {
// need to check trust monitor
Address actorAddress=RT.callableAddress(controller);
if (actorAddress==null) return ctx.withError(ErrorCodes.TRUST,"Cannot control address because controller is not a valid address or scoped actor");
AccountStatus actorAccount=this.getAccountStatus(actorAddress);
if (actorAccount==null) return ctx.withError(ErrorCodes.TRUST,"Cannot control address because controller does not exist: "+controller);
// (call target amount (receive-coin source amount nil))
ctx=ctx.actorCall(controller,ZERO_OFFER,Symbols.CHECK_TRUSTED_Q,caller,Keywords.CONTROL,target);
if (ctx.isExceptional()) {
return ctx.withError(ErrorCodes.TRUST,"Failure trying to obtain :control rights");
}
canControl=RT.bool(ctx.getResult());
}
if (!canControl) return ctx.withError(ErrorCodes.TRUST,"Cannot control address: "+target);
// SECURITY: eval with a context switch
final Context exContext=Context.create(ctx.getState(), ctx.juice,juiceLimit, EMPTY_BINDINGS, NO_RESULT, depth+1, getOrigin(),caller, target,ZERO_OFFER,ctx.log,NO_COMPILER_STATE);
final Context rContext=exContext.eval(form);
// SECURITY: must handle results as if returning from an actor call
return handleStateResults(rContext,false);
}
/**
* Executes code as if run in the specified account, but always rolling back state changes.
* @param address Address of Account in which to execute the query
* @param form Code to execute.
* @return Context updated with only query result and juice consumed
*/
public Context queryAs(Address address, ACell form) {
// chainstate with the target address as origin.
State s=getState();
ChainState cs=ChainState.create(s,getOrigin(),getAddress(),address,ZERO_OFFER,NULL_SCOPE);
if (cs==null) return withError(ErrorCodes.NOBODY,"Address does not exist: "+address);
Context ctx=Context.create(cs, juice,juiceLimit, EMPTY_BINDINGS, NO_RESULT, depth,log,NO_COMPILER_STATE);
ctx=ctx.eval(form);
return handleStateResults(ctx,true);
}
/**
* Compiles a sequence of forms in the current context.
* Returns a vector of ops in the updated Context.
*
* Maintains depth.
*
* @param forms A sequence of forms to compile
* @return Updated context with vector of compiled forms
*/
public Context compileAll(ASequence forms) {
Context rctx = Compiler.compileAll(forms, this);
return rctx;
}
// public Context adjustDepth(int delta) {
// int newDepth=Math.addExact(depth,delta);
// return withDepth(newDepth);
// }
/**
* Changes the depth of this context. Returns exceptional result if depth limit exceeded.
* @param newDepth New depth value
* @return Updated context with new depth set
*/
Context withDepth(int newDepth) {
if (newDepth==depth) return this;
if ((newDepth<0)||(newDepth>Constants.MAX_DEPTH)) return withError(ErrorCodes.DEPTH,"Invalid depth: "+newDepth);
depth=newDepth;
return this;
}
public Context withJuice(long newJuice) {
juice=newJuice;
return this;
}
public Context withJuiceLimit(long newJuiceLimit) {
juiceLimit = newJuiceLimit;
return this;
}
public Context withCompilerState(CompilerState comp) {
compilerState=comp;
return this;
}
/**
* Tests if this Context holds an exceptional result.
*
* Ops should cancel and return exceptional results unchanged, unless they can handle them.
* @return true if context has an exceptional value, false otherwise
*/
public boolean isExceptional() {
return exception!=null;
}
/**
* Tests if this Context's current status contains an Error. Errors are an uncatchable subset of Exceptions.
*
* @return true if context has an Error value, false otherwise
*/
public boolean isError() {
return (exception!=null)&&(exception instanceof ErrorValue);
}
/**
* Tests if an Address is valid, i.e. refers to an existing Account
*
* @param address Address to check. May be null
* @return true if Account exists, false otherwise
*/
public boolean isValidAccount(Address address) {
if (address==null) return false;
return getAccountStatus(address)!=null;
}
/**
* Transfers funds from the current address to the target.
*
* Uses no juice
*
* @param target Target Address, will be created if does not already exist.
* @param amount Amount to transfer, must be between 0 and Amount.MAX_VALUE inclusive
* @return Context with sent amount if the transaction succeeds, or an exceptional value if the transfer fails
*/
public Context transfer(Address target, long amount) {
if (amount<0) return withError(ErrorCodes.ARGUMENT,"Can't transfer a negative amount");
if (amount>Constants.MAX_SUPPLY) return withError(ErrorCodes.ARGUMENT,"Can't transfer an amount beyond maximum limit");
AVector accounts=getState().getAccounts();
Address source=getAddress();
long sourceIndex=source.longValue();
AccountStatus sourceAccount=accounts.get(sourceIndex);
long currentBalance=sourceAccount.getBalance();
if (currentBalance=accounts.count()) {
return this.withError(ErrorCodes.NOBODY,"Target account for transfer "+target+" does not exist");
}
AccountStatus targetAccount=accounts.get(targetIndex);
if (targetAccount.isActor()) {
// (call target amount (receive-coin source amount nil))
// SECURITY: actorCall must do fork to preserve this
Context actx=this.fork();
actx=actorCall(target,amount,Symbols.RECEIVE_COIN,source,CVMLong.create(amount),null);
if (actx.isExceptional()) return actx;
long sent=currentBalance-actx.getBalance(source);
return actx.withResult(CVMLong.create(sent));
} else {
// must be a user account
long oldTargetBalance=targetAccount.getBalance();
long newTargetBalance=oldTargetBalance+amount;
AccountStatus newTargetAccount=targetAccount.withBalance(newTargetBalance);
accounts=accounts.assoc(targetIndex, newTargetAccount);
// SECURITY: new context with updated accounts
Context result=withChainState(chainState.withAccounts(accounts)).withResult(CVMLong.create(amount));
return result;
}
}
/**
* Transfers memory allowance from the current address to the target.
*
* Uses no juice
*
* @param target Target Address, must already exist
* @param amountToSend Amount of memory to transfer, must be between 0 and Amount.MAX_VALUE inclusive
* @return Context with a null result if the transaction succeeds, or an exceptional value if the transfer fails
*/
public Context transferMemoryAllowance(Address target, CVMLong amountToSend) {
long amount=amountToSend.longValue();
if (amount<0) return withError(ErrorCodes.ARGUMENT,"Can't transfer a negative allowance amount");
if (amount>Constants.MAX_SUPPLY) return withError(ErrorCodes.ARGUMENT,"Can't transfer an allowance amount beyond maximum limit");
AVector accounts=getState().getAccounts();
Address source=getAddress();
long sourceIndex=source.longValue();
AccountStatus sourceAccount=accounts.get(sourceIndex);
long currentBalance=sourceAccount.getMemory();
if (currentBalance=accounts.count()) {
return withError(ErrorCodes.NOBODY,"Cannot transfer memory allowance to non-existent account: "+target);
}
AccountStatus targetAccount=accounts.get(targetIndex);
long newTargetBalance=targetAccount.getMemory()+amount;
AccountStatus newTargetAccount=targetAccount.withMemory(newTargetBalance);
accounts=accounts.assoc(targetIndex, newTargetAccount);
// SECURITY: new context with updated accounts
Context result=withChainState(chainState.withAccounts(accounts)).withResult(amountToSend);
return result;
}
/**
* Sets the memory allowance for the current account, buying / selling from the pool as necessary to
* ensure the correct final allowance
* @param allowance New memory allowance
* @return Context indicating the price paid for the allowance change (may be zero or negative for refund)
*/
public Context setMemory(long allowance) {
State state=getState();
AVector accounts=state.getAccounts();
if (allowance<0) return withError(ErrorCodes.ARGUMENT,"Can't transfer a negative allowance amount");
Address source=getAddress();
long sourceIndex=source.longValue();
AccountStatus sourceAccount=accounts.get(sourceIndex);
long current=sourceAccount.getMemory();
long balance=sourceAccount.getBalance();
long delta=allowance-current;
if (delta==0L) return this.withResult(CVMLong.ZERO);
try {
long poolAllowance=state.getGlobalMemoryPool().longValue();
long poolBalance=state.getGlobalMemoryValue().longValue();
long price = (long)Economics.swapPrice(delta, poolAllowance,poolBalance);
if (price>balance) {
return withError(ErrorCodes.FUNDS,"Cannot afford allowance, would cost: "+price);
}
sourceAccount=sourceAccount.withBalances(balance-price, allowance);
state=state.updateMemoryPool(poolBalance+price, poolAllowance-delta);
// Update accounts
AVector newAccounts=accounts.assoc(sourceIndex, sourceAccount);
state=state.withAccounts(newAccounts);
return withState(state).withResult(Juice.MEMORY_TRADE,CVMLong.create(price));
} catch (IllegalArgumentException e) {
return withError(ErrorCodes.FUNDS,"Cannot trade allowance: "+e.getMessage());
}
}
/**
* Accepts offered funds for the given address.
*
* STATE error if offered amount is insufficient. ARGUMENT error if acceptance is negative.
*
* @param amount Amount to accept
* @return Updated context, with long amount accepted as result
*/
public Context acceptFunds(long amount) {
if (amount<0L) return this.withError(ErrorCodes.ARGUMENT,"Negative accept argument");
if (amount==0L) return this.withResult(Juice.ACCEPT, CVMLong.ZERO);
long offer=getOffer();
if (amount>offer) return this.withError(ErrorCodes.STATE,"Insufficient offered funds");
State state=getState();
Address addr=getAddress();
long balance=state.getBalance(addr);
state=state.withBalance(addr,balance+amount);
// need to update both state and offer
ChainState cs=chainState.withStateOffer(state,offer-amount);
Context ctx=this.withChainState(cs);
return ctx.withResult(Juice.ACCEPT, CVMLong.create(amount));
}
/**
* Executes a call to an Actor. Utility function which convert a java String function name
*
* @param target Target Actor address or scope vector
* @param offer Amount of Convex Coins to offer in Actor call
* @param functionName Symbol of function name defined by Actor
* @param args Arguments to Actor function invocation
* @return Context with result of Actor call (may be exceptional)
*/
public Context actorCall(ACell target, long offer, String functionName, ACell... args) {
return actorCall(target,offer,Symbol.create(functionName),args);
}
/**
* Executes a call to an Actor.
*
* @param target Target Actor address
* @param offer Amount of Convex Coins to offer in Actor call
* @param functionName Symbol of function name defined by Actor
* @param args Arguments to Actor function invocation
* @return Context with result of Actor call (may be exceptional)
*/
@SuppressWarnings("unchecked")
public Context actorCall(ACell target, long offer, ACell functionName, ACell... args) {
// SECURITY: set up state for actor call
State state=getState();
Symbol sym=RT.ensureSymbol(functionName);
Address targetAddress;
ACell scope=null;
// Handle target possibilities, may be an Address or Scoped Actor reference
if (target instanceof Address) {
targetAddress=(Address)target;
} else {
if (!(target instanceof AVector)) {
return this.withCastError(target, "call target must be an Address or [Address *scope*] vector");
}
AVector v=(AVector)target;
if (!(v.count()==2)) {
return this.withCastError(target, "call target vector must have length 2");
}
targetAddress=RT.ensureAddress(v.get(0));
if (targetAddress==null) {
return this.withCastError(target, "call target vector must start with an Address");
}
scope=v.get(1);
}
AccountStatus as=state.getAccount(targetAddress);
if (as==null) return this.withError(ErrorCodes.NOBODY,"Call target Account does not exist: "+target);
// Handling for non-zero offers.
// SECURITY: Subtract offer from balance first so we don't have double-spend issues!
if (offer>0L) {
Address senderAddress=getAddress();
AccountStatus cas=state.getAccount(senderAddress);
long balance=cas.getBalance();
if (balance fn = as.getCallableFunction(sym);
if (fn == null) {
if (!as.getEnvironment().containsKey(sym)) {
return this.withError(ErrorCodes.STATE, "Account " + targetAddress + " does not define Symbol: " + sym);
}
return this.withError(ErrorCodes.STATE, "Value defined in account " + targetAddress + " is not a callable function: " + sym);
}
// Ensure we create a forked Context for the Actor call
final Context exContext=forkActorCall(state, targetAddress, offer, scope);
// INVOKE ACTOR FUNCTION
final Context rctx=exContext.invoke(fn,args);
ErrorValue ev=rctx.getError();
if (ev!=null) {
ev.addTrace("Calling Actor "+target+" with function ("+sym+" ...)");
}
// SECURITY: must handle state transitions in results correctly
// calling handleStateReturns on 'this' to ensure original values are restored
return handleStateResults(rctx,false);
}
/**
* Create new forked Context for execution of Actor call.
* SECURITY: Increments depth, will be restored in handleStateResults
* SECURITY: Must change address to the target Actor address.
* SECURITY: Must change caller to current address.
* @param state for forked context.
* @param target Target actor call address, will become new *address* for context
* @param offer Offer amount for actor call. Must have been pre-subtracted from caller account.
* @return
*/
private Context forkActorCall(State state, Address target, long offer, ACell scope) {
Context fctx=Context.create(state, juice, juiceLimit,EMPTY_BINDINGS, NO_RESULT, depth+1, getOrigin(),getAddress(), target,offer, log,NO_COMPILER_STATE);
if (scope!=null) {
fctx.chainState=fctx.chainState.withScope(scope);
}
return fctx;
}
/**
* Handle results at the end of an execution boundary (actor call, transaction etc.)
* @param returnContext Context containing return from child transaction / call
* @param rollback If true, always rolls back state effects
* @return Updated parent context
*/
public
Context handleStateResults(Context returnContext, boolean rollback) {
/** Return value */
Object rv;
if (returnContext.isExceptional()) {
// SECURITY: need to handle exceptional states correctly
AExceptional ex=returnContext.getExceptional();
if (ex instanceof RollbackValue) {
// roll back state to before Actor call
// Note: this will also refund unused offer.
rollback=true;
rv=((RollbackValue>)ex).getValue();
} else if (ex instanceof HaltValue) {
rv=((HaltValue>)ex).getValue();
} else if (ex instanceof ErrorValue) {
// OK to pass through error, but need to roll back state changes
rollback=true;
rv=ex;
} else if (ex instanceof ReturnValue) {
// Normally doesn't happen (invoke catches this)
// but might in a user transaction. Treat as a Halt.
rv=((ReturnValue>)ex).getValue();
} else {
rollback=true;
if (ex instanceof Failure) {
rv=ex;
} else {
String msg;
if (ex instanceof ATrampoline) {
msg="attempt to recur or tail call outside of a function body";
} if (ex instanceof ReducedValue) {
msg="reduced used outside of a reduce operation";
} else {
msg="Unhandled Exception with Code:"+ex.getCode();
}
rv=ErrorValue.create(ErrorCodes.EXCEPTION, msg);
}
}
} else {
rv=returnContext.getResult();
}
final Address address=getAddress(); // address we are returning to
State returnState;
if (rollback) {
returnState=this.getState();
} else {
// take state from the returning context
returnState=returnContext.getState();
// Take log from returning context
log=returnContext.getLog();
// Refund offer
// Not necessary if rolling back to initial context before offer was subtracted
long refund=returnContext.getOffer();
if (refund>0) {
// we need to refund caller
AccountStatus cas=returnState.getAccount(address);
long balance=cas.getBalance();
cas=cas.withBalance(balance+refund);
returnState=returnState.putAccount(address, cas);
}
}
// Rebuild context for the current execution
// SECURITY: must restore origin,depth,caller,address,local bindings, offer
Context result=this.withState(returnState);
result.juice=returnContext.juice;
result=this.withValue(rv);
return result;
}
/**
* Deploys a new account.
*
* Arguments must be an account setup code, which will be evaluated in the new account.
*
* Result will contain the new address if successful, an exception otherwise.
*
* @param code Account initialisation code
* @return Updated Context with account deployed, or an exceptional result
*/
public Context deploy(ACell... code) {
int n=code.length;
final State initialState=getState();
// deploy initial contract state to next address
Address address=initialState.nextAddress();
State stateSetup=initialState.addActor();
// Deployment execution context with forked context and incremented depth
Context ctx=Context.create(stateSetup, juice, juiceLimit,EMPTY_BINDINGS, NO_RESULT, depth+1, getOrigin(),getAddress(), address,ZERO_OFFER,log,NO_COMPILER_STATE);
for (int i=0; i op) {
// check vs current timestamp
long timestamp=getTimeStamp().longValue();
if (timestamp<0L) return withError(ErrorCodes.STATE);
if (timeConstants.MAX_SUPPLY) return this.withArgumentError("Target stake out of valid Amount range");
Address myAddress=getAddress();
long balance=getBalance(myAddress);
long currentStake=ps.getDelegatedStake(myAddress);
long delta=newStake-currentStake;
if (delta==0) return this; // no change
// need to check sufficient balance if increasing stake
if (delta>balance) return this.withFundsError("Insufficient balance ("+balance+") to increase Delegated Stake to "+newStake);
// Final updates. Hopefully everything balances. SECURITY: test this. A lot.
PeerStatus updatedPeer=ps.withDelegatedStake(myAddress, newStake);
s=s.withBalance(myAddress, balance-delta); // adjust own balance
s=s.withPeer(peerKey, updatedPeer); // adjust peer
return withState(s).withResult(CVMLong.create(delta));
}
/**
* Sets the stake for a given Peer, transferring coins from the current address.
* @param peerKey Peer Account Key for which to update Stake
* @param newStake New stake for Peer
* @return Updated Context
*/
public Context setPeerStake(AccountKey peerKey, long newStake) {
State s=getState();
PeerStatus ps=s.getPeer(peerKey);
if (ps==null) return withError(ErrorCodes.STATE,"Peer does not exist for account key: "+peerKey);
if (newStake<0) return this.withArgumentError("Cannot set a negative stake");
if (newStake>Constants.MAX_SUPPLY) return this.withArgumentError("Target stake out of valid Amount range");
Address myAddress=getAddress();
if (!ps.getController().equals(myAddress)) return withError(ErrorCodes.STATE,"Current address "+myAddress+" is not the controller of this peer account");
long balance=getBalance(myAddress);
long currentStake=ps.getPeerStake();
long delta=newStake-currentStake;
if (delta==0) return this; // no change
// need to check sufficient balance if increasing stake
if (delta>balance) return this.withFundsError("Insufficient balance ("+balance+") to increase Peer Stake to "+newStake);
// Final updates assuming everything OK. Hopefully everything balances. SECURITY: test this. A lot.
PeerStatus updatedPeer=ps.withPeerStake(newStake);
s=s.withBalance(myAddress, balance-delta); // adjust own balance
s=s.withPeer(peerKey, updatedPeer); // adjust peer
return withState(s).withResult(CVMLong.create(delta));
}
/**
* Creates a new peer with the specified stake.
* The accountKey must not be in the list of peers.
* The accountKey must be assigend to the current transaction address
* Stake must be greater than 0.
* Stake must be less than to the account balance.
*
* @param accountKey Peer Account key to create the PeerStatus
* @param initialStake Initial stake amount
* @return Context with final take set
*/
public Context createPeer(AccountKey accountKey, long initialStake) {
State s=getState();
PeerStatus ps=s.getPeer(accountKey);
if (ps!=null) return withError(ErrorCodes.STATE,"Peer already exists for this account key: "+accountKey.toChecksumHex());
if (initialStake == 0) return this.withArgumentError("Cannot create a peer with zero stake");
if (!Coin.isValidAmount(initialStake)) return this.withArgumentError("Target stake out of valid Amount range: "+initialStake);
Address myAddress=getAddress();
// TODO: SECURITY fix
// AccountStatus as=getAccountStatus(myAddress);
// if (!as.getAccountKey().equals(accountKey)) return this.withArgumentError("Cannot create a peer with a different account-key");
long balance=getBalance(myAddress);
if (initialStake>balance) return this.withFundsError("Insufficient balance ("+balance+") to assign an initial stake of "+initialStake);
PeerStatus newPeerStatus = PeerStatus.create(myAddress, initialStake);
// Final updates. Hopefully everything balances. SECURITY: test this. A lot.
s=s.withBalance(myAddress, balance-initialStake); // adjust own balance
s=s.withPeer(accountKey, newPeerStatus); // add peer
return withState(s);
}
/**
* Sets peer data.
*
* @param peerKey Peer to set data for
* @param data Map of data to set for the peer
* @return Context with final peer data set
*/
public Context setPeerData(AccountKey peerKey, AHashMap data) {
State s=getState();
// get the callers account and account status
Address address = getAddress();
AccountStatus as = getAccountStatus(address);
AccountKey ak = as.getAccountKey();
if (ak == null) return withError(ErrorCodes.STATE,"The account signing this transaction must have a public key");
PeerStatus ps=s.getPeer(ak);
if (ps==null) return withError(ErrorCodes.STATE,"Peer does not exist for this account and account key: "+ak.toChecksumHex());
if (!ps.getController().equals(address)) return withError(ErrorCodes.STATE,"Current address "+address+" is not the controller of this peer account");
Hash lastStateHash = s.getHash();
// TODO: should use complete Map
// at the moment only :url is used in the data map
AHashMap newMeta=data;
PeerStatus updatedPeer=ps.withPeerData(newMeta);
s=s.withPeer(ak, updatedPeer); // adjust peer
// if no change just return the current context
if (lastStateHash.equals(s.getHash())){
return this;
}
return withState(s);
}
/**
* Sets the holding for a specified target account. Returns NOBODY exception if account does not exist.
* @param targetAddress Account address at which to set the holding
* @param value Value to set for the holding.
* @return Updated context
*/
public Context setHolding(Address targetAddress, ACell value) {
AccountStatus as=getAccountStatus(targetAddress);
if (as==null) return withError(ErrorCodes.NOBODY,"Can't set set holding for non-existent account "+targetAddress);
as=as.withHolding(getAddress(), value);
return withAccountStatus(targetAddress,as);
}
/**
* Sets the controller for the current Account
* @param address New controller Address / scoped reference
* @return Context with current Account controller set
*/
public Context setController(ACell address) {
AccountStatus as=getAccountStatus();
as=as.withController(address);
return withAccountStatus(getAddress(),as);
}
/**
* Sets the parent for the current Account
* @param address New parent Address
* @return Context with current Account parent set
*/
public Context setParent(Address address) {
AccountStatus as=getAccountStatus();
as=as.withParent(address);
return withAccountStatus(getAddress(),as);
}
/**
* Sets the public key for the current account
* @param publicKey New Account Public Key
* @return Context with current Account Key set
*/
public Context setAccountKey(AccountKey publicKey) {
AccountStatus as=getAccountStatus();
as=as.withAccountKey(publicKey);
return withAccountStatus(getAddress(),as);
}
protected Context withAccountStatus(Address target, AccountStatus accountStatus) {
return withState(getState().putAccount(target, accountStatus));
}
/**
* Switches the context to a new address, creating a new execution context. Suitable for testing.
* @param newAddress New Address to use.
* @return Result type of new Context
*/
public Context forkWithAddress(Address newAddress) {
return create(getState(),newAddress);
}
/**
* Forks this context, creating a new copy of all local state but clears any exceptional value
* @return A new forked Context
*/
public Context fork() {
return new Context(chainState, juice, juiceLimit, localBindings,result, depth,NO_EXCEPTION,log, compilerState);
}
/**
* Appends a log entry for the current address.
* @param values Values to log
* @return Updated Context
*/
public Context appendLog(AVector values) {
Address addr=getAddress();
ACell scope=getScope();
AVector> log=this.log;
if (log==null) {
log=Vectors.empty();
}
AVector entry = Vectors.of(addr,scope,null,values);
log=log.conj(entry);
this.log=log;
return this;
}
/**
* Gets the log map for the current context.
*
* @return Index of addresses to log entries created in the course of current execution context.
*/
public AVector> getLog() {
if (log==null) return Vectors.empty();
return log;
}
public Context lookupCNS(String name) {
Context ctx=this.fork();
ctx=this.actorCall(Init.REGISTRY_ADDRESS, 0, Symbols.CNS_RESOLVE, Symbol.create(name));
return ctx;
}
/**
* Expands a form with the default *initial-expander*
* @param form Form to expand
* @return Syntax Object resulting from expansion.
*/
public Context expand(ACell form) {
return expand(Core.INITIAL_EXPANDER, form, Core.INITIAL_EXPANDER);
}
public Context expand(AFn> expander, ACell form, AFn> cont) {
// execute with adjusted depth
int savedDepth=getDepth();
Context ctx = this.withDepth(savedDepth+1);
if (ctx.isExceptional()) return ctx; // depth error, won't have modified depth
//AVector savedEnv=getLocalBindings();
Context rctx= invoke(expander, form, cont);
// reset depth after execution.
//rctx=rctx.withLocalBindings(savedEnv);
rctx=rctx.withDepth(savedDepth);
return rctx;
}
/**
* Looks up an expander from a form in this context
* @param form Form which might be an expander reference (either a symbol or (lookup...) form)
* @return Expander instance, or null if no expander found
*/
public AFn lookupExpander(ACell form) {
/**
* MapEntry for Expander metadata lookup
*/
AHashMap me = null;
Address addr;
Symbol sym;
if (form instanceof Symbol) {
sym = (Symbol)form;
me = this.lookupMeta(sym);
addr = null;
} else if (form instanceof AList) {
// Need to check for (lookup ....) as this could reference an expander
@SuppressWarnings("unchecked")
AList listForm = (AList)form;
int n = listForm.size();
if (n <= 1) return null;
if (!Symbols.LOOKUP.equals(listForm.get(0))) return null;
ACell maybeSym = listForm.get(n-1);
if (!(maybeSym instanceof Symbol)) return null;
sym = (Symbol)maybeSym;
if (n == 2) {
addr = null;
me = lookupMeta(sym);
} else if (n == 3) {
ACell maybeAddress = listForm.get(1);
if (maybeAddress instanceof Symbol) {
// one lookup via Environment for alias
maybeAddress = lookupValue((Symbol)maybeAddress);
}
if (!(maybeAddress instanceof Address)) return null;
addr = (Address)maybeAddress;
me = lookupMeta((Address)maybeAddress,sym);
} else {
return null;
}
} else {
return null;
}
// If no metadata found, definitely not an expander
if (me == null) return null;
// TODO: examine syntax object for expander details?
ACell expBool = me.get(Keywords.EXPANDER_META);
if (RT.bool(expBool)) {
// expand form using specified expander and continuation expander
ACell v = lookupValue(addr,sym);
AFn expander = RT.castFunction(v);
if (expander != null) return expander;
}
return null;
}
}