Compare commits

...

5 Commits

Author SHA1 Message Date
8971462c74 GameResultBuilder subflow
- looks for custodian and add it to the State participants
- extra parameter to Commit subflow
 to initiate exchange session with Custodian as well
2023-09-22 21:39:42 +02:00
e1cc9bd9fd RankingFLow: use actual GameResultStates 2023-09-22 17:23:06 +02:00
5f59260120 ammend 2023-09-22 16:07:54 +02:00
fe1708ad32 Draft: RankingFlow +Test 2023-09-22 16:07:44 +02:00
74176ecf45 HoldingIdentityResolver Test 2023-09-22 14:55:53 +02:00
16 changed files with 408 additions and 85 deletions

View File

@ -19,6 +19,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import djmil.cordacheckers.cordaclient.dao.GameState;
import djmil.cordacheckers.cordaclient.dao.HoldingIdentity;
import djmil.cordacheckers.cordaclient.dao.Piece;
import djmil.cordacheckers.cordaclient.dao.Rank;
import djmil.cordacheckers.cordaclient.dao.VirtualNode;
import djmil.cordacheckers.cordaclient.dao.VirtualNodeList;
import djmil.cordacheckers.cordaclient.dao.flow.RequestBody;
@ -29,6 +30,7 @@ import djmil.cordacheckers.cordaclient.dao.flow.arguments.ReqGameBoardMove;
import djmil.cordacheckers.cordaclient.dao.flow.arguments.ReqGameProposalCreate;
import djmil.cordacheckers.cordaclient.dao.flow.arguments.RspGameState;
import djmil.cordacheckers.cordaclient.dao.flow.arguments.RspGameStateList;
import djmil.cordacheckers.cordaclient.dao.flow.arguments.RspRankList;
@Service
public class CordaClient {
@ -58,6 +60,17 @@ public class CordaClient {
).virtualNodes();
}
// use custodian holding identity to get ranking table of all players
public List<Rank> fetchRanking(HoldingIdentity holdingIdentity) {
final RequestBody requestBody = new RequestBody(
"ranking-" +UUID.randomUUID(),
"djmil.cordacheckers.gameresult.RankingFlow",
new Req());
return cordaFlowExecute(holdingIdentity, requestBody, RspRankList.class)
.getResponce(requestBody);
}
/**
* @param holdingIdentity
* @return list of unconsumed (active) GameStateViews

View File

@ -0,0 +1,9 @@
package djmil.cordacheckers.cordaclient.dao;
public record Rank(
String name,
Integer gamesPlayed,
Integer gamesWon
) {
}

View File

@ -0,0 +1,11 @@
package djmil.cordacheckers.cordaclient.dao.flow.arguments;
import java.util.List;
import djmil.cordacheckers.cordaclient.dao.Rank;
public record RspRankList(
List<Rank> successStatus,
String failureStatus) implements Rsp<List<Rank>> {
}

View File

@ -0,0 +1,67 @@
package djmil.cordacheckers;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import djmil.cordacheckers.user.HoldingIdentityResolver;
@SpringBootTest
public class HoldingIdentityResolverTests {
@Autowired
HoldingIdentityResolver holdingIdentityResolver;
/*
* The test is expecting that CSDE static-network-config.json to look like stated below.
* Important bits:
* - the contents of 'CN', 'OU', 'O' fields
* - there shall be only one Custodian
*/
/*
[
{
"x500Name" : "CN=Alice, OU=Player, O=Checkers, L=Zug, C=CH",
"cpi" : "CordaCheckers"
},
{
"x500Name" : "CN=Bob, OU=Player, O=Checkers, L=Kyiv, C=UA",
"cpi" : "CordaCheckers"
},
{
"x500Name" : "CN=Charlie, OU=Player, O=Checkers, L=London, C=GB",
"cpi" : "CordaCheckers"
},
{
"x500Name" : "CN=Kumar, OU=Custodian, O=Checkers, L=Mumbai, C=IN",
"cpi" : "CordaCheckers"
},
{
"x500Name" : "CN=NotaryRep1, OU=Test Dept, O=R3, L=London, C=GB",
"cpi" : "NotaryServer",
"serviceX500Name": "CN=NotaryService, OU=Test Dept, O=R3, L=London, C=GB"
}
]
*/
@Test
void testPlayer() {
assertDoesNotThrow( () -> {
holdingIdentityResolver.getByUsername("alice");
holdingIdentityResolver.getByUsername("BOB");
holdingIdentityResolver.getByUsername("Charlie");
});
assertThat(holdingIdentityResolver.getByUsername("SomeRundomName")).isNull();
}
@Test
void testCustodian() {
assertThat(holdingIdentityResolver.getByUsername("Kumar")).isNull();
assertThat(holdingIdentityResolver.getCustodian()).isNotNull();
}
}

