Corda: UtxoTrxCandidate validation

validation logic shall be stored in Command class,
which is invoced by Contract class
This commit is contained in:
djmil 2023-09-13 20:54:40 +02:00
parent a836d14fbd
commit d7b6ce1f25
5 changed files with 91 additions and 60 deletions

View File

@ -13,9 +13,9 @@ public class GameBoardCommand implements Command {
public static enum Type { public static enum Type {
MOVE, MOVE,
SURRENDER, SURRENDER,
REQUEST_DRAW, DRAW,
REQUEST_VICTORY, VICTORY,
FINISH; // aka accept DRAW or VICTORY request ACCEPT; // aka accept opponents DRAW or VICTORY request
} }
private final Type type; private final Type type;

View File

@ -22,23 +22,34 @@ public class GameBoardContract implements net.corda.v5.ledger.utxo.Contract {
public void verify(UtxoLedgerTransaction trx) { public void verify(UtxoLedgerTransaction trx) {
log.info("GameBoardContract.verify() called"); log.info("GameBoardContract.verify() called");
requireThat(trx.getCommands().size() == 1, REQUIRE_SINGLE_COMMAND);
final Command command = getSingleCommand(trx, Command.class); final Command command = getSingleCommand(trx, Command.class);
if (command instanceof GameProposalCommand) { if (command instanceof GameProposalCommand) {
log.info("GameBoardContract.verify() as GameProposalCommand "+(GameProposalCommand)command); log.info("GameBoardContract.verify() as GameProposalCommand "+(GameProposalCommand)command);
switch ((GameProposalCommand)command) {
case ACCEPT:
GameProposalCommand.validateAcceptTrx(trx);
break;
default:
throw new CordaRuntimeException(UNKNOWN_COMMAND);
}
} else } else
if (command instanceof GameBoardCommand) { if (command instanceof GameBoardCommand) {
log.info("GameBoardContract.verify() as GameBoardCommand "+((GameBoardCommand)command).getType()); log.info("GameBoardContract.verify() as GameBoardCommand "+((GameBoardCommand)command).getType());
switch (((GameBoardCommand)command).getType()) { switch (((GameBoardCommand)command).getType()) {
case MOVE: case MOVE:
break; break;
case SURRENDER: case SURRENDER:
break; break;
case REQUEST_DRAW:
case DRAW:
break; break;
case REQUEST_VICTORY: case VICTORY:
break; break;
case FINISH: case ACCEPT:
break; break;
} }
} else { } else {

View File

@ -1,9 +1,15 @@
package djmil.cordacheckers.contracts; package djmil.cordacheckers.contracts;
import djmil.cordacheckers.states.GameBoardState;
import djmil.cordacheckers.states.GameProposalState; import djmil.cordacheckers.states.GameProposalState;
import net.corda.v5.base.types.MemberX500Name; import net.corda.v5.base.types.MemberX500Name;
import net.corda.v5.ledger.utxo.Command; import net.corda.v5.ledger.utxo.Command;
import net.corda.v5.ledger.utxo.StateAndRef; import net.corda.v5.ledger.utxo.StateAndRef;
import net.corda.v5.ledger.utxo.transaction.UtxoLedgerTransaction;
import static djmil.cordacheckers.contracts.UtxoLedgerTransactionUtil.getSingleInputState;
import static djmil.cordacheckers.contracts.UtxoLedgerTransactionUtil.getSingleOutputState;
import static djmil.cordacheckers.contracts.UtxoLedgerTransactionUtil.requireThat;
public enum GameProposalCommand implements Command { public enum GameProposalCommand implements Command {
CREATE, CREATE,
@ -45,5 +51,52 @@ public enum GameProposalCommand implements Command {
return getRespondent(utxoGameProposal.getState().getContractState()); return getRespondent(utxoGameProposal.getState().getContractState());
} }
public static void validateCreateTrx(UtxoLedgerTransaction trx) {
requireThat(trx.getInputContractStates().isEmpty(), CREATE_INPUT_STATE);
requireThat(trx.getOutputContractStates().size() == 1, CREATE_OUTPUT_STATE);
GameProposalState outputState = getSingleOutputState(trx, GameProposalState.class);
requireThat(outputState.getAcquierColor() != null, NON_NULL_RECIPIENT_COLOR);
}
public static void validateAcceptTrx(UtxoLedgerTransaction trx) {
requireThat(trx.getInputContractStates().size() == 1, ACCEPT_INPUT_STATE);
requireThat(trx.getOutputContractStates().size() == 1, ACCEPT_OUTPUT_STATE);
GameProposalState inGameProposal = getSingleInputState(trx, GameProposalState.class);
GameBoardState outGameBoard = getSingleOutputState(trx, GameBoardState.class);
requireThat(outGameBoard.getParticipants().containsAll(inGameProposal.getParticipants()), ACCEPT_PARTICIPANTS);
}
public static void validateRejectTrx(UtxoLedgerTransaction trx) {
requireThat(trx.getInputContractStates().size() == 1, REJECT_INPUT_STATE);
requireThat(trx.getOutputContractStates().isEmpty(), REJECT_OUTPUT_STATE);
getSingleInputState(trx, GameProposalState.class);
}
public static void validateCancelTrx(UtxoLedgerTransaction trx) {
requireThat(trx.getInputContractStates().size() == 1, CANCEL_INPUT_STATE);
requireThat(trx.getOutputContractStates().isEmpty(), CANCEL_OUTPUT_STATE);
getSingleInputState(trx, GameProposalState.class);
}
public static final String UNSUPPORTED_VALUE_OF = "Unsupported GameProposalCommand value: "; public static final String UNSUPPORTED_VALUE_OF = "Unsupported GameProposalCommand value: ";
static final String CREATE_INPUT_STATE = "Create command should have no input states";
static final String CREATE_OUTPUT_STATE = "Create command should output exactly one GameProposal state";
static final String NON_NULL_RECIPIENT_COLOR = "GameProposal.recipientColor field can not be null";
static final String REJECT_INPUT_STATE = "Reject command should have exactly one GameProposal input state";
static final String REJECT_OUTPUT_STATE = "Reject command should have no output states";
static final String CANCEL_INPUT_STATE = "Cancel command should have exactly one GameProposal input state";
static final String CANCEL_OUTPUT_STATE = "Cancel command should have no output states";
static final String ACCEPT_INPUT_STATE = "Accept command should have exactly one GameProposal input state";
static final String ACCEPT_OUTPUT_STATE = "Accept command should have exactly one GameBoard output state";
static final String ACCEPT_PARTICIPANTS = "Accept command: GameBoard participants should math GameProposal participants";
} }

View File

@ -1,17 +1,14 @@
package djmil.cordacheckers.contracts; package djmil.cordacheckers.contracts;
import static djmil.cordacheckers.contracts.UtxoLedgerTransactionUtil.getSingleCommand;
import static djmil.cordacheckers.contracts.UtxoLedgerTransactionUtil.getSingleInputState;
import static djmil.cordacheckers.contracts.UtxoLedgerTransactionUtil.getSingleOutputState;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import djmil.cordacheckers.states.GameBoardState;
import djmil.cordacheckers.states.GameProposalState;
import net.corda.v5.base.exceptions.CordaRuntimeException; import net.corda.v5.base.exceptions.CordaRuntimeException;
import net.corda.v5.ledger.utxo.transaction.UtxoLedgerTransaction; import net.corda.v5.ledger.utxo.transaction.UtxoLedgerTransaction;
import static djmil.cordacheckers.contracts.UtxoLedgerTransactionUtil.getSingleCommand;
import static djmil.cordacheckers.contracts.UtxoLedgerTransactionUtil.requireThat;
public class GameProposalContract implements net.corda.v5.ledger.utxo.Contract { public class GameProposalContract implements net.corda.v5.ledger.utxo.Contract {
private final static Logger log = LoggerFactory.getLogger(GameProposalContract.class); private final static Logger log = LoggerFactory.getLogger(GameProposalContract.class);
@ -20,67 +17,31 @@ public class GameProposalContract implements net.corda.v5.ledger.utxo.Contract {
public void verify(UtxoLedgerTransaction trx) { public void verify(UtxoLedgerTransaction trx) {
log.info("GameProposalContract.verify() called"); log.info("GameProposalContract.verify() called");
requireThat(trx.getCommands().size() == 1, REQUIRE_SINGLE_COMMAND);
final GameProposalCommand command = getSingleCommand(trx, GameProposalCommand.class); final GameProposalCommand command = getSingleCommand(trx, GameProposalCommand.class);
switch (command) { switch (command) {
case CREATE: { case CREATE:
requireThat(trx.getInputContractStates().isEmpty(), CREATE_INPUT_STATE); GameProposalCommand.validateCreateTrx(trx);
requireThat(trx.getOutputContractStates().size() == 1, CREATE_OUTPUT_STATE); break;
GameProposalState outputState = getSingleOutputState(trx, GameProposalState.class); case ACCEPT:
GameProposalCommand.validateAcceptTrx(trx);
break;
requireThat(outputState.getAcquierColor() != null, NON_NULL_RECIPIENT_COLOR); case REJECT:
break; } GameProposalCommand.validateRejectTrx(trx);
break;
case ACCEPT: {
requireThat(trx.getInputContractStates().size() == 1, ACCEPT_INPUT_STATE);
requireThat(trx.getOutputContractStates().size() == 1, ACCEPT_OUTPUT_STATE);
GameProposalState inGameProposal = getSingleInputState(trx, GameProposalState.class);
GameBoardState outGameBoard = getSingleOutputState(trx, GameBoardState.class);
requireThat(outGameBoard.getParticipants().containsAll(inGameProposal.getParticipants()), ACCEPT_PARTICIPANTS);
break; }
case REJECT: {
requireThat(trx.getInputContractStates().size() == 1, REJECT_INPUT_STATE);
requireThat(trx.getOutputContractStates().isEmpty(), REJECT_OUTPUT_STATE);
getSingleInputState(trx, GameProposalState.class);
break; }
case CANCEL: case CANCEL:
requireThat(trx.getInputContractStates().size() == 1, CANCEL_INPUT_STATE); GameProposalCommand.validateCancelTrx(trx);
requireThat(trx.getOutputContractStates().isEmpty(), CANCEL_OUTPUT_STATE);
getSingleInputState(trx, GameProposalState.class);
break; break;
default: default:
throw new CordaRuntimeException(UNKNOWN_COMMAND); throw new CordaRuntimeException(UNKNOWN_COMMAND);
} }
} }
private void requireThat(boolean asserted, String errorMessage) {
if (!asserted) {
throw new CordaRuntimeException("Failed requirement: " + errorMessage);
}
}
static final String REQUIRE_SINGLE_COMMAND = "Require a single command"; static final String REQUIRE_SINGLE_COMMAND = "Require a single command";
static final String UNKNOWN_COMMAND = "Unsupported command"; static final String UNKNOWN_COMMAND = "Unsupported command";
}
static final String CREATE_INPUT_STATE = "Create command should have no input states";
static final String CREATE_OUTPUT_STATE = "Create command should output exactly one GameProposal state";
static final String NON_NULL_RECIPIENT_COLOR = "GameProposal.recipientColor field can not be null";
static final String REJECT_INPUT_STATE = "Reject command should have exactly one GameProposal input state";
static final String REJECT_OUTPUT_STATE = "Reject command should have no output states";
static final String CANCEL_INPUT_STATE = "Cancel command should have exactly one GameProposal input state";
static final String CANCEL_OUTPUT_STATE = "Cancel command should have no output states";
static final String ACCEPT_INPUT_STATE = "Accept command should have exactly one GameProposal input state";
static final String ACCEPT_OUTPUT_STATE = "Accept command should have exactly one GameBoard output state";
static final String ACCEPT_PARTICIPANTS = "Accept command: GameBoard participants should math GameProposal participants";
}

View File

@ -40,6 +40,12 @@ public class UtxoLedgerTransactionUtil {
return optional(utxoTrx.getOutputStates(clazz), clazz); return optional(utxoTrx.getOutputStates(clazz), clazz);
} }
public static void requireThat(boolean asserted, String errorMessage) {
if (!asserted) {
throw new IllegalStateException("Failed requirement: " + errorMessage);
}
}
private static <T> Optional<T> optional(List<T> list, Class<T> clazz) { private static <T> Optional<T> optional(List<T> list, Class<T> clazz) {
return list return list
.stream() .stream()