Skip to content

Commit 30e92a7

Browse files
committed
Add evict-peer functionality and tests
1 parent 019a359 commit 30e92a7

File tree

5 files changed

+108
-7
lines changed

5 files changed

+108
-7
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1313
- `dissoc-in`, `update` and `update-in` core functions
1414
- `switch` conditional macro
1515
- Tagged values in Reader (e.g. `#Index {}`)
16+
- `evict-peer` core function to remove old / understaked peers
1617

1718
### Changed
1819

convex-core/src/main/java/convex/core/data/PeerStatus.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,10 @@ public long getBalance() {
8585
public long getDelegatedStake() {
8686
return delegatedStake;
8787
}
88+
89+
public Index<Address,CVMLong> getStakes() {
90+
return stakes;
91+
}
8892

8993
/**
9094
* Gets the self-owned stake of this peer

convex-core/src/main/java/convex/core/lang/Context.java

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
import convex.core.lang.impl.CoreFn;
4545
import convex.core.util.Economics;
4646
import convex.core.util.Errors;
47+
import convex.core.util.Utils;
4748

4849
/**
4950
* Representation of CVM execution context.
@@ -346,7 +347,6 @@ public Context completeTransaction(State initialState, ResultContext rc) {
346347
long balance=account.getBalance();
347348
boolean juiceFailure=juiceFees>balance;
348349

349-
350350
boolean memoryFailure=false;
351351
long memorySpend=0L; // usually zero
352352

@@ -2024,7 +2024,7 @@ public Context setPeerStake(AccountKey peerKey, long newStake) {
20242024
/**
20252025
* Creates a new peer with the specified stake.
20262026
* The accountKey must not be in the list of peers.
2027-
* The accountKey must be assigend to the current transaction address
2027+
* The accountKey must be assigned to the current transaction address
20282028
* Stake must be greater than 0.
20292029
* Stake must be less than to the account balance.
20302030
*
@@ -2053,12 +2053,54 @@ public Context createPeer(AccountKey accountKey, long initialStake) {
20532053
}
20542054

20552055
public Context evictPeer(AccountKey peerKey) {
2056-
State s=getState();
2057-
PeerStatus ps=s.getPeer(peerKey);
2056+
Context ctx=this;
2057+
PeerStatus ps=ctx.getState().getPeer(peerKey);
20582058
if (ps==null) {
20592059
return this;
20602060
}
2061-
return withError(ErrorCodes.TODO,"Can't evict peers yet!");
2061+
Address controller=ps.getController();
2062+
if (Utils.equals(ctx.getAddress(),ps.getController())) {
2063+
// OK
2064+
} else {
2065+
if (ps.getPeerStake()>=Constants.MINIMUM_EFFECTIVE_STAKE) {
2066+
return ctx.withError(ErrorCodes.STATE,"Peer has too much stake to be evicted");
2067+
}
2068+
}
2069+
Index<Address, CVMLong> stakes = ps.getStakes();
2070+
long ns=stakes.count();
2071+
for (int i=0; i<ns; i++) {
2072+
// SECURITY: update juice limit while evicting delegated stakes
2073+
// This is safe because we are only deleting stuff
2074+
MapEntry<Address,CVMLong> staked=stakes.entryAt(i);
2075+
ctx=ctx.withJuiceLimit(getJuiceLimit()+Juice.TRANSFER);
2076+
ctx=ctx.consumeJuice(Juice.TRANSFER);
2077+
Address stakedAddress=staked.getKey();
2078+
long stake=staked.getValue().longValue();
2079+
if (stake<=0) continue; // shouldn't happen? just in case....
2080+
2081+
// Do refund to delegated staker
2082+
AccountStatus stakingAccount=ctx.getAccountStatus(stakedAddress); // should always exist
2083+
stakingAccount=stakingAccount.withBalance(stakingAccount.getBalance()+stake);
2084+
ctx=ctx.withAccountStatus(stakedAddress, stakingAccount);
2085+
}
2086+
2087+
// Get the controller account for stake refund
2088+
AccountStatus controlAccount=ctx.getAccountStatus(controller); // should always exist. Use ctx!!!
2089+
if (controlAccount==null) {
2090+
// we refund the caller
2091+
controller=getAddress();
2092+
controlAccount=getAccountStatus();
2093+
}
2094+
2095+
long peerStake=ps.getPeerStake();
2096+
controlAccount=controlAccount.withBalance(controlAccount.getBalance()+peerStake);
2097+
ctx=ctx.withAccountStatus(controller, controlAccount);
2098+
2099+
// Finally remove peer record from state
2100+
State s=ctx.getState();
2101+
ctx=ctx.withState(s.withPeers(s.getPeers().dissoc(peerKey)));
2102+
2103+
return ctx.withResult(CVMLong.create(peerStake));
20622104
}
20632105

20642106
/**

convex-core/src/main/java/convex/core/lang/Core.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -967,7 +967,6 @@ public Context invoke(Context context, ACell[] args) {
967967
if (amount == null) return context.withCastError(1,args, Types.LONG);
968968

969969
return context.setDelegatedStake(accountKey, amount.longValue()).consumeJuice(Juice.TRANSFER);
970-
971970
}
972971
});
973972

@@ -993,10 +992,17 @@ public Context invoke(Context context, ACell[] args) {
993992
public Context invoke(Context context, ACell[] args) {
994993
if (args.length != 1) return context.withArityError(exactArityMessage(1, args.length));
995994

995+
996996
AccountKey accountKey = RT.ensureAccountKey(args[0]);
997997
if (accountKey == null) return context.withCastError(0,args, Types.BLOB);
998+
999+
// Security: Consume juice first, since eviction can potentially use arbitrary juice
1000+
// We still want juice to be paid in case of any error
1001+
context=context.consumeJuice(Juice.PEER_UPDATE);
1002+
if (context.isExceptional()) return context;
9981003

999-
return context.evictPeer(accountKey).consumeJuice(Juice.PEER_UPDATE);
1004+
// SECURITY: no juice consumption here, we always let this succeed if last op
1005+
return context.evictPeer(accountKey);
10001006
}
10011007
});
10021008

convex-core/src/test/java/convex/core/lang/CoreTest.java

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131

3232
import org.junit.jupiter.api.Test;
3333

34+
import convex.core.Coin;
3435
import convex.core.Constants;
3536
import convex.core.ErrorCodes;
3637
import convex.core.cpos.Block;
@@ -3448,7 +3449,54 @@ public void testCreatePeer() {
34483449
public void testCreatePeerRegression() {
34493450
assertJuiceError(step("(create-peer 0x42ae93b185bd2ba64fd9b0304fec81a4d4809221a5b68de4da041b48c85bcc2e *balance*)"));
34503451
assertFundsError(step("(create-peer 0x42ae93b185bd2ba64fd9b0304fec81a4d4809221a5b68de4da041b48c85bcc2e (inc *balance*))"));
3452+
}
3453+
3454+
@Test
3455+
public void testEvictPeer() {
3456+
{ // create a peer then evict it
3457+
Context ctx=context();
3458+
long PEERSTAKE=2*Constants.MINIMUM_EFFECTIVE_STAKE;
3459+
AccountKey PK=AccountKey.fromHex("42ae93b185bd2ba64fd9b0304fec81a4d4809221a5b68de4da041b48c85bcc2e");
3460+
ctx=exec(ctx,"(create-peer "+PK+" "+PEERSTAKE+")");
3461+
assertNotNull(ctx.getState().getPeer(PK));
3462+
assertEquals(Coin.MAX_SUPPLY,ctx.getState().computeTotalBalance());
3463+
assertCVMEquals(PEERSTAKE,eval(ctx,"(get-in *state* [:peers "+PK+" :stake])"));
3464+
ctx=exec(ctx,"(evict-peer "+PK+")");
3465+
assertCVMEquals(PEERSTAKE,ctx.getResult());
3466+
assertNull(ctx.getState().getPeer(PK));
3467+
assertEquals(Coin.MAX_SUPPLY,ctx.getState().computeTotalBalance());
3468+
}
3469+
3470+
{ // create a peer with delegated stake and evict
3471+
Context ctx=context();
3472+
long PEERSTAKE=2*Constants.MINIMUM_EFFECTIVE_STAKE;
3473+
long USERFUND=10000000;
3474+
long USERSTAKE=7000000;
3475+
AccountKey PK=AccountKey.fromHex("42ae93b185bd2ba64fd9b0304fec81a4d4809221a5b68de4da041b48c85bcc2e");
3476+
ctx=exec(ctx,"(create-peer "+PK+" "+PEERSTAKE+")");
3477+
ctx=exec(ctx,"(def USER (deploy '(set-controller *caller*) '(defn ^:callable receive-coin [& args] (accept *offer*))))");
3478+
Address USER=ctx.getResult();
3479+
ctx=exec(ctx,"(transfer USER "+USERFUND+")");
3480+
ctx=exec(ctx,"(eval-as USER '(stake "+PK+" "+USERSTAKE+"))");
3481+
assertCVMEquals(USERSTAKE,ctx.getResult());
3482+
assertEquals(Coin.MAX_SUPPLY,ctx.getState().computeTotalBalance());
3483+
assertEquals(PEERSTAKE+USERSTAKE,ctx.getState().getPeer(PK).getTotalStake());
3484+
assertEquals(USERFUND-USERSTAKE,ctx.getBalance(USER));
3485+
3486+
// USER should't be able to evict
3487+
assertStateError(step(ctx,"(eval-as USER '(evict-peer "+PK+"))"));
3488+
3489+
// We can evict, user refund should happen
3490+
ctx=exec(ctx,"(evict-peer "+PK+")");
3491+
assertEquals(USERFUND,ctx.getBalance(USER));
3492+
assertEquals(Coin.MAX_SUPPLY,ctx.getState().computeTotalBalance());
3493+
}
34513494

3495+
assertCastError(step("(evict-peer nil)"));
3496+
assertCastError(step("(evict-peer 0x)"));
3497+
assertCastError(step("(evict-peer [])"));
3498+
assertArityError(step("(evict-peer :foo :bar)"));
3499+
assertArityError(step("(evict-peer)"));
34523500
}
34533501

34543502
@Test

0 commit comments

Comments
 (0)