View File

@ -60,40 +60,4 @@ public class GameBoardTests {
assertThat(winnerGameView.status()).isEqualByComparingTo(Status.GAME_RESULT_YOU_WON);
}
// @Test
// void testGameBoardMove() throws JsonMappingException, JsonProcessingException, InvalidNameException {
// final var hiAlice = holdingIdentityResolver.getByUsername("alice");
// final var hiBob = holdingIdentityResolver.getByUsername("bob");
// final var bobColor = Piece.Color.WHITE;
// final UUID gpUuid = cordaClient.gameProposalCreate(
// hiAlice, hiBob,
// bobColor, "GameBoard MOVE test"
// );
// System.out.println("New GameProposal UUID "+ gpUuid);
// final GameBoard gbState = cordaClient.gameProposalAccept(
// hiBob, gpUuid
// );
// System.out.println("New GameBoard UUID "+ gbState.gameUuid());
// assertThatThrownBy(() -> { // Alice can not move, since it is Bob's turn
// cordaClient.gameBoardMove(
// hiAlice, gbState.gameUuid(),
// Arrays.asList(1, 2));
// });
// final GameBoard gameBoard = cordaClient.gameBoardMove(
// hiBob, gbState.gameUuid(), Arrays.asList(1, 2));
// }
// private <T extends GameState> T findByUuid(List<T> statesList, UUID uuid) {
// for (T state : statesList) {
// if (state.uuid().compareTo(uuid) == 0)
// return state;
// };
// return null;
// }
}

View File

@ -1,20 +1,14 @@
package djmil.cordacheckers.cordaclient;
import static org.assertj.core.api.Assertions.assertThat;
import java.util.List;
import java.util.UUID;
import javax.naming.InvalidNameException;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import com.fasterxml.jackson.core.JsonProcessingException;
import djmil.cordacheckers.cordaclient.dao.GameState;
import djmil.cordacheckers.cordaclient.dao.VirtualNode;
import djmil.cordacheckers.cordaclient.dao.Piece;
import djmil.cordacheckers.user.HoldingIdentityResolver;
@SpringBootTest
@ -25,15 +19,12 @@ public class GameStateTests {
@Autowired
HoldingIdentityResolver holdingIdentityResolver;
@Test
void testGetVirtualNodeList() throws InvalidNameException {
List<VirtualNode> vNodes = cordaClient.getVirtualNodeList();
assertThat(vNodes.size()).isEqualTo(5); // default vNode config for CSDE
}
final String issuer = "alice";
final String acquier = "bob";
final Piece.Color acquierColor = Piece.Color.WHITE;
@Test
void testList() throws JsonProcessingException {
void testList() {
List<GameState> gsList = cordaClient.gameStateList(
holdingIdentityResolver.getByUsername("bob"));
@ -41,7 +32,7 @@ public class GameStateTests {
}
@Test
void testGet() throws JsonProcessingException {
void testGet() {
GameState gameStateView = cordaClient.gameStateGet(
holdingIdentityResolver.getByUsername("bob"),
UUID.fromString("cf357d0a-8f64-4599-b9b5-d263163812d4")

View File

@ -0,0 +1,52 @@
package djmil.cordacheckers.cordaclient;
import static org.assertj.core.api.Assertions.assertThat;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import djmil.cordacheckers.cordaclient.dao.GameState;
import djmil.cordacheckers.cordaclient.dao.GameState.Status;
import djmil.cordacheckers.cordaclient.dao.Piece;
import djmil.cordacheckers.cordaclient.dao.Rank;
import djmil.cordacheckers.user.HoldingIdentityResolver;
@SpringBootTest
public class RankingTests {
@Autowired
CordaClient cordaClient;
@Autowired
HoldingIdentityResolver holdingIdentityResolver;
@Test
void testGlobalRanking() {
final var hiCustodian = holdingIdentityResolver.getCustodian();
final List<Rank> liderboard1 = cordaClient.fetchRanking(hiCustodian);
final var hiWinner = holdingIdentityResolver.getByUsername("Charlie");
final var hiLooser = holdingIdentityResolver.getByUsername("Bob");
final GameState game = cordaClient.gameProposalCreate(
hiWinner, hiLooser, Piece.Color.WHITE, "GameBoard GLOBAL_RANKING test");
cordaClient.gameProposalAccept(hiLooser, game.uuid());
cordaClient.gameBoardSurrender(hiLooser, game.uuid());
final List<Rank> liderboard2 = cordaClient.fetchRanking(hiCustodian);
System.out.println(liderboard1);
System.out.println(liderboard2);
}
@Test
void testIndividualRanking() {
final var hiCustodian = holdingIdentityResolver.getByUsername("Bob");
final List<Rank> liderboard = cordaClient.fetchRanking(hiCustodian);
System.out.println(liderboard);
}
}

View File

@ -21,8 +21,8 @@ public class GameResultState extends GameState {
this.winnerName = winnerName;
}
public GameResultState(GameBoardState gameBoardState, MemberX500Name winnerName) {
super(gameBoardState.whitePlayer, gameBoardState.blackPlayer, gameBoardState.gameUuid, null, gameBoardState.participants);
public GameResultState(GameBoardState gameBoardState, MemberX500Name winnerName, PublicKey custodianPubicKey) {
super(gameBoardState.whitePlayer, gameBoardState.blackPlayer, gameBoardState.gameUuid, null, gameBoardState.participants, custodianPubicKey);
this.winnerName = winnerName;
}

View File

@ -1,6 +1,7 @@
package djmil.cordacheckers.states;
import java.security.PublicKey;
import java.util.LinkedList;
import java.util.List;
import java.util.UUID;
@ -25,6 +26,18 @@ public abstract class GameState implements ContractState {
this.participants = participants;
}
GameState(MemberX500Name whitePlayer, MemberX500Name blackPlayer, UUID gameUuid,
String message, List<PublicKey> participants, PublicKey additionalParticipand) {
this.whitePlayer = whitePlayer;
this.blackPlayer = blackPlayer;
this.gameUuid = gameUuid;
this.message = message;
var deepCopy = new LinkedList<>(participants);
deepCopy.add(additionalParticipand);
this.participants = deepCopy;
}
public MemberX500Name getWhitePlayer() {
return whitePlayer;
}

View File

@ -8,13 +8,13 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import djmil.cordacheckers.contracts.GameCommand;
import djmil.cordacheckers.gameresult.GameResultBuilder;
import djmil.cordacheckers.gameresult.GameResultBuilder.Reason;
import djmil.cordacheckers.gamestate.CommitSubFlow;
import djmil.cordacheckers.gamestate.FlowResponce;
import djmil.cordacheckers.gamestate.GetFlow;
import djmil.cordacheckers.gamestate.View;
import djmil.cordacheckers.gamestate.ViewBuilder;
import djmil.cordacheckers.states.GameBoardState;
import djmil.cordacheckers.states.GameResultState;
import djmil.cordacheckers.states.GameState;
import net.corda.v5.application.flows.ClientRequestBody;
import net.corda.v5.application.flows.ClientStartableFlow;
@ -23,7 +23,6 @@ import net.corda.v5.application.flows.FlowEngine;
import net.corda.v5.application.marshalling.JsonMarshallingService;
import net.corda.v5.application.membership.MemberLookup;
import net.corda.v5.base.annotations.Suspendable;
import net.corda.v5.base.types.MemberX500Name;
import net.corda.v5.crypto.SecureHash;
import net.corda.v5.ledger.common.NotaryLookup;
import net.corda.v5.ledger.utxo.StateAndRef;
@ -57,24 +56,27 @@ public class SurrenderFlow implements ClientStartableFlow{
try {
final GameCommand command = new GameCommand(GameCommand.Action.GAME_BOARD_SURRENDER);
final UUID gameStateUuid = UUID.fromString(requestBody.getRequestBody());
final UUID gameUuid = UUID.fromString(requestBody.getRequestBody());
final StateAndRef<GameState> inputStateSar = this.flowEngine
.subFlow(new GetFlow(gameStateUuid));
final StateAndRef<GameState> gameBoardSar = this.flowEngine
.subFlow(new GetFlow(gameUuid));
final GameResultState outputState = prepareGameResultState(inputStateSar);
final GameResultBuilder.Result out = this.flowEngine
.subFlow(new GameResultBuilder(gameBoardSar, Reason.SURRENDER));
final UtxoSignedTransaction gameBoardSurrenderTrx = utxoLedgerService.createTransactionBuilder()
.addCommand(command)
.addInputState(inputStateSar.getRef())
.addOutputState(outputState)
.addSignatories(outputState.getParticipants())
.setNotary(inputStateSar.getState().getNotaryName())
.addInputState(gameBoardSar.getRef())
.addOutputState(out.gameResult)
.addSignatories(out.gameResult.getParticipants())
.setNotary(gameBoardSar.getState().getNotaryName())
.setTimeWindowUntil(Instant.now().plusMillis(Duration.ofDays(1).toMillis()))
.toSignedTransaction();
utxoTrxId = this.flowEngine
.subFlow(new CommitSubFlow(gameBoardSurrenderTrx, command.getCounterparty(inputStateSar)));
.subFlow(new CommitSubFlow(gameBoardSurrenderTrx,
command.getCounterparty(gameBoardSar),
out.custodyName));
final View gameStateView = this.flowEngine
.subFlow(new ViewBuilder(utxoTrxId));
@ -90,15 +92,4 @@ public class SurrenderFlow implements ClientStartableFlow{
}
}
@Suspendable
GameResultState prepareGameResultState(StateAndRef<GameState> gameStateSar) {
final GameState gameState = gameStateSar.getState().getContractState();
final GameBoardState gameBoard = (GameBoardState) gameState;
final MemberX500Name myName = memberLookup.myInfo().getName();
final MemberX500Name winnerName = gameBoard.getOpponentName(myName);
return new GameResultState(gameBoard, winnerName);
}
}

View File

@ -0,0 +1,74 @@
package djmil.cordacheckers.gameresult;
import djmil.cordacheckers.states.GameBoardState;
import djmil.cordacheckers.states.GameResultState;
import djmil.cordacheckers.states.GameState;
import net.corda.v5.application.flows.CordaInject;
import net.corda.v5.application.flows.SubFlow;
import net.corda.v5.application.membership.MemberLookup;
import net.corda.v5.base.annotations.Suspendable;
import net.corda.v5.base.types.MemberX500Name;
import net.corda.v5.ledger.utxo.StateAndRef;
import net.corda.v5.membership.MemberInfo;
public class GameResultBuilder implements SubFlow<GameResultBuilder.Result> {
public class Result {
final public GameResultState gameResult;
final public MemberX500Name custodyName;
public Result(GameResultState gameResult, MemberX500Name custodyName){
this.gameResult = gameResult;
this.custodyName = custodyName;
}
}
public static enum Reason {
VICTORY,
SURRENDER
}
private final GameBoardState gameBoard;
private final Reason reason;
public GameResultBuilder(StateAndRef<GameState> gameBoardSar, Reason reason) {
this.gameBoard = (GameBoardState)gameBoardSar.getState().getContractState();
this.reason = reason;
}
@CordaInject
public MemberLookup memberLookup;
@Override
@Suspendable
public Result call() {
final MemberInfo custodiaInfo = findCustodian();
return new Result(
new GameResultState(gameBoard, winnerName(), custodiaInfo.getLedgerKeys().get(0)),
custodiaInfo.getName());
}
@Suspendable
MemberInfo findCustodian() {
return memberLookup.lookup()
.stream()
.filter(m -> m.getName().getOrganizationUnit().equals("Custodian") )
.reduce((a,b) -> {throw new IllegalStateException("Multiple Custodian VNodes");})
.orElseThrow( () -> new IllegalStateException("No Custodian VNode found"));
}
@Suspendable
MemberX500Name winnerName() {
final MemberX500Name myName = memberLookup.myInfo().getName();
switch(reason) {
case VICTORY:
return myName;
case SURRENDER:
return gameBoard.getOpponentName(myName);
}
throw new IllegalStateException("Bad reason");
}
}

View File

@ -0,0 +1,20 @@
package djmil.cordacheckers.gameresult;
public class Rank {
public final String name;
public final Integer gamesPlayed;
public final Integer gamesWon;
// Serialisation service requires a default constructor
Rank() {
name = null;
gamesPlayed = null;
gamesWon = null;
}
public Rank(String name, Integer gamesPlayed, Integer gamesWon) {
this.name = name;
this.gamesPlayed = gamesPlayed;
this.gamesWon = gamesWon;
}
}

View File

@ -0,0 +1,75 @@
package djmil.cordacheckers.gameresult;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import djmil.cordacheckers.states.GameResultState;
import net.corda.v5.application.flows.ClientRequestBody;
import net.corda.v5.application.flows.ClientStartableFlow;
import net.corda.v5.application.flows.CordaInject;
import net.corda.v5.application.flows.FlowEngine;
import net.corda.v5.application.marshalling.JsonMarshallingService;
import net.corda.v5.base.annotations.Suspendable;
import net.corda.v5.ledger.utxo.UtxoLedgerService;
public class RankingFlow implements ClientStartableFlow {
private final static Logger log = LoggerFactory.getLogger(RankingFlow.class);
@CordaInject
public UtxoLedgerService utxoLedgerService;
@CordaInject
public JsonMarshallingService jsonMarshallingService;
@CordaInject
public FlowEngine flowEngine;
@Suspendable
@Override
public String call(ClientRequestBody requestBody) {
try {
final List<GameResultState> gameStateResutList = getGameResultsList();
Map<String, Integer> won = new HashMap<>();
for (GameResultState gs : gameStateResutList) {
final var winner = gs.getWinnerName().getCommonName();
won.put(winner, won.getOrDefault(winner, 0) +1);
}
Map<String, Integer> played = new HashMap<>(won);
for (GameResultState gs : gameStateResutList) {
final var looseer = gs.getLooserName().getCommonName();
played.put(looseer, played.getOrDefault(looseer, 0) +1);
}
List<Rank> board = new LinkedList<Rank>();
for (String name : played.keySet()) {
board.add(new Rank(name, played.getOrDefault(name, 0), won.getOrDefault(name, 0)));
}
return new RankingFlowResponce(board)
.toJsonEncodedString(jsonMarshallingService);
} catch (Exception e) {
log.warn("Exception during processing " + requestBody + " request: " + e.getMessage());
return new RankingFlowResponce(e)
.toJsonEncodedString(jsonMarshallingService);
}
}
@Suspendable
List<GameResultState> getGameResultsList() {
return utxoLedgerService
.findUnconsumedStatesByType(GameResultState.class)
.stream()
.map( stateAndRef -> stateAndRef.getState().getContractState() )
.collect(Collectors.toList());
}
}

View File

@ -0,0 +1,25 @@
package djmil.cordacheckers.gameresult;
import java.util.List;
import net.corda.v5.application.marshalling.JsonMarshallingService;
public class RankingFlowResponce {
public final List<Rank> successStatus;
public final String failureStatus;
public RankingFlowResponce(List<Rank> success) {
this.successStatus = success;
this.failureStatus = null;
}
public RankingFlowResponce(Exception exception) {
this.successStatus = null;
this.failureStatus = exception.getMessage();
}
public String toJsonEncodedString(JsonMarshallingService jsonMarshallingService) {
return jsonMarshallingService.format(this);
}
}

View File

@ -1,6 +1,7 @@
package djmil.cordacheckers.gamestate;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import org.slf4j.Logger;
@ -9,6 +10,7 @@ import org.slf4j.LoggerFactory;
import net.corda.v5.application.flows.CordaInject;
import net.corda.v5.application.flows.InitiatingFlow;
import net.corda.v5.application.flows.SubFlow;
import net.corda.v5.application.membership.MemberLookup;
import net.corda.v5.application.messaging.FlowMessaging;
import net.corda.v5.application.messaging.FlowSession;
import net.corda.v5.base.annotations.Suspendable;
@ -23,10 +25,18 @@ public class CommitSubFlow implements SubFlow<SecureHash> {
private final static Logger log = LoggerFactory.getLogger(CommitSubFlow.class);
private final UtxoSignedTransaction utxTrxCandidate;
private final MemberX500Name counterpartyName;
private final MemberX500Name custodyName;
public CommitSubFlow(UtxoSignedTransaction signedTransaction, MemberX500Name counterpartyName) {
this.utxTrxCandidate = signedTransaction;
this.counterpartyName = counterpartyName;
this.custodyName = null;
}
public CommitSubFlow(UtxoSignedTransaction signedTransaction, MemberX500Name counterpartyName, MemberX500Name custodyName) {
this.utxTrxCandidate = signedTransaction;
this.counterpartyName = counterpartyName;
this.custodyName = custodyName;
}
@CordaInject
@ -35,19 +45,25 @@ public class CommitSubFlow implements SubFlow<SecureHash> {
@CordaInject
public FlowMessaging flowMessaging;
@CordaInject
public MemberLookup memberLookup;
@Override
@Suspendable
public SecureHash call() {
log.info("GameState commit started");
final FlowSession session = flowMessaging.initiateFlow(this.counterpartyName);
/*
* Calls the Corda provided finalise() function which gather signatures from the counterparty,
* notarises the transaction and persists the transaction to each party's vault.
*/
final List<FlowSession> sessionsList = Arrays.asList(session);
final FlowSession session = flowMessaging.initiateFlow(this.counterpartyName);
List<FlowSession> sessionsList = new LinkedList<FlowSession>(Arrays.asList(session));
if (custodyName != null) {
sessionsList.add(flowMessaging.initiateFlow(custodyName));
}
final SecureHash trxId = ledgerService
.finalize(this.utxTrxCandidate, sessionsList)

View File

@ -100,16 +100,18 @@ public class CommitSubFlowResponder implements ResponderFlow {
@Suspendable
void checkParticipants(FlowSession session, GameCommand gameCommand, GameState gameState) throws ParticipantException {
final var myName = memberLookup.myInfo().getName();
final var opponentName = gameState.getOpponentName(myName); // throws NotInvolved
final var conterpartyName = session.getCounterparty();
final var actorName = gameCommand.getInitiator(gameState);
if (conterpartyName.compareTo(opponentName) != 0)
throw new ParticipantException("Counterparty", conterpartyName, opponentName);
if (actorName.compareTo(conterpartyName) != 0)
throw new ParticipantException("Actor", conterpartyName, actorName);
final var myName = memberLookup.myInfo().getName();
if (myName.getOrganizationUnit().equals("Custodian"))
return; // Custodian shall not validate state's counterparty
final var opponentName = gameState.getOpponentName(myName); // throws NotInvolved
if (conterpartyName.compareTo(opponentName) != 0)
throw new ParticipantException("Counterparty", conterpartyName, opponentName);
}
public static class ParticipantException extends Exception {