mirror of
https://github.com/corda/corda.git
synced 2024-12-18 20:47:57 +00:00
Added a basic Notary service with protocol for signing transactions
This commit is contained in:
parent
539e23a0b1
commit
fa3f7e7fa6
2
.idea/runConfigurations/Node__seller.xml
generated
2
.idea/runConfigurations/Node__seller.xml
generated
@ -3,7 +3,7 @@
|
||||
<extension name="coverage" enabled="false" merge="false" sample_coverage="true" runner="idea" />
|
||||
<option name="MAIN_CLASS_NAME" value="demos.TraderDemoKt" />
|
||||
<option name="VM_PARAMETERS" value="-ea -javaagent:lib/quasar.jar -Dco.paralleluniverse.fibers.verifyInstrumentation" />
|
||||
<option name="PROGRAM_PARAMETERS" value="--dir=seller --fake-trade-with=localhost --network-address=localhost:31327 --timestamper-identity-file=buyer/identity-public --timestamper-address=localhost" />
|
||||
<option name="PROGRAM_PARAMETERS" value="--dir=seller --fake-trade-with=localhost --network-address=localhost:31340 --network-map-identity-file=buyer/identity-public --network-map-address=localhost" />
|
||||
<option name="WORKING_DIRECTORY" value="" />
|
||||
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" />
|
||||
<option name="ALTERNATIVE_JRE_PATH" />
|
||||
|
@ -9,9 +9,6 @@
|
||||
package contracts.isolated
|
||||
|
||||
import core.*
|
||||
import core.Contract
|
||||
import core.ContractState
|
||||
import core.TransactionForVerification
|
||||
import core.crypto.SecureHash
|
||||
|
||||
// The dummy contract doesn't do anything useful. It exists for testing purposes.
|
||||
@ -19,7 +16,7 @@ import core.crypto.SecureHash
|
||||
val ANOTHER_DUMMY_PROGRAM_ID = AnotherDummyContract()
|
||||
|
||||
class AnotherDummyContract : Contract, core.node.DummyContractBackdoor {
|
||||
class State(val magicNumber: Int = 0) : ContractState {
|
||||
class State(val magicNumber: Int = 0, override val notary: Party) : ContractState {
|
||||
override val contract = ANOTHER_DUMMY_PROGRAM_ID
|
||||
}
|
||||
|
||||
@ -34,11 +31,11 @@ class AnotherDummyContract : Contract, core.node.DummyContractBackdoor {
|
||||
// The "empty contract"
|
||||
override val legalContractReference: SecureHash = SecureHash.sha256("https://anotherdummy.org")
|
||||
|
||||
override fun generateInitial(owner: PartyAndReference, magicNumber: Int) : TransactionBuilder {
|
||||
val state = State(magicNumber)
|
||||
return TransactionBuilder().withItems( state, Command(Commands.Create(), owner.party.owningKey) )
|
||||
override fun generateInitial(owner: PartyAndReference, magicNumber: Int, notary: Party): TransactionBuilder {
|
||||
val state = State(magicNumber, notary)
|
||||
return TransactionBuilder().withItems(state, Command(Commands.Create(), owner.party.owningKey))
|
||||
}
|
||||
|
||||
override fun inspectState(state: core.ContractState) : Int = (state as State).magicNumber
|
||||
override fun inspectState(state: core.ContractState): Int = (state as State).magicNumber
|
||||
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
package core.node
|
||||
|
||||
interface DummyContractBackdoor {
|
||||
fun generateInitial(owner: core.PartyAndReference, magicNumber: Int) : core.TransactionBuilder
|
||||
fun generateInitial(owner: core.PartyAndReference, magicNumber: Int, notary: core.Party): core.TransactionBuilder
|
||||
|
||||
fun inspectState(state: core.ContractState) : Int
|
||||
fun inspectState(state: core.ContractState): Int
|
||||
}
|
@ -1,17 +1,18 @@
|
||||
package contracts;
|
||||
|
||||
import core.*;
|
||||
import core.TransactionForVerification.*;
|
||||
import core.crypto.*;
|
||||
import core.node.services.*;
|
||||
import org.jetbrains.annotations.*;
|
||||
import core.TransactionForVerification.InOutGroup;
|
||||
import core.crypto.NullPublicKey;
|
||||
import core.crypto.SecureHash;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.security.*;
|
||||
import java.time.*;
|
||||
import java.util.*;
|
||||
import java.security.PublicKey;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
import static core.ContractsDSLKt.*;
|
||||
import static kotlin.collections.CollectionsKt.*;
|
||||
import static core.ContractsDSLKt.requireSingleCommand;
|
||||
import static kotlin.collections.CollectionsKt.single;
|
||||
|
||||
|
||||
/**
|
||||
@ -27,35 +28,37 @@ public class JavaCommercialPaper implements Contract {
|
||||
private PublicKey owner;
|
||||
private Amount faceValue;
|
||||
private Instant maturityDate;
|
||||
private Party notary;
|
||||
|
||||
public State() {
|
||||
} // For serialization
|
||||
|
||||
public State(PartyAndReference issuance, PublicKey owner, Amount faceValue, Instant maturityDate) {
|
||||
public State(PartyAndReference issuance, PublicKey owner, Amount faceValue, Instant maturityDate, Party notary) {
|
||||
this.issuance = issuance;
|
||||
this.owner = owner;
|
||||
this.faceValue = faceValue;
|
||||
this.maturityDate = maturityDate;
|
||||
this.notary = notary;
|
||||
}
|
||||
|
||||
public State copy() {
|
||||
return new State(this.issuance, this.owner, this.faceValue, this.maturityDate);
|
||||
return new State(this.issuance, this.owner, this.faceValue, this.maturityDate, this.notary);
|
||||
}
|
||||
|
||||
public ICommercialPaperState withOwner(PublicKey newOwner) {
|
||||
return new State(this.issuance, newOwner, this.faceValue, this.maturityDate);
|
||||
return new State(this.issuance, newOwner, this.faceValue, this.maturityDate, this.notary);
|
||||
}
|
||||
|
||||
public ICommercialPaperState withIssuance(PartyAndReference newIssuance) {
|
||||
return new State(newIssuance, this.owner, this.faceValue, this.maturityDate);
|
||||
return new State(newIssuance, this.owner, this.faceValue, this.maturityDate, this.notary);
|
||||
}
|
||||
|
||||
public ICommercialPaperState withFaceValue(Amount newFaceValue) {
|
||||
return new State(this.issuance, this.owner, newFaceValue, this.maturityDate);
|
||||
return new State(this.issuance, this.owner, newFaceValue, this.maturityDate, this.notary);
|
||||
}
|
||||
|
||||
public ICommercialPaperState withMaturityDate(Instant newMaturityDate) {
|
||||
return new State(this.issuance, this.owner, this.faceValue, newMaturityDate);
|
||||
return new State(this.issuance, this.owner, this.faceValue, newMaturityDate, this.notary);
|
||||
}
|
||||
|
||||
public PartyAndReference getIssuance() {
|
||||
@ -74,6 +77,12 @@ public class JavaCommercialPaper implements Contract {
|
||||
return maturityDate;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public Party getNotary() {
|
||||
return notary;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public Contract getContract() {
|
||||
@ -91,8 +100,8 @@ public class JavaCommercialPaper implements Contract {
|
||||
if (issuance != null ? !issuance.equals(state.issuance) : state.issuance != null) return false;
|
||||
if (owner != null ? !owner.equals(state.owner) : state.owner != null) return false;
|
||||
if (faceValue != null ? !faceValue.equals(state.faceValue) : state.faceValue != null) return false;
|
||||
if (notary != null ? !notary.equals(state.notary) : state.notary != null) return false;
|
||||
return !(maturityDate != null ? !maturityDate.equals(state.maturityDate) : state.maturityDate != null);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -101,11 +110,12 @@ public class JavaCommercialPaper implements Contract {
|
||||
result = 31 * result + (owner != null ? owner.hashCode() : 0);
|
||||
result = 31 * result + (faceValue != null ? faceValue.hashCode() : 0);
|
||||
result = 31 * result + (maturityDate != null ? maturityDate.hashCode() : 0);
|
||||
result = 31 * result + (notary != null ? notary.hashCode() : 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
public State withoutOwner() {
|
||||
return new State(issuance, NullPublicKey.INSTANCE, faceValue, maturityDate);
|
||||
return new State(issuance, NullPublicKey.INSTANCE, faceValue, maturityDate, notary);
|
||||
}
|
||||
}
|
||||
|
||||
@ -158,7 +168,7 @@ public class JavaCommercialPaper implements Contract {
|
||||
throw new IllegalStateException("Failed Requirement: the face value is not zero");
|
||||
}
|
||||
|
||||
TimestampCommand timestampCommand = tx.getTimestampBy(DummyTimestampingAuthority.INSTANCE.getIdentity());
|
||||
TimestampCommand timestampCommand = tx.getTimestampByName("Notary Service");
|
||||
if (timestampCommand == null)
|
||||
throw new IllegalArgumentException("Failed Requirement: must be timestamped");
|
||||
|
||||
@ -188,7 +198,7 @@ public class JavaCommercialPaper implements Contract {
|
||||
!output.getMaturityDate().equals(input.getMaturityDate()))
|
||||
throw new IllegalStateException("Failed requirement: the output state is the same as the input state except for owner");
|
||||
} else if (cmd.getValue() instanceof JavaCommercialPaper.Commands.Redeem) {
|
||||
TimestampCommand timestampCommand = tx.getTimestampBy(DummyTimestampingAuthority.INSTANCE.getIdentity());
|
||||
TimestampCommand timestampCommand = tx.getTimestampByName("Notary Service");
|
||||
if (timestampCommand == null)
|
||||
throw new IllegalArgumentException("Failed Requirement: must be timestamped");
|
||||
Instant time = timestampCommand.getBefore();
|
||||
@ -215,8 +225,8 @@ public class JavaCommercialPaper implements Contract {
|
||||
return SecureHash.sha256("https://en.wikipedia.org/wiki/Commercial_paper");
|
||||
}
|
||||
|
||||
public TransactionBuilder generateIssue(@NotNull PartyAndReference issuance, @NotNull Amount faceValue, @Nullable Instant maturityDate) {
|
||||
State state = new State(issuance, issuance.getParty().getOwningKey(), faceValue, maturityDate);
|
||||
public TransactionBuilder generateIssue(@NotNull PartyAndReference issuance, @NotNull Amount faceValue, @Nullable Instant maturityDate, @NotNull Party notary) {
|
||||
State state = new State(issuance, issuance.getParty().getOwningKey(), faceValue, maturityDate, notary);
|
||||
return new TransactionBuilder().withItems(state, new Command(new Commands.Issue(), issuance.getParty().getOwningKey()));
|
||||
}
|
||||
|
||||
@ -228,7 +238,7 @@ public class JavaCommercialPaper implements Contract {
|
||||
|
||||
public void generateMove(TransactionBuilder tx, StateAndRef<State> paper, PublicKey newOwner) {
|
||||
tx.addInputState(paper.getRef());
|
||||
tx.addOutputState(new State(paper.getState().getIssuance(), newOwner, paper.getState().getFaceValue(), paper.getState().getMaturityDate()));
|
||||
tx.addOutputState(new State(paper.getState().getIssuance(), newOwner, paper.getState().getFaceValue(), paper.getState().getMaturityDate(), paper.getState().getNotary()));
|
||||
tx.addCommand(new Command(new Commands.Move(), paper.getState().getOwner()));
|
||||
}
|
||||
}
|
||||
|
@ -62,7 +62,9 @@ class Cash : Contract {
|
||||
override val amount: Amount,
|
||||
|
||||
/** There must be a MoveCommand signed by this key to claim the amount */
|
||||
override val owner: PublicKey
|
||||
override val owner: PublicKey,
|
||||
|
||||
override val notary: Party
|
||||
) : CommonCashState<Cash.IssuanceDefinition> {
|
||||
override val issuanceDef: Cash.IssuanceDefinition
|
||||
get() = Cash.IssuanceDefinition(deposit, amount.currency)
|
||||
@ -160,16 +162,16 @@ class Cash : Contract {
|
||||
/**
|
||||
* Puts together an issuance transaction from the given template, that starts out being owned by the given pubkey.
|
||||
*/
|
||||
fun generateIssue(tx: TransactionBuilder, issuanceDef: CashIssuanceDefinition, pennies: Long, owner: PublicKey)
|
||||
= generateIssue(tx, Amount(pennies, issuanceDef.currency), issuanceDef.deposit, owner)
|
||||
fun generateIssue(tx: TransactionBuilder, issuanceDef: CashIssuanceDefinition, pennies: Long, owner: PublicKey, notary: Party)
|
||||
= generateIssue(tx, Amount(pennies, issuanceDef.currency), issuanceDef.deposit, owner, notary)
|
||||
|
||||
/**
|
||||
* Puts together an issuance transaction for the specified amount that starts out being owned by the given pubkey.
|
||||
*/
|
||||
fun generateIssue(tx: TransactionBuilder, amount: Amount, at: PartyAndReference, owner: PublicKey) {
|
||||
fun generateIssue(tx: TransactionBuilder, amount: Amount, at: PartyAndReference, owner: PublicKey, notary: Party) {
|
||||
check(tx.inputStates().isEmpty())
|
||||
check(tx.outputStates().sumCashOrNull() == null)
|
||||
tx.addOutputState(Cash.State(at, amount, owner))
|
||||
tx.addOutputState(Cash.State(at, amount, owner, notary))
|
||||
tx.addCommand(Cash.Commands.Issue(), at.party.owningKey)
|
||||
}
|
||||
|
||||
@ -230,7 +232,7 @@ class Cash : Contract {
|
||||
val states = gathered.groupBy { it.state.deposit }.map {
|
||||
val (deposit, coins) = it
|
||||
val totalAmount = coins.map { it.state.amount }.sumOrThrow()
|
||||
State(deposit, totalAmount, to)
|
||||
State(deposit, totalAmount, to, coins.first().state.notary)
|
||||
}
|
||||
|
||||
val outputs = if (change.pennies > 0) {
|
||||
@ -241,7 +243,7 @@ class Cash : Contract {
|
||||
// Add a change output and adjust the last output downwards.
|
||||
states.subList(0, states.lastIndex) +
|
||||
states.last().let { it.copy(amount = it.amount - change) } +
|
||||
State(gathered.last().state.deposit, change, changeKey)
|
||||
State(gathered.last().state.deposit, change, changeKey, gathered.last().state.notary)
|
||||
} else states
|
||||
|
||||
for (state in gathered) tx.addInputState(state.ref)
|
||||
|
@ -41,7 +41,8 @@ class CommercialPaper : Contract {
|
||||
val issuance: PartyAndReference,
|
||||
override val owner: PublicKey,
|
||||
val faceValue: Amount,
|
||||
val maturityDate: Instant
|
||||
val maturityDate: Instant,
|
||||
override val notary: Party
|
||||
) : OwnableState, ICommercialPaperState {
|
||||
override val contract = CP_PROGRAM_ID
|
||||
|
||||
@ -76,7 +77,7 @@ class CommercialPaper : Contract {
|
||||
// Here, we match acceptable timestamp authorities by name. The list of acceptable TSAs (oracles) must be
|
||||
// hard coded into the contract because otherwise we could fail to gain consensus, if nodes disagree about
|
||||
// who or what is a trusted authority.
|
||||
val timestamp: TimestampCommand? = tx.commands.getTimestampByName("Mock Company 0", "Timestamping Service", "Bank A")
|
||||
val timestamp: TimestampCommand? = tx.commands.getTimestampByName("Mock Company 0", "Notary Service", "Bank A")
|
||||
|
||||
for ((inputs, outputs, key) in groups) {
|
||||
when (command.value) {
|
||||
@ -129,8 +130,8 @@ class CommercialPaper : Contract {
|
||||
* an existing transaction because you aren't able to issue multiple pieces of CP in a single transaction
|
||||
* at the moment: this restriction is not fundamental and may be lifted later.
|
||||
*/
|
||||
fun generateIssue(issuance: PartyAndReference, faceValue: Amount, maturityDate: Instant): TransactionBuilder {
|
||||
val state = State(issuance, issuance.party.owningKey, faceValue, maturityDate)
|
||||
fun generateIssue(issuance: PartyAndReference, faceValue: Amount, maturityDate: Instant, notary: Party): TransactionBuilder {
|
||||
val state = State(issuance, issuance.party.owningKey, faceValue, maturityDate, notary)
|
||||
return TransactionBuilder().withItems(state, Command(Commands.Issue(), issuance.party.owningKey))
|
||||
}
|
||||
|
||||
|
@ -2,7 +2,6 @@ package contracts
|
||||
|
||||
import core.*
|
||||
import core.crypto.SecureHash
|
||||
import core.node.services.DummyTimestampingAuthority
|
||||
import java.security.PublicKey
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
@ -46,6 +45,7 @@ class CrowdFund : Contract {
|
||||
|
||||
data class State(
|
||||
val campaign: Campaign,
|
||||
override val notary: Party,
|
||||
val closed: Boolean = false,
|
||||
val pledges: List<Pledge> = ArrayList()
|
||||
) : ContractState {
|
||||
@ -74,7 +74,7 @@ class CrowdFund : Contract {
|
||||
val outputCrowdFund: CrowdFund.State = tx.outStates.filterIsInstance<CrowdFund.State>().single()
|
||||
val outputCash: List<Cash.State> = tx.outStates.filterIsInstance<Cash.State>()
|
||||
|
||||
val time = tx.getTimestampBy(DummyTimestampingAuthority.identity)?.midpoint
|
||||
val time = tx.commands.getTimestampByName("Notary Service")?.midpoint
|
||||
if (time == null) throw IllegalArgumentException("must be timestamped")
|
||||
|
||||
when (command.value) {
|
||||
@ -136,9 +136,9 @@ class CrowdFund : Contract {
|
||||
* Returns a transaction that registers a crowd-funding campaing, owned by the issuing institution's key. Does not update
|
||||
* an existing transaction because it's not possible to register multiple campaigns in a single transaction
|
||||
*/
|
||||
fun generateRegister(owner: PartyAndReference, fundingTarget: Amount, fundingName: String, closingTime: Instant): TransactionBuilder {
|
||||
fun generateRegister(owner: PartyAndReference, fundingTarget: Amount, fundingName: String, closingTime: Instant, notary: Party): TransactionBuilder {
|
||||
val campaign = Campaign(owner = owner.party.owningKey, name = fundingName, target = fundingTarget, closingTime = closingTime)
|
||||
val state = State(campaign)
|
||||
val state = State(campaign, notary)
|
||||
return TransactionBuilder().withItems(state, Command(Commands.Register(), owner.party.owningKey))
|
||||
}
|
||||
|
||||
|
@ -8,7 +8,8 @@ import core.crypto.SecureHash
|
||||
val DUMMY_PROGRAM_ID = DummyContract()
|
||||
|
||||
class DummyContract : Contract {
|
||||
class State(val magicNumber: Int = 0) : ContractState {
|
||||
class State(val magicNumber: Int = 0,
|
||||
override val notary: Party) : ContractState {
|
||||
override val contract = DUMMY_PROGRAM_ID
|
||||
}
|
||||
|
||||
@ -23,8 +24,8 @@ class DummyContract : Contract {
|
||||
// The "empty contract"
|
||||
override val legalContractReference: SecureHash = SecureHash.sha256("")
|
||||
|
||||
fun generateInitial(owner: PartyAndReference, magicNumber: Int) : TransactionBuilder {
|
||||
val state = State(magicNumber)
|
||||
return TransactionBuilder().withItems( state, Command(Commands.Create(), owner.party.owningKey) )
|
||||
fun generateInitial(owner: PartyAndReference, magicNumber: Int, notary: Party): TransactionBuilder {
|
||||
val state = State(magicNumber, notary)
|
||||
return TransactionBuilder().withItems(state, Command(Commands.Create(), owner.party.owningKey))
|
||||
}
|
||||
}
|
@ -492,7 +492,7 @@ class InterestRateSwap() : Contract {
|
||||
val groups = tx.groupStates() { state: InterestRateSwap.State -> state.common.tradeID }
|
||||
|
||||
val command = tx.commands.requireSingleCommand<InterestRateSwap.Commands>()
|
||||
val time = tx.commands.getTimestampByName("Mock Company 0", "Timestamping Service", "Bank A")?.midpoint
|
||||
val time = tx.commands.getTimestampByName("Mock Company 0", "Notary Service", "Bank A")?.midpoint
|
||||
if (time == null) throw IllegalArgumentException("must be timestamped")
|
||||
|
||||
for ((inputs, outputs, key) in groups) {
|
||||
@ -586,7 +586,8 @@ class InterestRateSwap() : Contract {
|
||||
val fixedLeg: FixedLeg,
|
||||
val floatingLeg: FloatingLeg,
|
||||
val calculation: Calculation,
|
||||
val common: Common
|
||||
val common: Common,
|
||||
override val notary: Party
|
||||
) : FixableDealState {
|
||||
|
||||
override val contract = IRS_PROGRAM_ID
|
||||
@ -616,7 +617,7 @@ class InterestRateSwap() : Contract {
|
||||
}
|
||||
}
|
||||
|
||||
override fun generateAgreement(): TransactionBuilder = InterestRateSwap().generateAgreement(floatingLeg, fixedLeg, calculation, common)
|
||||
override fun generateAgreement(): TransactionBuilder = InterestRateSwap().generateAgreement(floatingLeg, fixedLeg, calculation, common, notary)
|
||||
|
||||
override fun generateFix(ptx: TransactionBuilder, oldStateRef: StateRef, fix: Fix) {
|
||||
InterestRateSwap().generateFix(ptx, StateAndRef(this, oldStateRef), Pair(fix.of.forDay, Rate(RatioUnit(fix.value))))
|
||||
@ -660,7 +661,8 @@ class InterestRateSwap() : Contract {
|
||||
* This generates the agreement state and also the schedules from the initial data.
|
||||
* Note: The day count, interest rate calculation etc are not finished yet, but they are demonstrable.
|
||||
*/
|
||||
fun generateAgreement(floatingLeg: FloatingLeg, fixedLeg: FixedLeg, calculation: Calculation, common: Common): TransactionBuilder {
|
||||
fun generateAgreement(floatingLeg: FloatingLeg, fixedLeg: FixedLeg, calculation: Calculation,
|
||||
common: Common, notary: Party): TransactionBuilder {
|
||||
|
||||
val fixedLegPaymentSchedule = HashMap<LocalDate, FixedRatePaymentEvent>()
|
||||
var dates = BusinessCalendar.createGenericSchedule(fixedLeg.effectiveDate, fixedLeg.paymentFrequency, fixedLeg.paymentCalendar, fixedLeg.rollConvention, endDate = fixedLeg.terminationDate)
|
||||
@ -709,7 +711,7 @@ class InterestRateSwap() : Contract {
|
||||
val newCalculation = Calculation(calculation.expression, floatingLegPaymentSchedule, fixedLegPaymentSchedule)
|
||||
|
||||
// Put all the above into a new State object.
|
||||
val state = State(fixedLeg, floatingLeg, newCalculation, common)
|
||||
val state = State(fixedLeg, floatingLeg, newCalculation, common, notary)
|
||||
return TransactionBuilder().withItems(state, Command(Commands.Agree(), listOf(state.floatingLeg.floatingRatePayer.owningKey, state.fixedLeg.fixedRatePayer.owningKey)))
|
||||
}
|
||||
|
||||
|
@ -23,10 +23,11 @@ interface NamedByHash {
|
||||
* updated, instead, any changes must generate a new successor state.
|
||||
*/
|
||||
interface ContractState {
|
||||
/**
|
||||
* Contract by which the state belongs
|
||||
*/
|
||||
/** Contract by which the state belongs */
|
||||
val contract: Contract
|
||||
|
||||
/** Identity of the notary that ensures this state is not used as an input to a transaction more than once */
|
||||
val notary: Party
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,15 +1,11 @@
|
||||
package core
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import core.crypto.DigitalSignature
|
||||
import core.crypto.SecureHash
|
||||
import core.crypto.signWithECDSA
|
||||
import core.node.services.TimestamperService
|
||||
import core.node.services.TimestampingError
|
||||
import core.serialization.serialize
|
||||
import java.security.KeyPair
|
||||
import java.security.PublicKey
|
||||
import java.time.Clock
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
@ -29,8 +25,8 @@ class TransactionBuilder(private val inputs: MutableList<StateRef> = arrayListOf
|
||||
|
||||
/**
|
||||
* Places a [TimestampCommand] in this transaction, removing any existing command if there is one.
|
||||
* To get the right signature from the timestamping service, use the [timestamp] method after building is
|
||||
* finished, or run use the [TimestampingProtocol] yourself.
|
||||
* The command requires a signature from the Notary service, which acts as a Timestamp Authority.
|
||||
* The signature can be obtained using [NotaryProtocol].
|
||||
*
|
||||
* The window of time in which the final timestamp may lie is defined as [time] +/- [timeTolerance].
|
||||
* If you want a non-symmetrical time window you must add the command via [addCommand] yourself. The tolerance
|
||||
@ -39,19 +35,19 @@ class TransactionBuilder(private val inputs: MutableList<StateRef> = arrayListOf
|
||||
* collaborating parties may therefore require a higher time tolerance than a transaction being built by a single
|
||||
* node.
|
||||
*/
|
||||
fun setTime(time: Instant, authenticatedBy: Party, timeTolerance: Duration) {
|
||||
fun setTime(time: Instant, authority: Party, timeTolerance: Duration) {
|
||||
check(currentSigs.isEmpty()) { "Cannot change timestamp after signing" }
|
||||
commands.removeAll { it.value is TimestampCommand }
|
||||
addCommand(TimestampCommand(time, timeTolerance), authenticatedBy.owningKey)
|
||||
addCommand(TimestampCommand(time, timeTolerance), authority.owningKey)
|
||||
}
|
||||
|
||||
/** A more convenient way to add items to this transaction that calls the add* methods for you based on type */
|
||||
fun withItems(vararg items: Any): TransactionBuilder {
|
||||
for (t in items) {
|
||||
when (t) {
|
||||
is StateRef -> inputs.add(t)
|
||||
is ContractState -> outputs.add(t)
|
||||
is Command -> commands.add(t)
|
||||
is StateRef -> addInputState(t)
|
||||
is ContractState -> addOutputState(t)
|
||||
is Command -> addCommand(t)
|
||||
else -> throw IllegalArgumentException("Wrong argument type: ${t.javaClass}")
|
||||
}
|
||||
}
|
||||
@ -63,7 +59,6 @@ class TransactionBuilder(private val inputs: MutableList<StateRef> = arrayListOf
|
||||
|
||||
fun signWith(key: KeyPair) {
|
||||
check(currentSigs.none { it.by == key.public }) { "This partial transaction was already signed by ${key.public}" }
|
||||
check(commands.count { it.signers.contains(key.public) } > 0) { "Trying to sign with a key that isn't in any command" }
|
||||
val data = toWireTransaction().serialize()
|
||||
addSignatureUnchecked(key.signWithECDSA(data.bits))
|
||||
}
|
||||
@ -87,7 +82,7 @@ class TransactionBuilder(private val inputs: MutableList<StateRef> = arrayListOf
|
||||
* @throws IllegalArgumentException if the signature key doesn't appear in any command.
|
||||
*/
|
||||
fun checkSignature(sig: DigitalSignature.WithKey) {
|
||||
require(commands.count { it.signers.contains(sig.by) } > 0) { "Signature key doesn't match any command" }
|
||||
require(commands.any { it.signers.contains(sig.by) }) { "Signature key doesn't match any command" }
|
||||
sig.verifyWithECDSA(toWireTransaction().serialized)
|
||||
}
|
||||
|
||||
@ -96,32 +91,6 @@ class TransactionBuilder(private val inputs: MutableList<StateRef> = arrayListOf
|
||||
currentSigs.add(sig)
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses the given timestamper service to request a signature over the WireTransaction be added. There must always be
|
||||
* at least one such signature, but others may be added as well. You may want to have multiple redundant timestamps
|
||||
* in the following cases:
|
||||
*
|
||||
* - Cross border contracts where local law says that only local timestamping authorities are acceptable.
|
||||
* - Backup in case a TSA's signing key is compromised.
|
||||
*
|
||||
* The signature of the trusted timestamper merely asserts that the time field of this transaction is valid.
|
||||
*/
|
||||
@Suspendable
|
||||
fun timestamp(timestamper: TimestamperService, clock: Clock = Clock.systemUTC()) {
|
||||
val t = time ?: throw IllegalStateException("Timestamping requested but no time was inserted into the transaction")
|
||||
|
||||
// Obviously this is just a hard-coded dummy value for now.
|
||||
val maxExpectedLatency = 5.seconds
|
||||
if (Duration.between(clock.instant(), t.before) > maxExpectedLatency)
|
||||
throw TimestampingError.NotOnTimeException()
|
||||
|
||||
// The timestamper may also throw NotOnTimeException if our clocks are desynchronised or if we are right on the
|
||||
// boundary of t.notAfter and network latency pushes us over the edge. By "synchronised" here we mean relative
|
||||
// to GPS time i.e. the United States Naval Observatory.
|
||||
val sig = timestamper.timestamp(toWireTransaction().serialize())
|
||||
addSignatureUnchecked(sig)
|
||||
}
|
||||
|
||||
fun toWireTransaction() = WireTransaction(ArrayList(inputs), ArrayList(attachments),
|
||||
ArrayList(outputs), ArrayList(commands))
|
||||
|
||||
@ -153,7 +122,6 @@ class TransactionBuilder(private val inputs: MutableList<StateRef> = arrayListOf
|
||||
|
||||
fun addCommand(arg: Command) {
|
||||
check(currentSigs.isEmpty())
|
||||
|
||||
// We should probably merge the lists of pubkeys for identical commands here.
|
||||
commands.add(arg)
|
||||
}
|
||||
|
@ -3,9 +3,6 @@ package core
|
||||
import core.crypto.SecureHash
|
||||
import java.util.*
|
||||
|
||||
class TransactionResolutionException(val hash: SecureHash) : Exception()
|
||||
class TransactionConflictException(val conflictRef: StateRef, val tx1: LedgerTransaction, val tx2: LedgerTransaction) : Exception()
|
||||
|
||||
// TODO: Consider moving this out of the core module and providing a different way for unit tests to test contracts.
|
||||
|
||||
/**
|
||||
@ -63,27 +60,37 @@ data class TransactionForVerification(val inStates: List<ContractState>,
|
||||
override fun equals(other: Any?) = other is TransactionForVerification && other.origHash == origHash
|
||||
|
||||
/**
|
||||
* Runs the contracts for this transaction.
|
||||
* Verifies that the transaction is valid:
|
||||
* - Checks that the input states and the timestamp point to the same Notary
|
||||
* - Runs the contracts for this transaction. If any contract fails to verify, the whole transaction
|
||||
* is considered to be invalid
|
||||
*
|
||||
* TODO: Move this out of the core data structure definitions, once unit tests are more cleanly separated.
|
||||
*
|
||||
* @throws TransactionVerificationException if a contract throws an exception, the original is in the cause field
|
||||
* @throws IllegalStateException if a state refers to an unknown contract.
|
||||
* @throws TransactionVerificationException if a contract throws an exception (the original is in the cause field)
|
||||
* or the transaction has references to more than one Notary
|
||||
*/
|
||||
@Throws(TransactionVerificationException::class, IllegalStateException::class)
|
||||
@Throws(TransactionVerificationException::class)
|
||||
fun verify() {
|
||||
// For each input and output state, locate the program to run. Then execute the verification function. If any
|
||||
// throws an exception, the entire transaction is invalid.
|
||||
val programs = (inStates.map { it.contract } + outStates.map { it.contract }).toSet()
|
||||
for (program in programs) {
|
||||
verifySingleNotary()
|
||||
val contracts = (inStates.map { it.contract } + outStates.map { it.contract }).toSet()
|
||||
for (contract in contracts) {
|
||||
try {
|
||||
program.verify(this)
|
||||
contract.verify(this)
|
||||
} catch(e: Throwable) {
|
||||
throw TransactionVerificationException(this, program, e)
|
||||
throw TransactionVerificationException.ContractRejection(this, contract, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun verifySingleNotary() {
|
||||
if (inStates.isEmpty()) return
|
||||
val notary = inStates.first().notary
|
||||
if (inStates.any { it.notary != notary }) throw TransactionVerificationException.MoreThanOneNotary(this)
|
||||
val timestampCmd = commands.singleOrNull { it.value is TimestampCommand } ?: return
|
||||
if (!timestampCmd.signers.contains(notary.owningKey)) throw TransactionVerificationException.MoreThanOneNotary(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Utilities for contract writers to incorporate into their logic.
|
||||
*/
|
||||
@ -99,6 +106,8 @@ data class TransactionForVerification(val inStates: List<ContractState>,
|
||||
|
||||
/** Simply calls [commands.getTimestampBy] as a shortcut to make code completion more intuitive. */
|
||||
fun getTimestampBy(timestampingAuthority: Party): TimestampCommand? = commands.getTimestampBy(timestampingAuthority)
|
||||
/** Simply calls [commands.getTimestampByName] as a shortcut to make code completion more intuitive. */
|
||||
fun getTimestampByName(vararg authorityName: String): TimestampCommand? = commands.getTimestampByName(*authorityName)
|
||||
|
||||
/**
|
||||
* Given a type and a function that returns a grouping key, associates inputs and outputs together so that they
|
||||
@ -152,5 +161,10 @@ data class TransactionForVerification(val inStates: List<ContractState>,
|
||||
}
|
||||
}
|
||||
|
||||
/** Thrown if a verification fails due to a contract rejection. */
|
||||
class TransactionVerificationException(val tx: TransactionForVerification, val contract: Contract, cause: Throwable?) : Exception(cause)
|
||||
class TransactionResolutionException(val hash: SecureHash) : Exception()
|
||||
class TransactionConflictException(val conflictRef: StateRef, val tx1: LedgerTransaction, val tx2: LedgerTransaction) : Exception()
|
||||
|
||||
sealed class TransactionVerificationException(val tx: TransactionForVerification, cause: Throwable?) : Exception(cause) {
|
||||
class ContractRejection(tx: TransactionForVerification, val contract: Contract, cause: Throwable?) : TransactionVerificationException(tx, cause)
|
||||
class MoreThanOneNotary(tx: TransactionForVerification) : TransactionVerificationException(tx, null)
|
||||
}
|
@ -107,7 +107,7 @@ data class SignedTransaction(val txBits: SerializedBytes<WireTransaction>,
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the signatures, deserialise the wire transaction and then check that the set of signatures found matches
|
||||
* Verify the signatures, deserialise the wire transaction and then check that the set of signatures found contains
|
||||
* the set of pubkeys in the commands. If any signatures are missing, either throws an exception (by default) or
|
||||
* returns the list of keys that have missing signatures, depending on the parameter.
|
||||
*
|
||||
@ -115,25 +115,34 @@ data class SignedTransaction(val txBits: SerializedBytes<WireTransaction>,
|
||||
*/
|
||||
fun verify(throwIfSignaturesAreMissing: Boolean = true): Set<PublicKey> {
|
||||
verifySignatures()
|
||||
// Verify that every command key was in the set that we just verified: there should be no commands that were
|
||||
// unverified.
|
||||
val cmdKeys = tx.commands.flatMap { it.signers }.toSet()
|
||||
val sigKeys = sigs.map { it.by }.toSet()
|
||||
if (sigKeys == cmdKeys)
|
||||
return emptySet()
|
||||
|
||||
val missing = cmdKeys - sigKeys
|
||||
if (throwIfSignaturesAreMissing)
|
||||
val missing = getMissingSignatures()
|
||||
if (missing.isNotEmpty() && throwIfSignaturesAreMissing)
|
||||
throw SignatureException("Missing signatures on transaction ${id.prefixChars()} for: ${missing.map { it.toStringShort() }}")
|
||||
else
|
||||
|
||||
return missing
|
||||
}
|
||||
|
||||
/** Returns the same transaction but with an additional (unchecked) signature */
|
||||
fun withAdditionalSignature(sig: DigitalSignature.WithKey) = copy(sigs = sigs + sig)
|
||||
fun withAdditionalSignature(sig: DigitalSignature.WithKey): SignedTransaction {
|
||||
// TODO: need to make sure the Notary signs last
|
||||
return copy(sigs = sigs + sig)
|
||||
}
|
||||
|
||||
/** Alias for [withAdditionalSignature] to let you use Kotlin operator overloading. */
|
||||
operator fun plus(sig: DigitalSignature.WithKey) = withAdditionalSignature(sig)
|
||||
|
||||
/**
|
||||
* Returns the set of missing signatures - a signature must be present for every command pub key
|
||||
* and the Notary (if it is specified)
|
||||
*/
|
||||
fun getMissingSignatures(): Set<PublicKey> {
|
||||
val requiredKeys = tx.commands.flatMap { it.signers }.toSet()
|
||||
val sigKeys = sigs.map { it.by }.toSet()
|
||||
|
||||
if (sigKeys.containsAll(requiredKeys)) return emptySet()
|
||||
return requiredKeys - sigKeys
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -57,4 +57,14 @@ interface ServiceHub {
|
||||
txStorage.putAll(txns)
|
||||
walletService.notifyAll(txs.map { it.tx })
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a [StateRef] loads the referenced transaction and looks up the specified output [ContractState]
|
||||
*
|
||||
* @throws TransactionResolutionException if the [StateRef] points to a non-existent transaction
|
||||
*/
|
||||
fun loadState(stateRef: StateRef): ContractState {
|
||||
val definingTx = storageService.validatedTransactions[stateRef.txhash] ?: throw TransactionResolutionException(stateRef.txhash)
|
||||
return definingTx.tx.outputs[stateRef.index]
|
||||
}
|
||||
}
|
@ -1,45 +0,0 @@
|
||||
package core.node.services
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import core.Party
|
||||
import core.WireTransaction
|
||||
import core.crypto.DigitalSignature
|
||||
import core.crypto.generateKeyPair
|
||||
import core.serialization.SerializedBytes
|
||||
|
||||
/**
|
||||
* Simple interface (for testing) to an abstract timestamping service. Note that this is not "timestamping" in the
|
||||
* blockchain sense of a total ordering of transactions, but rather, a signature from a well known/trusted timestamping
|
||||
* service over a transaction that indicates the timestamp in it is accurate. Such a signature may not always be
|
||||
* necessary: if there are multiple parties involved in a transaction then they can cross-check the timestamp
|
||||
* themselves.
|
||||
*/
|
||||
interface TimestamperService {
|
||||
object Type : ServiceType("corda.timestamper")
|
||||
@Suspendable
|
||||
fun timestamp(wtxBytes: SerializedBytes<WireTransaction>): DigitalSignature.LegallyIdentifiable
|
||||
|
||||
/** The name+pubkey that this timestamper will sign with. */
|
||||
val identity: Party
|
||||
}
|
||||
|
||||
// Smart contracts may wish to specify explicitly which timestamping authorities are trusted to assert the time.
|
||||
// We define a dummy authority here to allow to us to develop prototype contracts in the absence of a real authority.
|
||||
// The timestamper itself is implemented in the unit test part of the code (in TestUtils.kt).
|
||||
object DummyTimestampingAuthority {
|
||||
val key = generateKeyPair()
|
||||
val identity = Party("Mock Company 0", key.public)
|
||||
}
|
||||
|
||||
sealed class TimestampingError : Exception() {
|
||||
class RequiresExactlyOneCommand : TimestampingError()
|
||||
/**
|
||||
* Thrown if an attempt is made to timestamp a transaction using a trusted timestamper, but the time on the
|
||||
* transaction is too far in the past or future relative to the local clock and thus the timestamper would reject
|
||||
* it.
|
||||
*/
|
||||
class NotOnTimeException : TimestampingError()
|
||||
|
||||
/** Thrown if the command in the transaction doesn't list this timestamping authorities public key as a signer */
|
||||
class NotForMe : TimestampingError()
|
||||
}
|
@ -3,10 +3,17 @@ package core.node.subsystems
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import core.Contract
|
||||
import core.Party
|
||||
import core.crypto.SecureHash
|
||||
import core.messaging.MessagingService
|
||||
import core.node.NodeInfo
|
||||
import core.node.services.ServiceType
|
||||
import core.node.services.*
|
||||
import core.serialization.deserialize
|
||||
import core.serialization.serialize
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.security.PublicKey
|
||||
import java.security.SignatureException
|
||||
import java.util.*
|
||||
import javax.annotation.concurrent.ThreadSafe
|
||||
|
||||
/**
|
||||
* A network map contains lists of nodes on the network along with information about their identity keys, services
|
||||
@ -21,23 +28,19 @@ interface NetworkMapCache {
|
||||
|
||||
/** A list of nodes that advertise a network map service */
|
||||
val networkMapNodes: List<NodeInfo>
|
||||
/** A list of nodes that advertise a timestamping service */
|
||||
val timestampingNodes: List<NodeInfo>
|
||||
/** A list of nodes that advertise a notary service */
|
||||
val notaryNodes: List<NodeInfo>
|
||||
/** A list of nodes that advertise a rates oracle service */
|
||||
val ratesOracleNodes: List<NodeInfo>
|
||||
/** A list of all nodes the cache is aware of */
|
||||
val partyNodes: List<NodeInfo>
|
||||
/** A list of nodes that advertise a regulatory service. Identifying the correct regulator for a trade is outwith
|
||||
/**
|
||||
* A list of nodes that advertise a regulatory service. Identifying the correct regulator for a trade is outside
|
||||
* the scope of the network map service, and this is intended solely as a sanity check on configuration stored
|
||||
* elsewhere.
|
||||
*/
|
||||
val regulators: List<NodeInfo>
|
||||
|
||||
/**
|
||||
* Look up the node info for a party.
|
||||
*/
|
||||
fun nodeForPartyName(name: String): NodeInfo? = partyNodes.singleOrNull { it.identity.name == name }
|
||||
|
||||
/**
|
||||
* Get a copy of all nodes in the map.
|
||||
*/
|
||||
@ -55,6 +58,16 @@ interface NetworkMapCache {
|
||||
*/
|
||||
fun getRecommended(type: ServiceType, contract: Contract, vararg party: Party): NodeInfo?
|
||||
|
||||
/**
|
||||
* Look up the node info for a legal name.
|
||||
*/
|
||||
fun getNodeByLegalName(name: String): NodeInfo?
|
||||
|
||||
/**
|
||||
* Look up the node info for a public key.
|
||||
*/
|
||||
fun getNodeByPublicKey(publicKey: PublicKey): NodeInfo?
|
||||
|
||||
/**
|
||||
* Add a network map service; fetches a copy of the latest map from the service and subscribes to any further
|
||||
* updates.
|
||||
|
@ -1,53 +0,0 @@
|
||||
package protocols
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import core.WireTransaction
|
||||
import core.crypto.DigitalSignature
|
||||
import core.messaging.MessageRecipients
|
||||
import core.node.NodeInfo
|
||||
import core.protocols.ProtocolLogic
|
||||
import core.random63BitValue
|
||||
import core.serialization.SerializedBytes
|
||||
import core.utilities.ProgressTracker
|
||||
|
||||
/**
|
||||
* The TimestampingProtocol class is the client code that talks to a [NodeTimestamperService] on some remote node. It is a
|
||||
* [ProtocolLogic], meaning it can either be a sub-protocol of some other protocol, or be driven independently.
|
||||
*
|
||||
* If you are not yourself authoring a protocol and want to timestamp something, the [TimestampingProtocol.Client] class
|
||||
* implements the [TimestamperService] interface, meaning it can be passed to [TransactionBuilder.timestamp] to timestamp
|
||||
* the built transaction. Please be aware that this will block, meaning it should not be used on a thread that is handling
|
||||
* a network message: use it only from spare application threads that don't have to respond to anything.
|
||||
*/
|
||||
class TimestampingProtocol(private val node: NodeInfo,
|
||||
private val wtxBytes: SerializedBytes<WireTransaction>,
|
||||
override val progressTracker: ProgressTracker = TimestampingProtocol.tracker()) : ProtocolLogic<DigitalSignature.LegallyIdentifiable>() {
|
||||
|
||||
companion object {
|
||||
object REQUESTING : ProgressTracker.Step("Requesting signature by timestamping service")
|
||||
object VALIDATING : ProgressTracker.Step("Validating received signature from timestamping service")
|
||||
|
||||
fun tracker() = ProgressTracker(REQUESTING, VALIDATING)
|
||||
|
||||
val TOPIC = "platform.timestamping.request"
|
||||
}
|
||||
|
||||
|
||||
@Suspendable
|
||||
override fun call(): DigitalSignature.LegallyIdentifiable {
|
||||
progressTracker.currentStep = REQUESTING
|
||||
val sessionID = random63BitValue()
|
||||
val req = Request(wtxBytes, serviceHub.networkService.myAddress, sessionID)
|
||||
|
||||
val maybeSignature = sendAndReceive<DigitalSignature.LegallyIdentifiable>(TOPIC, node.address, 0, sessionID, req)
|
||||
|
||||
// Check that the timestamping authority gave us back a valid signature and didn't break somehow
|
||||
progressTracker.currentStep = VALIDATING
|
||||
maybeSignature.validate { sig ->
|
||||
sig.verifyWithECDSA(wtxBytes)
|
||||
return sig
|
||||
}
|
||||
}
|
||||
|
||||
class Request(val tx: SerializedBytes<WireTransaction>, replyTo: MessageRecipients, sessionID: Long) : AbstractRequestMessage(replyTo, sessionID)
|
||||
}
|
@ -41,7 +41,7 @@ object TwoPartyDealProtocol {
|
||||
val sessionID: Long
|
||||
)
|
||||
|
||||
class SignaturesFromPrimary(val timestampAuthoritySig: DigitalSignature.WithKey, val sellerSig: DigitalSignature.WithKey)
|
||||
class SignaturesFromPrimary(val sellerSig: DigitalSignature.WithKey, val notarySig: DigitalSignature.WithKey)
|
||||
|
||||
/**
|
||||
* Abstracted bilateral deal protocol participant that initiates communication/handshake.
|
||||
@ -53,21 +53,19 @@ object TwoPartyDealProtocol {
|
||||
val otherSide: SingleMessageRecipient,
|
||||
val otherSessionID: Long,
|
||||
val myKeyPair: KeyPair,
|
||||
val timestampingAuthority: NodeInfo,
|
||||
val notaryNode: NodeInfo,
|
||||
override val progressTracker: ProgressTracker = Primary.tracker()) : ProtocolLogic<SignedTransaction>() {
|
||||
|
||||
companion object {
|
||||
object AWAITING_PROPOSAL : ProgressTracker.Step("Handshaking and awaiting transaction proposal")
|
||||
object VERIFYING : ProgressTracker.Step("Verifying proposed transaction")
|
||||
object SIGNING : ProgressTracker.Step("Signing transaction")
|
||||
object TIMESTAMPING : ProgressTracker.Step("Timestamping transaction")
|
||||
object NOTARY : ProgressTracker.Step("Getting notary signature")
|
||||
object SENDING_SIGS : ProgressTracker.Step("Sending transaction signatures to other party")
|
||||
object RECORDING : ProgressTracker.Step("Recording completed transaction")
|
||||
object COPYING_TO_REGULATOR : ProgressTracker.Step("Copying regulator")
|
||||
|
||||
fun tracker() = ProgressTracker(AWAITING_PROPOSAL, VERIFYING, SIGNING, TIMESTAMPING, SENDING_SIGS, RECORDING, COPYING_TO_REGULATOR).apply {
|
||||
childrenFor[TIMESTAMPING] = TimestampingProtocol.tracker()
|
||||
}
|
||||
fun tracker() = ProgressTracker(AWAITING_PROPOSAL, VERIFYING, SIGNING, NOTARY, SENDING_SIGS, RECORDING, COPYING_TO_REGULATOR)
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
@ -93,7 +91,7 @@ object TwoPartyDealProtocol {
|
||||
|
||||
// Check that the tx proposed by the buyer is valid.
|
||||
val missingSigs = it.verify(throwIfSignaturesAreMissing = false)
|
||||
if (missingSigs != setOf(myKeyPair.public, timestampingAuthority.identity.owningKey))
|
||||
if (missingSigs != setOf(myKeyPair.public, notaryNode.identity.owningKey))
|
||||
throw SignatureException("The set of missing signatures is not as expected: $missingSigs")
|
||||
|
||||
val wtx: WireTransaction = it.tx
|
||||
@ -128,13 +126,13 @@ object TwoPartyDealProtocol {
|
||||
|
||||
@Suspendable
|
||||
override fun call(): SignedTransaction {
|
||||
val partialTX: SignedTransaction = verifyPartialTransaction(getPartialTransaction())
|
||||
val stx: SignedTransaction = verifyPartialTransaction(getPartialTransaction())
|
||||
|
||||
// These two steps could be done in parallel, in theory. Our framework doesn't support that yet though.
|
||||
val ourSignature = signWithOurKey(partialTX)
|
||||
val tsaSig = timestamp(partialTX)
|
||||
val ourSignature = signWithOurKey(stx)
|
||||
val notarySignature = getNotarySignature(stx)
|
||||
|
||||
val fullySigned = sendSignatures(partialTX, ourSignature, tsaSig)
|
||||
val fullySigned = sendSignatures(stx, ourSignature, notarySignature)
|
||||
|
||||
progressTracker.currentStep = RECORDING
|
||||
|
||||
@ -155,12 +153,11 @@ object TwoPartyDealProtocol {
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
private fun timestamp(partialTX: SignedTransaction): DigitalSignature.LegallyIdentifiable {
|
||||
progressTracker.currentStep = TIMESTAMPING
|
||||
return subProtocol(TimestampingProtocol(timestampingAuthority, partialTX.txBits, progressTracker.childrenFor[TIMESTAMPING]!!))
|
||||
private fun getNotarySignature(stx: SignedTransaction): DigitalSignature.LegallyIdentifiable {
|
||||
progressTracker.currentStep = NOTARY
|
||||
return subProtocol(NotaryProtocol(stx.tx))
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
open fun signWithOurKey(partialTX: SignedTransaction): DigitalSignature.WithKey {
|
||||
progressTracker.currentStep = SIGNING
|
||||
return myKeyPair.signWithECDSA(partialTX.txBits)
|
||||
@ -168,13 +165,13 @@ object TwoPartyDealProtocol {
|
||||
|
||||
@Suspendable
|
||||
private fun sendSignatures(partialTX: SignedTransaction, ourSignature: DigitalSignature.WithKey,
|
||||
tsaSig: DigitalSignature.LegallyIdentifiable): SignedTransaction {
|
||||
notarySignature: DigitalSignature.LegallyIdentifiable): SignedTransaction {
|
||||
progressTracker.currentStep = SENDING_SIGS
|
||||
val fullySigned = partialTX + tsaSig + ourSignature
|
||||
val fullySigned = partialTX + ourSignature + notarySignature
|
||||
|
||||
logger.trace { "Built finished transaction, sending back to other party!" }
|
||||
|
||||
send(DEAL_TOPIC, otherSide, otherSessionID, SignaturesFromPrimary(tsaSig, ourSignature))
|
||||
send(DEAL_TOPIC, otherSide, otherSessionID, SignaturesFromPrimary(ourSignature, notarySignature))
|
||||
return fullySigned
|
||||
}
|
||||
}
|
||||
@ -187,7 +184,7 @@ object TwoPartyDealProtocol {
|
||||
* and helper methods etc.
|
||||
*/
|
||||
abstract class Secondary<U>(val otherSide: SingleMessageRecipient,
|
||||
val timestampingAuthority: Party,
|
||||
val notary: Party,
|
||||
val sessionID: Long,
|
||||
override val progressTracker: ProgressTracker = Secondary.tracker()) : ProtocolLogic<SignedTransaction>() {
|
||||
|
||||
@ -212,7 +209,7 @@ object TwoPartyDealProtocol {
|
||||
val signatures = swapSignaturesWithPrimary(stx, handshake.sessionID)
|
||||
|
||||
logger.trace { "Got signatures from other party, verifying ... " }
|
||||
val fullySigned = stx + signatures.timestampAuthoritySig + signatures.sellerSig
|
||||
val fullySigned = stx + signatures.sellerSig + signatures.notarySig
|
||||
fullySigned.verify()
|
||||
|
||||
logger.trace { "Signatures received are valid. Deal transaction complete! :-)" }
|
||||
@ -264,22 +261,20 @@ object TwoPartyDealProtocol {
|
||||
* One side of the protocol for inserting a pre-agreed deal.
|
||||
*/
|
||||
open class Instigator<T : DealState>(otherSide: SingleMessageRecipient,
|
||||
timestampingAuthority: NodeInfo,
|
||||
notaryNode: NodeInfo,
|
||||
dealBeingOffered: T,
|
||||
myKeyPair: KeyPair,
|
||||
buyerSessionID: Long,
|
||||
override val progressTracker: ProgressTracker = Primary.tracker()) : Primary<T>(dealBeingOffered, otherSide, buyerSessionID, myKeyPair, timestampingAuthority)
|
||||
override val progressTracker: ProgressTracker = Primary.tracker()) : Primary<T>(dealBeingOffered, otherSide, buyerSessionID, myKeyPair, notaryNode)
|
||||
|
||||
/**
|
||||
* One side of the protocol for inserting a pre-agreed deal.
|
||||
*/
|
||||
open class Acceptor<T : DealState>(otherSide: SingleMessageRecipient,
|
||||
timestampingAuthority: Party,
|
||||
notary: Party,
|
||||
val dealToBuy: T,
|
||||
sessionID: Long,
|
||||
override val progressTracker: ProgressTracker = Secondary.tracker()) : Secondary<T>(otherSide, timestampingAuthority, sessionID) {
|
||||
|
||||
@Suspendable
|
||||
override val progressTracker: ProgressTracker = Secondary.tracker()) : Secondary<T>(otherSide, notary, sessionID) {
|
||||
override fun validateHandshake(handshake: Handshake<T>): Handshake<T> {
|
||||
with(handshake) {
|
||||
// What is the seller trying to sell us?
|
||||
@ -307,13 +302,12 @@ object TwoPartyDealProtocol {
|
||||
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
override fun assembleSharedTX(handshake: Handshake<T>): Pair<TransactionBuilder, List<PublicKey>> {
|
||||
val ptx = handshake.payload.generateAgreement()
|
||||
|
||||
// And add a request for timestamping: it may be that none of the contracts need this! But it can't hurt
|
||||
// to have one.
|
||||
ptx.setTime(serviceHub.clock.instant(), timestampingAuthority, 30.seconds)
|
||||
ptx.setTime(serviceHub.clock.instant(), notary, 30.seconds)
|
||||
return Pair(ptx, arrayListOf(handshake.payload.parties.single { it.name == serviceHub.storageService.myLegalIdentity.name }.owningKey))
|
||||
}
|
||||
|
||||
@ -327,10 +321,10 @@ object TwoPartyDealProtocol {
|
||||
* who does what in the protocol.
|
||||
*/
|
||||
open class Fixer<T : FixableDealState>(otherSide: SingleMessageRecipient,
|
||||
timestampingAuthority: Party,
|
||||
notary: Party,
|
||||
val dealToFix: StateAndRef<T>,
|
||||
sessionID: Long,
|
||||
val replacementProgressTracker: ProgressTracker? = null) : Secondary<StateRef>(otherSide, timestampingAuthority, sessionID) {
|
||||
val replacementProgressTracker: ProgressTracker? = null) : Secondary<StateRef>(otherSide, notary, sessionID) {
|
||||
private val ratesFixTracker = RatesFixProtocol.tracker(dealToFix.state.nextFixingOf()!!.name)
|
||||
|
||||
override val progressTracker: ProgressTracker = replacementProgressTracker ?: createTracker()
|
||||
@ -339,7 +333,6 @@ object TwoPartyDealProtocol {
|
||||
childrenFor[SIGNING] = ratesFixTracker
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
override fun validateHandshake(handshake: Handshake<StateRef>): Handshake<StateRef> {
|
||||
with(handshake) {
|
||||
logger.trace { "Got fixing request for: ${dealToFix.state}" }
|
||||
@ -377,7 +370,7 @@ object TwoPartyDealProtocol {
|
||||
|
||||
// And add a request for timestamping: it may be that none of the contracts need this! But it can't hurt
|
||||
// to have one.
|
||||
ptx.setTime(serviceHub.clock.instant(), timestampingAuthority, 30.seconds)
|
||||
ptx.setTime(serviceHub.clock.instant(), notary, 30.seconds)
|
||||
}
|
||||
}
|
||||
subProtocol(addFixing)
|
||||
@ -395,9 +388,9 @@ object TwoPartyDealProtocol {
|
||||
*/
|
||||
open class Floater<T : FixableDealState>(otherSide: SingleMessageRecipient,
|
||||
otherSessionID: Long,
|
||||
timestampingAuthority: NodeInfo,
|
||||
notary: NodeInfo,
|
||||
dealToFix: StateAndRef<T>,
|
||||
myKeyPair: KeyPair,
|
||||
val sessionID: Long,
|
||||
override val progressTracker: ProgressTracker = Primary.tracker()) : Primary<StateRef>(dealToFix.ref, otherSide, otherSessionID, myKeyPair, timestampingAuthority)
|
||||
override val progressTracker: ProgressTracker = Primary.tracker()) : Primary<StateRef>(dealToFix.ref, otherSide, otherSessionID, myKeyPair, notary)
|
||||
}
|
@ -99,5 +99,6 @@
|
||||
"dailyInterestAmount": "(CashAmount * InterestRate ) / (fixedLeg.notional.currency.currencyCode.equals('GBP')) ? 365 : 360",
|
||||
"tradeID": "tradeXXX",
|
||||
"hashLegalDocs": "put hash here"
|
||||
}
|
||||
},
|
||||
"notary": "Bank A"
|
||||
}
|
@ -39,7 +39,7 @@ elif [[ "$mode" == "nodeB" ]]; then
|
||||
do
|
||||
build/install/r3prototyping/bin/irsdemo --dir=nodeB --network-address=localhost:31340 \
|
||||
--fake-trade-with-address=localhost --fake-trade-with-identity=nodeA/identity-public \
|
||||
-network-map-identity-file=nodeA/identity-public --network-map-address=localhost &
|
||||
--network-map-identity-file=nodeA/identity-public --network-map-address=localhost &
|
||||
while ! curl -F rates=@scripts/example.rates.txt http://localhost:31341/upload/interest-rates; do
|
||||
echo "Retry to upload interest rates to oracle after 5 seconds"
|
||||
sleep 5
|
||||
|
@ -7,15 +7,16 @@ import com.google.common.util.concurrent.ListenableFuture
|
||||
import com.google.common.util.concurrent.MoreExecutors
|
||||
import com.google.common.util.concurrent.SettableFuture
|
||||
import core.Party
|
||||
import core.crypto.generateKeyPair
|
||||
import core.messaging.MessagingService
|
||||
import core.messaging.StateMachineManager
|
||||
import core.messaging.runOnNextMessage
|
||||
import core.node.services.*
|
||||
import core.node.subsystems.*
|
||||
import core.node.storage.CheckpointStorage
|
||||
import core.node.storage.PerFileCheckpointStorage
|
||||
import core.node.subsystems.*
|
||||
import core.random63BitValue
|
||||
import core.seconds
|
||||
import core.serialization.deserialize
|
||||
import core.serialization.serialize
|
||||
import core.utilities.AddOrRemove
|
||||
@ -85,7 +86,7 @@ abstract class AbstractNode(val dir: Path, val configuration: NodeConfiguration,
|
||||
lateinit var wallet: WalletService
|
||||
lateinit var keyManagement: E2ETestKeyManagementService
|
||||
var inNodeNetworkMapService: NetworkMapService? = null
|
||||
var inNodeTimestampingService: NodeTimestamperService? = null
|
||||
var inNodeNotaryService: NotaryService? = null
|
||||
lateinit var identity: IdentityService
|
||||
lateinit var net: MessagingService
|
||||
lateinit var api: APIServer
|
||||
@ -103,7 +104,7 @@ abstract class AbstractNode(val dir: Path, val configuration: NodeConfiguration,
|
||||
|
||||
// Build services we're advertising
|
||||
if (NetworkMapService.Type in info.advertisedServices) makeNetworkMapService()
|
||||
if (TimestamperService.Type in info.advertisedServices) makeTimestampingService()
|
||||
if (NotaryService.Type in info.advertisedServices) makeNotaryService()
|
||||
|
||||
identity = makeIdentityService()
|
||||
|
||||
@ -161,8 +162,10 @@ abstract class AbstractNode(val dir: Path, val configuration: NodeConfiguration,
|
||||
inNodeNetworkMapService = InMemoryNetworkMapService(net, reg, services.networkMapCache)
|
||||
}
|
||||
|
||||
open protected fun makeTimestampingService() {
|
||||
inNodeTimestampingService = NodeTimestamperService(net, storage.myLegalIdentity, storage.myLegalIdentityKey, platformClock)
|
||||
open protected fun makeNotaryService() {
|
||||
val uniquenessProvider = InMemoryUniquenessProvider()
|
||||
val timestampChecker = TimestampChecker(platformClock, 30.seconds)
|
||||
inNodeNotaryService = NotaryService(net, storage.myLegalIdentity, storage.myLegalIdentityKey, uniquenessProvider, timestampChecker)
|
||||
}
|
||||
|
||||
lateinit var interestRatesService: NodeInterestRates.Service
|
||||
@ -243,7 +246,9 @@ abstract class AbstractNode(val dir: Path, val configuration: NodeConfiguration,
|
||||
}
|
||||
}
|
||||
|
||||
private fun makeAttachmentStorage(dir: Path): NodeAttachmentService {
|
||||
protected open fun generateKeyPair() = core.crypto.generateKeyPair()
|
||||
|
||||
protected fun makeAttachmentStorage(dir: Path): NodeAttachmentService {
|
||||
val attachmentsDir = dir.resolve("attachments")
|
||||
try {
|
||||
Files.createDirectory(attachmentsDir)
|
||||
|
@ -1,78 +0,0 @@
|
||||
package core.node.services
|
||||
|
||||
import co.paralleluniverse.common.util.VisibleForTesting
|
||||
import core.Party
|
||||
import core.TimestampCommand
|
||||
import core.crypto.DigitalSignature
|
||||
import core.crypto.signWithECDSA
|
||||
import core.messaging.MessagingService
|
||||
import core.seconds
|
||||
import core.serialization.deserialize
|
||||
import core.until
|
||||
import org.slf4j.LoggerFactory
|
||||
import protocols.TimestampingProtocol
|
||||
import java.security.KeyPair
|
||||
import java.time.Clock
|
||||
import java.time.Duration
|
||||
import javax.annotation.concurrent.ThreadSafe
|
||||
|
||||
/**
|
||||
* This class implements the server side of the timestamping protocol, using the local clock. A future version might
|
||||
* add features like checking against other NTP servers to make sure the clock hasn't drifted by too much.
|
||||
*
|
||||
* See the doc site to learn more about timestamping authorities (nodes) and the role they play in the data model.
|
||||
*/
|
||||
@ThreadSafe
|
||||
class NodeTimestamperService(net: MessagingService,
|
||||
val identity: Party,
|
||||
val signingKey: KeyPair,
|
||||
val clock: Clock = Clock.systemDefaultZone(),
|
||||
val tolerance: Duration = 30.seconds) : AbstractNodeService(net) {
|
||||
companion object {
|
||||
private val logger = LoggerFactory.getLogger(NodeTimestamperService::class.java)
|
||||
}
|
||||
|
||||
init {
|
||||
require(identity.owningKey == signingKey.public)
|
||||
addMessageHandler(TimestampingProtocol.TOPIC,
|
||||
{ req: TimestampingProtocol.Request -> processRequest(req) },
|
||||
{ message, e ->
|
||||
if (e is TimestampingError) {
|
||||
logger.warn("Failure during timestamping request due to bad request: ${e.javaClass.name}")
|
||||
} else {
|
||||
logger.error("Exception during timestamping", e)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
fun processRequest(req: TimestampingProtocol.Request): DigitalSignature.LegallyIdentifiable {
|
||||
// We don't bother verifying signatures anything about the transaction here: we simply don't need to see anything
|
||||
// except the relevant command, and a future privacy upgrade should ensure we only get a torn-off command
|
||||
// rather than the full transaction.
|
||||
val tx = req.tx.deserialize()
|
||||
val cmd = tx.commands.filter { it.value is TimestampCommand }.singleOrNull()
|
||||
if (cmd == null)
|
||||
throw TimestampingError.RequiresExactlyOneCommand()
|
||||
if (!cmd.signers.contains(identity.owningKey))
|
||||
throw TimestampingError.NotForMe()
|
||||
val tsCommand = cmd.value as TimestampCommand
|
||||
|
||||
val before = tsCommand.before
|
||||
val after = tsCommand.after
|
||||
|
||||
val now = clock.instant()
|
||||
|
||||
// We don't need to test for (before == null && after == null) or backwards bounds because the TimestampCommand
|
||||
// constructor already checks that.
|
||||
|
||||
if (before != null && before until now > tolerance)
|
||||
throw TimestampingError.NotOnTimeException()
|
||||
if (after != null && now until after > tolerance)
|
||||
throw TimestampingError.NotOnTimeException()
|
||||
|
||||
return signingKey.signWithECDSA(req.tx.bits, identity)
|
||||
}
|
||||
}
|
||||
|
115
src/main/kotlin/core/node/services/NotaryService.kt
Normal file
115
src/main/kotlin/core/node/services/NotaryService.kt
Normal file
@ -0,0 +1,115 @@
|
||||
package core.node.services
|
||||
|
||||
import core.Party
|
||||
import core.TimestampCommand
|
||||
import core.WireTransaction
|
||||
import core.crypto.DigitalSignature
|
||||
import core.crypto.SignedData
|
||||
import core.crypto.signWithECDSA
|
||||
import core.messaging.MessagingService
|
||||
import core.serialization.SerializedBytes
|
||||
import core.serialization.deserialize
|
||||
import core.serialization.serialize
|
||||
import core.utilities.loggerFor
|
||||
import protocols.NotaryProtocol
|
||||
import java.security.KeyPair
|
||||
|
||||
/**
|
||||
* A Notary service acts as the final signer of a transaction ensuring two things:
|
||||
* - The (optional) timestamp of the transaction is valid
|
||||
* - None of the referenced input states have previously been consumed by a transaction signed by this Notary
|
||||
*
|
||||
* A transaction has to be signed by a Notary to be considered valid (except for output-only transactions w/o a timestamp)
|
||||
*/
|
||||
class NotaryService(net: MessagingService,
|
||||
val identity: Party,
|
||||
val signingKey: KeyPair,
|
||||
val uniquenessProvider: UniquenessProvider,
|
||||
val timestampChecker: TimestampChecker) : AbstractNodeService(net) {
|
||||
object Type : ServiceType("corda.notary")
|
||||
|
||||
private val logger = loggerFor<NotaryService>()
|
||||
|
||||
init {
|
||||
check(identity.owningKey == signingKey.public)
|
||||
addMessageHandler(NotaryProtocol.TOPIC,
|
||||
{ req: NotaryProtocol.SignRequest -> processRequest(req.txBits, req.callerIdentity) },
|
||||
{ message, e -> logger.error("Exception during notary service request processing", e) }
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks that the timestamp command is valid (if present) and commits the input state, or returns a conflict
|
||||
* if any of the input states have been previously committed
|
||||
*
|
||||
* Note that the transaction is not checked for contract-validity, as that would require fully resolving it
|
||||
* into a [TransactionForVerification], for which the caller would have to reveal the whole transaction history chain.
|
||||
* As a result, the Notary _will commit invalid transactions_ as well, but as it also records the identity of
|
||||
* the caller, it is possible to raise a dispute and verify the validity of the transaction and subsequently
|
||||
* undo the commit of the input states (the exact mechanism still needs to be worked out)
|
||||
*
|
||||
* TODO: the notary service should only be able to see timestamp commands and inputs
|
||||
*/
|
||||
fun processRequest(txBits: SerializedBytes<WireTransaction>, reqIdentity: Party): Result {
|
||||
val wtx = txBits.deserialize()
|
||||
try {
|
||||
validateTimestamp(wtx)
|
||||
commitInputStates(wtx, reqIdentity)
|
||||
} catch(e: NotaryException) {
|
||||
return Result.withError(e.error)
|
||||
}
|
||||
|
||||
val sig = sign(txBits)
|
||||
return Result.noError(sig)
|
||||
}
|
||||
|
||||
private fun validateTimestamp(tx: WireTransaction) {
|
||||
// Need to have at most one timestamp command
|
||||
val timestampCmds = tx.commands.filter { it.value is TimestampCommand }
|
||||
if (timestampCmds.count() > 1)
|
||||
throw NotaryException(NotaryError.MoreThanOneTimestamp())
|
||||
|
||||
val timestampCmd = timestampCmds.singleOrNull() ?: return
|
||||
if (!timestampCmd.signers.contains(identity.owningKey))
|
||||
throw NotaryException(NotaryError.NotForMe())
|
||||
if (!timestampChecker.isValid(timestampCmd.value as TimestampCommand))
|
||||
throw NotaryException(NotaryError.TimestampInvalid())
|
||||
}
|
||||
|
||||
private fun commitInputStates(tx: WireTransaction, reqIdentity: Party) {
|
||||
try {
|
||||
uniquenessProvider.commit(tx, reqIdentity)
|
||||
} catch (e: UniquenessException) {
|
||||
val conflictData = e.error.serialize()
|
||||
val signedConflict = SignedData(conflictData, sign(conflictData))
|
||||
throw NotaryException(NotaryError.Conflict(tx, signedConflict))
|
||||
}
|
||||
}
|
||||
|
||||
private fun <T : Any> sign(bits: SerializedBytes<T>): DigitalSignature.LegallyIdentifiable {
|
||||
return signingKey.signWithECDSA(bits, identity)
|
||||
}
|
||||
|
||||
data class Result private constructor(val sig: DigitalSignature.LegallyIdentifiable?, val error: NotaryError?) {
|
||||
companion object {
|
||||
fun withError(error: NotaryError) = Result(null, error)
|
||||
fun noError(sig: DigitalSignature.LegallyIdentifiable) = Result(sig, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class NotaryException(val error: NotaryError) : Exception()
|
||||
|
||||
sealed class NotaryError {
|
||||
class Conflict(val tx: WireTransaction, val conflict: SignedData<UniquenessProvider.Conflict>) : NotaryError() {
|
||||
override fun toString() = "One or more input states for transaction ${tx.id} have been used in another transaction"
|
||||
}
|
||||
|
||||
class MoreThanOneTimestamp : NotaryError()
|
||||
|
||||
/** Thrown if the timestamp command in the transaction doesn't list this Notary as a signer */
|
||||
class NotForMe : NotaryError()
|
||||
|
||||
/** Thrown if the time specified in the timestamp command is outside the allowed tolerance */
|
||||
class TimestampInvalid : NotaryError()
|
||||
}
|
26
src/main/kotlin/core/node/services/TimestampChecker.kt
Normal file
26
src/main/kotlin/core/node/services/TimestampChecker.kt
Normal file
@ -0,0 +1,26 @@
|
||||
package core.node.services
|
||||
|
||||
import core.TimestampCommand
|
||||
import core.seconds
|
||||
import core.until
|
||||
import java.time.Clock
|
||||
import java.time.Duration
|
||||
|
||||
/**
|
||||
* Checks if the given timestamp falls within the allowed tolerance interval
|
||||
*/
|
||||
class TimestampChecker(val clock: Clock = Clock.systemDefaultZone(),
|
||||
val tolerance: Duration = 30.seconds) {
|
||||
fun isValid(timestampCommand: TimestampCommand): Boolean {
|
||||
val before = timestampCommand.before
|
||||
val after = timestampCommand.after
|
||||
|
||||
val now = clock.instant()
|
||||
|
||||
// We don't need to test for (before == null && after == null) or backwards bounds because the TimestampCommand
|
||||
// constructor already checks that.
|
||||
if (before != null && before until now > tolerance) return false
|
||||
if (after != null && now until after > tolerance) return false
|
||||
return true
|
||||
}
|
||||
}
|
64
src/main/kotlin/core/node/services/UniquenessProvider.kt
Normal file
64
src/main/kotlin/core/node/services/UniquenessProvider.kt
Normal file
@ -0,0 +1,64 @@
|
||||
package core.node.services
|
||||
|
||||
import core.Party
|
||||
import core.StateRef
|
||||
import core.ThreadBox
|
||||
import core.WireTransaction
|
||||
import core.crypto.SecureHash
|
||||
import java.util.*
|
||||
import javax.annotation.concurrent.ThreadSafe
|
||||
|
||||
/**
|
||||
* A service that records input states of the given transaction and provides conflict information
|
||||
* if any of the inputs have already been used in another transaction
|
||||
*/
|
||||
interface UniquenessProvider {
|
||||
/** Commits all input states of the given transaction */
|
||||
fun commit(tx: WireTransaction, callerIdentity: Party)
|
||||
|
||||
/** Specifies the consuming transaction for every conflicting state */
|
||||
data class Conflict(val stateHistory: Map<StateRef, ConsumingTx>)
|
||||
|
||||
/**
|
||||
* Specifies the transaction id, the position of the consumed state in the inputs, and
|
||||
* the caller identity requesting the commit
|
||||
*
|
||||
* TODO: need to replace the transaction id with some other identifier to avoid privacy leaks
|
||||
* e.g after buying an asset with some cash a party can construct a fake transaction with
|
||||
* the cash outputs as inputs and find out if and where the other party had spent it
|
||||
*/
|
||||
data class ConsumingTx(val id: SecureHash, val inputIndex: Int, val requestingParty: Party)
|
||||
}
|
||||
|
||||
class UniquenessException(val error: UniquenessProvider.Conflict) : Exception()
|
||||
|
||||
/** A dummy Uniqueness provider that stores the whole history of consumed states in memory */
|
||||
@ThreadSafe
|
||||
class InMemoryUniquenessProvider() : UniquenessProvider {
|
||||
/**
|
||||
* For each state store the consuming transaction id along with the identity of the party
|
||||
* requesting validation of the transaction
|
||||
*/
|
||||
private val committedStates = ThreadBox(HashMap<StateRef, UniquenessProvider.ConsumingTx>())
|
||||
|
||||
// TODO: the uniqueness provider shouldn't be able to see all tx outputs and commands
|
||||
override fun commit(tx: WireTransaction, callerIdentity: Party) {
|
||||
val inputStates = tx.inputs
|
||||
committedStates.locked {
|
||||
val conflictingStates = LinkedHashMap<StateRef, UniquenessProvider.ConsumingTx>()
|
||||
for (inputState in inputStates) {
|
||||
val consumingTx = get(inputState)
|
||||
if (consumingTx != null) conflictingStates[inputState] = consumingTx
|
||||
}
|
||||
if (conflictingStates.isNotEmpty()) {
|
||||
val conflict = UniquenessProvider.Conflict(conflictingStates)
|
||||
throw UniquenessException(conflict)
|
||||
} else {
|
||||
inputStates.forEachIndexed { i, stateRef ->
|
||||
put(stateRef, UniquenessProvider.ConsumingTx(tx.id, i, callerIdentity))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
@ -15,6 +15,7 @@ import core.random63BitValue
|
||||
import core.serialization.deserialize
|
||||
import core.serialization.serialize
|
||||
import core.utilities.AddOrRemove
|
||||
import java.security.PublicKey
|
||||
import java.security.SignatureException
|
||||
import java.util.*
|
||||
import javax.annotation.concurrent.ThreadSafe
|
||||
@ -28,8 +29,8 @@ open class InMemoryNetworkMapCache() : NetworkMapCache {
|
||||
get() = get(NetworkMapService.Type)
|
||||
override val regulators: List<NodeInfo>
|
||||
get() = get(RegulatorService.Type)
|
||||
override val timestampingNodes: List<NodeInfo>
|
||||
get() = get(TimestamperService.Type)
|
||||
override val notaryNodes: List<NodeInfo>
|
||||
get() = get(NotaryService.Type)
|
||||
override val ratesOracleNodes: List<NodeInfo>
|
||||
get() = get(NodeInterestRates.Type)
|
||||
override val partyNodes: List<NodeInfo>
|
||||
@ -41,6 +42,8 @@ open class InMemoryNetworkMapCache() : NetworkMapCache {
|
||||
override fun get() = registeredNodes.map { it.value }
|
||||
override fun get(serviceType: ServiceType) = registeredNodes.filterValues { it.advertisedServices.contains(serviceType) }.map { it.value }
|
||||
override fun getRecommended(type: ServiceType, contract: Contract, vararg party: Party): NodeInfo? = get(type).firstOrNull()
|
||||
override fun getNodeByLegalName(name: String) = get().singleOrNull { it.identity.name == name }
|
||||
override fun getNodeByPublicKey(publicKey: PublicKey) = get().singleOrNull { it.identity.owningKey == publicKey }
|
||||
|
||||
override fun addMapService(net: MessagingService, service: NodeInfo, subscribe: Boolean,
|
||||
ifChangedSinceVer: Int?): ListenableFuture<Unit> {
|
||||
|
@ -139,8 +139,8 @@ class NodeWalletService(private val services: ServiceHub) : WalletService {
|
||||
*
|
||||
* TODO: Move this out of NodeWalletService
|
||||
*/
|
||||
fun fillWithSomeTestCash(howMuch: Amount, atLeastThisManyStates: Int = 3, atMostThisManyStates: Int = 10,
|
||||
rng: Random = Random()) {
|
||||
fun fillWithSomeTestCash(notary: Party, howMuch: Amount, atLeastThisManyStates: Int = 3,
|
||||
atMostThisManyStates: Int = 10, rng: Random = Random()) {
|
||||
val amounts = calculateRandomlySizedAmounts(howMuch, atLeastThisManyStates, atMostThisManyStates, rng)
|
||||
|
||||
val myIdentity = services.storageService.myLegalIdentity
|
||||
@ -155,7 +155,7 @@ class NodeWalletService(private val services: ServiceHub) : WalletService {
|
||||
|
||||
val issuance = TransactionBuilder()
|
||||
val freshKey = services.keyManagementService.freshKey()
|
||||
cash.generateIssue(issuance, Amount(pennies, howMuch.currency), depositRef, freshKey.public)
|
||||
cash.generateIssue(issuance, Amount(pennies, howMuch.currency), depositRef, freshKey.public, notary)
|
||||
issuance.signWith(myKey)
|
||||
|
||||
return@map issuance.toSignedTransaction(true)
|
||||
|
@ -7,7 +7,6 @@ import com.google.common.util.concurrent.SettableFuture
|
||||
import contracts.InterestRateSwap
|
||||
import core.*
|
||||
import core.crypto.SecureHash
|
||||
import core.testing.MockIdentityService
|
||||
import core.node.subsystems.linearHeadsOfType
|
||||
import core.utilities.JsonSupport
|
||||
import protocols.TwoPartyDealProtocol
|
||||
@ -64,9 +63,9 @@ class IRSSimulation(runAsync: Boolean, latencyInjector: InMemoryMessagingNetwork
|
||||
if (nextFixingDate > currentDay)
|
||||
currentDay = nextFixingDate
|
||||
|
||||
val sideA = TwoPartyDealProtocol.Floater(node2.net.myAddress, sessionID, timestamper.info,
|
||||
val sideA = TwoPartyDealProtocol.Floater(node2.net.myAddress, sessionID, notary.info,
|
||||
theDealRef, node1.services.keyManagementService.freshKey(), sessionID)
|
||||
val sideB = TwoPartyDealProtocol.Fixer(node1.net.myAddress, timestamper.info.identity,
|
||||
val sideB = TwoPartyDealProtocol.Fixer(node1.net.myAddress, notary.info.identity,
|
||||
theDealRef, sessionID)
|
||||
|
||||
linkConsensus(listOf(node1, node2, regulators[0]), sideB)
|
||||
@ -108,9 +107,9 @@ class IRSSimulation(runAsync: Boolean, latencyInjector: InMemoryMessagingNetwork
|
||||
|
||||
val sessionID = random63BitValue()
|
||||
|
||||
val instigator = TwoPartyDealProtocol.Instigator(node2.net.myAddress, timestamper.info,
|
||||
val instigator = TwoPartyDealProtocol.Instigator(node2.net.myAddress, notary.info,
|
||||
irs, node1.services.keyManagementService.freshKey(), sessionID)
|
||||
val acceptor = TwoPartyDealProtocol.Acceptor(node1.net.myAddress, timestamper.info.identity,
|
||||
val acceptor = TwoPartyDealProtocol.Acceptor(node1.net.myAddress, notary.info.identity,
|
||||
irs, sessionID)
|
||||
|
||||
// TODO: Eliminate the need for linkProtocolProgress
|
||||
|
@ -6,11 +6,6 @@ import com.google.common.util.concurrent.MoreExecutors
|
||||
import core.ThreadBox
|
||||
import core.crypto.sha256
|
||||
import core.messaging.*
|
||||
import core.node.NodeInfo
|
||||
import core.node.services.DummyTimestampingAuthority
|
||||
import core.node.services.NodeTimestamperService
|
||||
import core.node.services.ServiceType
|
||||
import core.node.services.TimestamperService
|
||||
import core.utilities.loggerFor
|
||||
import rx.Observable
|
||||
import rx.subjects.PublishSubject
|
||||
@ -144,18 +139,6 @@ class InMemoryMessagingNetwork {
|
||||
override fun hashCode() = id.hashCode()
|
||||
}
|
||||
|
||||
private var timestampingAdvert: NodeInfo? = null
|
||||
|
||||
@Synchronized
|
||||
fun setupTimestampingNode(manuallyPumped: Boolean): Pair<NodeInfo, InMemoryMessaging> {
|
||||
check(timestampingAdvert == null)
|
||||
val (handle, builder) = createNode(manuallyPumped)
|
||||
val node = builder.start().get()
|
||||
NodeTimestamperService(node, DummyTimestampingAuthority.identity, DummyTimestampingAuthority.key)
|
||||
timestampingAdvert = NodeInfo(handle, DummyTimestampingAuthority.identity, setOf(TimestamperService.Type))
|
||||
return Pair(timestampingAdvert!!, node)
|
||||
}
|
||||
|
||||
/**
|
||||
* An [InMemoryMessaging] provides a [MessagingService] that isn't backed by any kind of network or disk storage
|
||||
* system, but just uses regular queues on the heap instead. It is intended for unit testing and developer convenience
|
||||
|
@ -1,8 +1,6 @@
|
||||
package core.testing
|
||||
|
||||
import com.google.common.jimfs.Jimfs
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import com.google.common.util.concurrent.SettableFuture
|
||||
import core.Party
|
||||
import core.messaging.MessagingService
|
||||
import core.messaging.SingleMessageRecipient
|
||||
@ -11,13 +9,14 @@ import core.node.NodeConfiguration
|
||||
import core.node.NodeInfo
|
||||
import core.node.PhysicalLocation
|
||||
import core.node.services.NetworkMapService
|
||||
import core.node.services.NotaryService
|
||||
import core.node.services.ServiceType
|
||||
import core.node.services.TimestamperService
|
||||
import core.utilities.AffinityExecutor
|
||||
import core.utilities.loggerFor
|
||||
import org.slf4j.Logger
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.security.KeyPair
|
||||
import java.time.Clock
|
||||
import java.util.*
|
||||
|
||||
@ -47,19 +46,19 @@ class MockNetwork(private val threadPerNode: Boolean = false,
|
||||
|
||||
/** Allows customisation of how nodes are created. */
|
||||
interface Factory {
|
||||
fun create(dir: Path, config: NodeConfiguration, network: MockNetwork,
|
||||
networkMapAddr: NodeInfo?, advertisedServices: Set<ServiceType>, id: Int): MockNode
|
||||
fun create(dir: Path, config: NodeConfiguration, network: MockNetwork, networkMapAddr: NodeInfo?,
|
||||
advertisedServices: Set<ServiceType>, id: Int, keyPair: KeyPair?): MockNode
|
||||
}
|
||||
|
||||
object DefaultFactory : Factory {
|
||||
override fun create(dir: Path, config: NodeConfiguration, network: MockNetwork,
|
||||
networkMapAddr: NodeInfo?, advertisedServices: Set<ServiceType>, id: Int): MockNode {
|
||||
return MockNode(dir, config, network, networkMapAddr, advertisedServices, id)
|
||||
override fun create(dir: Path, config: NodeConfiguration, network: MockNetwork, networkMapAddr: NodeInfo?,
|
||||
advertisedServices: Set<ServiceType>, id: Int, keyPair: KeyPair?): MockNode {
|
||||
return MockNode(dir, config, network, networkMapAddr, advertisedServices, id, keyPair)
|
||||
}
|
||||
}
|
||||
|
||||
open class MockNode(dir: Path, config: NodeConfiguration, val mockNet: MockNetwork,
|
||||
networkMapAddr: NodeInfo?, advertisedServices: Set<ServiceType>, val id: Int) : AbstractNode(dir, config, networkMapAddr, advertisedServices, Clock.systemUTC()) {
|
||||
open class MockNode(dir: Path, config: NodeConfiguration, val mockNet: MockNetwork, networkMapAddr: NodeInfo?,
|
||||
advertisedServices: Set<ServiceType>, val id: Int, val keyPair: KeyPair?) : AbstractNode(dir, config, networkMapAddr, advertisedServices, Clock.systemUTC()) {
|
||||
override val log: Logger = loggerFor<MockNode>()
|
||||
override val serverThread: AffinityExecutor =
|
||||
if (mockNet.threadPerNode)
|
||||
@ -81,6 +80,8 @@ class MockNetwork(private val threadPerNode: Boolean = false,
|
||||
// Nothing to do
|
||||
}
|
||||
|
||||
override fun generateKeyPair(): KeyPair? = keyPair ?: super.generateKeyPair()
|
||||
|
||||
// There is no need to slow down the unit tests by initialising CityDatabase
|
||||
override fun findMyLocation(): PhysicalLocation? = null
|
||||
|
||||
@ -95,7 +96,7 @@ class MockNetwork(private val threadPerNode: Boolean = false,
|
||||
|
||||
/** Returns a started node, optionally created by the passed factory method */
|
||||
fun createNode(networkMapAddress: NodeInfo? = null, forcedID: Int = -1, nodeFactory: Factory = defaultFactory,
|
||||
vararg advertisedServices: ServiceType): MockNode {
|
||||
legalName: String? = null, keyPair: KeyPair? = null, vararg advertisedServices: ServiceType): MockNode {
|
||||
val newNode = forcedID == -1
|
||||
val id = if (newNode) counter++ else forcedID
|
||||
|
||||
@ -103,11 +104,11 @@ class MockNetwork(private val threadPerNode: Boolean = false,
|
||||
if (newNode)
|
||||
Files.createDirectories(path.resolve("attachments"))
|
||||
val config = object : NodeConfiguration {
|
||||
override val myLegalName: String = "Mock Company $id"
|
||||
override val myLegalName: String = legalName ?: "Mock Company $id"
|
||||
override val exportJMXto: String = ""
|
||||
override val nearestCity: String = "Atlantis"
|
||||
}
|
||||
val node = nodeFactory.create(path, config, this, networkMapAddress, advertisedServices.toSet(), id).start()
|
||||
val node = nodeFactory.create(path, config, this, networkMapAddress, advertisedServices.toSet(), id, keyPair).start()
|
||||
_nodes.add(node)
|
||||
return node
|
||||
}
|
||||
@ -128,16 +129,19 @@ class MockNetwork(private val threadPerNode: Boolean = false,
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up a two node network, in which the first node runs network map and timestamping services and the other
|
||||
* Sets up a two node network, in which the first node runs network map and notary services and the other
|
||||
* doesn't.
|
||||
*/
|
||||
fun createTwoNodes(nodeFactory: Factory = defaultFactory): Pair<MockNode, MockNode> {
|
||||
fun createTwoNodes(nodeFactory: Factory = defaultFactory, notaryKeyPair: KeyPair? = null): Pair<MockNode, MockNode> {
|
||||
require(nodes.isEmpty())
|
||||
return Pair(
|
||||
createNode(null, -1, nodeFactory, NetworkMapService.Type, TimestamperService.Type),
|
||||
createNode(nodes[0].info, -1, nodeFactory)
|
||||
createNode(null, -1, nodeFactory, null, notaryKeyPair, NetworkMapService.Type, NotaryService.Type),
|
||||
createNode(nodes[0].info, -1, nodeFactory, null)
|
||||
)
|
||||
}
|
||||
|
||||
fun createNotaryNode(legalName: String? = null, keyPair: KeyPair? = null) = createNode(null, -1, defaultFactory, legalName, keyPair, NetworkMapService.Type, NotaryService.Type)
|
||||
fun createPartyNode(networkMapAddr: NodeInfo, legalName: String? = null, keyPair: KeyPair? = null) = createNode(networkMapAddr, -1, defaultFactory, legalName, keyPair)
|
||||
|
||||
fun addressToNode(address: SingleMessageRecipient): MockNode = nodes.single { it.net.myAddress == address }
|
||||
}
|
@ -7,14 +7,15 @@ import core.node.NodeInfo
|
||||
import core.node.PhysicalLocation
|
||||
import core.node.services.NetworkMapService
|
||||
import core.node.services.NodeInterestRates
|
||||
import core.node.services.NotaryService
|
||||
import core.node.services.ServiceType
|
||||
import core.node.services.TimestamperService
|
||||
import core.protocols.ProtocolLogic
|
||||
import core.then
|
||||
import core.utilities.ProgressTracker
|
||||
import rx.Observable
|
||||
import rx.subjects.PublishSubject
|
||||
import java.nio.file.Path
|
||||
import java.security.KeyPair
|
||||
import java.time.LocalDate
|
||||
import java.util.*
|
||||
|
||||
@ -35,16 +36,16 @@ abstract class Simulation(val runAsync: Boolean,
|
||||
|
||||
// This puts together a mock network of SimulatedNodes.
|
||||
|
||||
open class SimulatedNode(dir: Path, config: NodeConfiguration, mockNet: MockNetwork,
|
||||
networkMapAddress: NodeInfo?, advertisedServices: Set<ServiceType>, id: Int) : MockNetwork.MockNode(dir, config, mockNet, networkMapAddress, advertisedServices, id) {
|
||||
open class SimulatedNode(dir: Path, config: NodeConfiguration, mockNet: MockNetwork, networkMapAddress: NodeInfo?,
|
||||
advertisedServices: Set<ServiceType>, id: Int, keyPair: KeyPair?) : MockNetwork.MockNode(dir, config, mockNet, networkMapAddress, advertisedServices, id, keyPair) {
|
||||
override fun findMyLocation(): PhysicalLocation? = CityDatabase[configuration.nearestCity]
|
||||
}
|
||||
|
||||
inner class BankFactory : MockNetwork.Factory {
|
||||
var counter = 0
|
||||
|
||||
override fun create(dir: Path, config: NodeConfiguration, network: MockNetwork,
|
||||
networkMapAddr: NodeInfo?, advertisedServices: Set<ServiceType>, id: Int): MockNetwork.MockNode {
|
||||
override fun create(dir: Path, config: NodeConfiguration, network: MockNetwork, networkMapAddr: NodeInfo?,
|
||||
advertisedServices: Set<ServiceType>, id: Int, keyPair: KeyPair?): MockNetwork.MockNode {
|
||||
val letter = 'A' + counter
|
||||
val city = bankLocations[counter++ % bankLocations.size]
|
||||
val cfg = object : NodeConfiguration {
|
||||
@ -53,7 +54,7 @@ abstract class Simulation(val runAsync: Boolean,
|
||||
override val exportJMXto: String = ""
|
||||
override val nearestCity: String = city
|
||||
}
|
||||
return SimulatedNode(dir, cfg, network, networkMapAddr, advertisedServices, id)
|
||||
return SimulatedNode(dir, cfg, network, networkMapAddr, advertisedServices, id, keyPair)
|
||||
}
|
||||
|
||||
fun createAll(): List<SimulatedNode> = bankLocations.
|
||||
@ -64,7 +65,7 @@ abstract class Simulation(val runAsync: Boolean,
|
||||
|
||||
object NetworkMapNodeFactory : MockNetwork.Factory {
|
||||
override fun create(dir: Path, config: NodeConfiguration, network: MockNetwork,
|
||||
networkMapAddr: NodeInfo?, advertisedServices: Set<ServiceType>, id: Int): MockNetwork.MockNode {
|
||||
networkMapAddr: NodeInfo?, advertisedServices: Set<ServiceType>, id: Int, keyPair: KeyPair?): MockNetwork.MockNode {
|
||||
require(advertisedServices.contains(NetworkMapService.Type))
|
||||
val cfg = object : NodeConfiguration {
|
||||
override val myLegalName: String = "Network Map Service Provider"
|
||||
@ -72,26 +73,26 @@ abstract class Simulation(val runAsync: Boolean,
|
||||
override val nearestCity: String = "Madrid"
|
||||
}
|
||||
|
||||
return object : SimulatedNode(dir, cfg, network, networkMapAddr, advertisedServices, id) { }
|
||||
return object : SimulatedNode(dir, cfg, network, networkMapAddr, advertisedServices, id, keyPair) {}
|
||||
}
|
||||
}
|
||||
|
||||
object TimestampingNodeFactory : MockNetwork.Factory {
|
||||
override fun create(dir: Path, config: NodeConfiguration, network: MockNetwork,
|
||||
networkMapAddr: NodeInfo?, advertisedServices: Set<ServiceType>, id: Int): MockNetwork.MockNode {
|
||||
require(advertisedServices.contains(TimestamperService.Type))
|
||||
object NotaryNodeFactory : MockNetwork.Factory {
|
||||
override fun create(dir: Path, config: NodeConfiguration, network: MockNetwork, networkMapAddr: NodeInfo?,
|
||||
advertisedServices: Set<ServiceType>, id: Int, keyPair: KeyPair?): MockNetwork.MockNode {
|
||||
require(advertisedServices.contains(NotaryService.Type))
|
||||
val cfg = object : NodeConfiguration {
|
||||
override val myLegalName: String = "Timestamping Service" // A magic string recognised by the CP contract
|
||||
override val myLegalName: String = "Notary Service"
|
||||
override val exportJMXto: String = ""
|
||||
override val nearestCity: String = "Zurich"
|
||||
}
|
||||
return SimulatedNode(dir, cfg, network, networkMapAddr, advertisedServices, id)
|
||||
return SimulatedNode(dir, cfg, network, networkMapAddr, advertisedServices, id, keyPair)
|
||||
}
|
||||
}
|
||||
|
||||
object RatesOracleFactory : MockNetwork.Factory {
|
||||
override fun create(dir: Path, config: NodeConfiguration, network: MockNetwork,
|
||||
networkMapAddr: NodeInfo?, advertisedServices: Set<ServiceType>, id: Int): MockNetwork.MockNode {
|
||||
override fun create(dir: Path, config: NodeConfiguration, network: MockNetwork, networkMapAddr: NodeInfo?,
|
||||
advertisedServices: Set<ServiceType>, id: Int, keyPair: KeyPair?): MockNetwork.MockNode {
|
||||
require(advertisedServices.contains(NodeInterestRates.Type))
|
||||
val cfg = object : NodeConfiguration {
|
||||
override val myLegalName: String = "Rates Service Provider"
|
||||
@ -99,7 +100,7 @@ abstract class Simulation(val runAsync: Boolean,
|
||||
override val nearestCity: String = "Madrid"
|
||||
}
|
||||
|
||||
return object : SimulatedNode(dir, cfg, network, networkMapAddr, advertisedServices, id) {
|
||||
return object : SimulatedNode(dir, cfg, network, networkMapAddr, advertisedServices, id, keyPair) {
|
||||
override fun makeInterestRatesOracleService() {
|
||||
super.makeInterestRatesOracleService()
|
||||
interestRatesService.upload(javaClass.getResourceAsStream("example.rates.txt"))
|
||||
@ -109,15 +110,15 @@ abstract class Simulation(val runAsync: Boolean,
|
||||
}
|
||||
|
||||
object RegulatorFactory : MockNetwork.Factory {
|
||||
override fun create(dir: Path, config: NodeConfiguration, network: MockNetwork,
|
||||
networkMapAddr: NodeInfo?, advertisedServices: Set<ServiceType>, id: Int): MockNetwork.MockNode {
|
||||
override fun create(dir: Path, config: NodeConfiguration, network: MockNetwork, networkMapAddr: NodeInfo?,
|
||||
advertisedServices: Set<ServiceType>, id: Int, keyPair: KeyPair?): MockNetwork.MockNode {
|
||||
val cfg = object : NodeConfiguration {
|
||||
override val myLegalName: String = "Regulator A"
|
||||
override val exportJMXto: String = ""
|
||||
override val nearestCity: String = "Paris"
|
||||
}
|
||||
|
||||
val n = object : SimulatedNode(dir, cfg, network, networkMapAddr, advertisedServices, id) {
|
||||
val n = object : SimulatedNode(dir, cfg, network, networkMapAddr, advertisedServices, id, keyPair) {
|
||||
// TODO: Regulatory nodes don't actually exist properly, this is a last minute demo request.
|
||||
// So we just fire a message at a node that doesn't know how to handle it, and it'll ignore it.
|
||||
// But that's fine for visualisation purposes.
|
||||
@ -131,11 +132,11 @@ abstract class Simulation(val runAsync: Boolean,
|
||||
val regulators: List<SimulatedNode> = listOf(network.createNode(null, nodeFactory = RegulatorFactory) as SimulatedNode)
|
||||
val networkMap: SimulatedNode
|
||||
= network.createNode(null, nodeFactory = NetworkMapNodeFactory, advertisedServices = NetworkMapService.Type) as SimulatedNode
|
||||
val timestamper: SimulatedNode
|
||||
= network.createNode(null, nodeFactory = TimestampingNodeFactory, advertisedServices = TimestamperService.Type) as SimulatedNode
|
||||
val notary: SimulatedNode
|
||||
= network.createNode(null, nodeFactory = NotaryNodeFactory, advertisedServices = NotaryService.Type) as SimulatedNode
|
||||
val ratesOracle: SimulatedNode
|
||||
= network.createNode(null, nodeFactory = RatesOracleFactory, advertisedServices = NodeInterestRates.Type) as SimulatedNode
|
||||
val serviceProviders: List<SimulatedNode> = listOf(timestamper, ratesOracle)
|
||||
val serviceProviders: List<SimulatedNode> = listOf(notary, ratesOracle)
|
||||
val banks: List<SimulatedNode> = bankFactory.createAll()
|
||||
|
||||
init {
|
||||
|
@ -23,24 +23,24 @@ class TradeSimulation(runAsync: Boolean, latencyInjector: InMemoryMessagingNetwo
|
||||
val buyer = banks[buyerBankIndex]
|
||||
val seller = banks[sellerBankIndex]
|
||||
|
||||
(buyer.services.walletService as NodeWalletService).fillWithSomeTestCash(1500.DOLLARS)
|
||||
(buyer.services.walletService as NodeWalletService).fillWithSomeTestCash(notary.info.identity, 1500.DOLLARS)
|
||||
|
||||
val issuance = run {
|
||||
val tx = CommercialPaper().generateIssue(seller.info.identity.ref(1, 2, 3), 1100.DOLLARS, Instant.now() + 10.days)
|
||||
tx.setTime(Instant.now(), timestamper.info.identity, 30.seconds)
|
||||
tx.signWith(timestamper.storage.myLegalIdentityKey)
|
||||
val tx = CommercialPaper().generateIssue(seller.info.identity.ref(1, 2, 3), 1100.DOLLARS, Instant.now() + 10.days, notary.info.identity)
|
||||
tx.setTime(Instant.now(), notary.info.identity, 30.seconds)
|
||||
tx.signWith(notary.storage.myLegalIdentityKey)
|
||||
tx.signWith(seller.storage.myLegalIdentityKey)
|
||||
tx.toSignedTransaction(true)
|
||||
}
|
||||
seller.services.storageService.validatedTransactions[issuance.id] = issuance
|
||||
|
||||
val sessionID = random63BitValue()
|
||||
val buyerProtocol = TwoPartyTradeProtocol.Buyer(seller.net.myAddress, timestamper.info.identity,
|
||||
val buyerProtocol = TwoPartyTradeProtocol.Buyer(seller.net.myAddress, notary.info.identity,
|
||||
1000.DOLLARS, CommercialPaper.State::class.java, sessionID)
|
||||
val sellerProtocol = TwoPartyTradeProtocol.Seller(buyer.net.myAddress, timestamper.info,
|
||||
val sellerProtocol = TwoPartyTradeProtocol.Seller(buyer.net.myAddress, notary.info,
|
||||
issuance.tx.outRef(0), 1000.DOLLARS, seller.storage.myLegalIdentityKey, sessionID)
|
||||
|
||||
linkConsensus(listOf(buyer, seller, timestamper), sellerProtocol)
|
||||
linkConsensus(listOf(buyer, seller, notary), sellerProtocol)
|
||||
linkProtocolProgress(buyer, buyerProtocol)
|
||||
linkProtocolProgress(seller, sellerProtocol)
|
||||
|
||||
|
@ -8,8 +8,11 @@ import core.node.Node
|
||||
import core.node.NodeConfiguration
|
||||
import core.node.NodeConfigurationFromConfig
|
||||
import core.node.NodeInfo
|
||||
import core.node.services.NetworkMapService
|
||||
import core.node.services.NodeInterestRates
|
||||
import core.node.services.NotaryService
|
||||
import core.node.services.ServiceType
|
||||
import core.node.subsystems.ArtemisMessagingService
|
||||
import core.node.services.*
|
||||
import core.serialization.deserialize
|
||||
import core.utilities.BriefLogFormatter
|
||||
import demos.protocols.AutoOfferProtocol
|
||||
@ -60,13 +63,13 @@ fun main(args: Array<String>) {
|
||||
val myNetAddr = HostAndPort.fromString(options.valueOf(networkAddressArg)).withDefaultPort(Node.DEFAULT_PORT)
|
||||
|
||||
val networkMapId = if (options.valueOf(networkMapNetAddr).equals(options.valueOf(networkAddressArg))) {
|
||||
// This node provides network map and timestamping services
|
||||
advertisedServices = setOf(NetworkMapService.Type, TimestamperService.Type)
|
||||
// This node provides network map and notary services
|
||||
advertisedServices = setOf(NetworkMapService.Type, NotaryService.Type)
|
||||
null
|
||||
} else {
|
||||
advertisedServices = setOf(NodeInterestRates.Type)
|
||||
try {
|
||||
nodeInfo(options.valueOf(networkMapNetAddr), options.valueOf(networkMapIdentityFile), setOf(NetworkMapService.Type, TimestamperService.Type))
|
||||
nodeInfo(options.valueOf(networkMapNetAddr), options.valueOf(networkMapIdentityFile), setOf(NetworkMapService.Type, NotaryService.Type))
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
@ -86,7 +89,7 @@ fun main(args: Array<String>) {
|
||||
val peerId = nodeInfo(hostAndPortString, identityFile)
|
||||
node.services.identityService.registerIdentity(peerId.identity)
|
||||
} catch (e: Exception) {
|
||||
println("Could not load peer identity file \"${identityFile}\".")
|
||||
println("Could not load peer identity file \"$identityFile\".")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -5,9 +5,9 @@ import core.*
|
||||
import core.node.Node
|
||||
import core.node.NodeConfiguration
|
||||
import core.node.NodeInfo
|
||||
import core.node.subsystems.ArtemisMessagingService
|
||||
import core.node.services.NodeInterestRates
|
||||
import core.node.services.ServiceType
|
||||
import core.node.subsystems.ArtemisMessagingService
|
||||
import core.serialization.deserialize
|
||||
import core.utilities.ANSIProgressRenderer
|
||||
import core.utilities.BriefLogFormatter
|
||||
@ -76,9 +76,11 @@ fun main(args: Array<String>) {
|
||||
|
||||
val node = logElapsedTime("Node startup") { Node(dir, myNetAddr, config, networkMapAddress, advertisedServices).start() }
|
||||
|
||||
val notary = node.services.networkMapCache.notaryNodes[0]
|
||||
|
||||
// Make a garbage transaction that includes a rate fix.
|
||||
val tx = TransactionBuilder()
|
||||
tx.addOutputState(Cash.State(node.storage.myLegalIdentity.ref(1), 1500.DOLLARS, node.keyManagement.freshKey().public))
|
||||
tx.addOutputState(Cash.State(node.storage.myLegalIdentity.ref(1), 1500.DOLLARS, node.keyManagement.freshKey().public, notary.identity))
|
||||
val protocol = RatesFixProtocol(tx, oracleNode, fixOf, expectedRate, rateTolerance)
|
||||
ANSIProgressRenderer.progressTracker = protocol.progressTracker
|
||||
node.smm.add("demo.ratefix", protocol).get()
|
||||
|
@ -12,9 +12,12 @@ import core.node.Node
|
||||
import core.node.NodeConfiguration
|
||||
import core.node.NodeConfigurationFromConfig
|
||||
import core.node.NodeInfo
|
||||
import core.node.services.NetworkMapService
|
||||
import core.node.services.NodeAttachmentService
|
||||
import core.node.services.NotaryService
|
||||
import core.node.services.ServiceType
|
||||
import core.node.subsystems.ArtemisMessagingService
|
||||
import core.node.subsystems.NodeWalletService
|
||||
import core.node.services.*
|
||||
import core.protocols.ProtocolLogic
|
||||
import core.serialization.deserialize
|
||||
import core.utilities.ANSIProgressRenderer
|
||||
@ -22,14 +25,13 @@ import core.utilities.BriefLogFormatter
|
||||
import core.utilities.Emoji
|
||||
import core.utilities.ProgressTracker
|
||||
import joptsimple.OptionParser
|
||||
import protocols.TimestampingProtocol
|
||||
import protocols.NotaryProtocol
|
||||
import protocols.TwoPartyTradeProtocol
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
import java.security.PublicKey
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
import kotlin.system.exitProcess
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
@ -88,7 +90,7 @@ fun main(args: Array<String>) {
|
||||
NodeInfo(ArtemisMessagingService.makeRecipient(addr), party, setOf(NetworkMapService.Type))
|
||||
} else {
|
||||
// We must be the network map service
|
||||
advertisedServices = setOf(NetworkMapService.Type, TimestamperService.Type)
|
||||
advertisedServices = setOf(NetworkMapService.Type, NotaryService.Type)
|
||||
null
|
||||
}
|
||||
|
||||
@ -102,7 +104,7 @@ fun main(args: Array<String>) {
|
||||
it.storePath
|
||||
}
|
||||
|
||||
val buyer = TraderDemoProtocolBuyer(attachmentsPath)
|
||||
val buyer = TraderDemoProtocolBuyer(attachmentsPath, node.info.identity)
|
||||
ANSIProgressRenderer.progressTracker = buyer.progressTracker
|
||||
node.smm.add("demo.buyer", buyer).get() // This thread will halt forever here.
|
||||
} else {
|
||||
@ -130,18 +132,20 @@ fun main(args: Array<String>) {
|
||||
|
||||
// We create a couple of ad-hoc test protocols that wrap the two party trade protocol, to give us the demo logic.
|
||||
|
||||
class TraderDemoProtocolBuyer(private val attachmentsPath: Path) : ProtocolLogic<Unit>() {
|
||||
class TraderDemoProtocolBuyer(private val attachmentsPath: Path, val notary: Party) : ProtocolLogic<Unit>() {
|
||||
companion object {
|
||||
object WAITING_FOR_SELLER_TO_CONNECT : ProgressTracker.Step("Waiting for seller to connect to us")
|
||||
|
||||
object STARTING_BUY : ProgressTracker.Step("Seller connected, purchasing commercial paper asset")
|
||||
}
|
||||
|
||||
override val progressTracker = ProgressTracker(WAITING_FOR_SELLER_TO_CONNECT, STARTING_BUY)
|
||||
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
// Give us some cash. Note that as nodes do not currently track forward pointers, we can spend the same cash over
|
||||
// and over again and the double spends will never be detected! Fixing that is the next step.
|
||||
(serviceHub.walletService as NodeWalletService).fillWithSomeTestCash(1500.DOLLARS)
|
||||
(serviceHub.walletService as NodeWalletService).fillWithSomeTestCash(notary, 1500.DOLLARS)
|
||||
|
||||
while (true) {
|
||||
// Wait around until a node asks to start a trade with us. In a real system, this part would happen out of band
|
||||
@ -157,8 +161,8 @@ class TraderDemoProtocolBuyer(private val attachmentsPath: Path) : ProtocolLogic
|
||||
progressTracker.currentStep = STARTING_BUY
|
||||
send("test.junktrade", newPartnerAddr, 0, sessionID)
|
||||
|
||||
val tsa = serviceHub.networkMapCache.timestampingNodes[0]
|
||||
val buyer = TwoPartyTradeProtocol.Buyer(newPartnerAddr, tsa.identity, 1000.DOLLARS,
|
||||
val notary = serviceHub.networkMapCache.notaryNodes[0]
|
||||
val buyer = TwoPartyTradeProtocol.Buyer(newPartnerAddr, notary.identity, 1000.DOLLARS,
|
||||
CommercialPaper.State::class.java, sessionID)
|
||||
val tradeTX: SignedTransaction = subProtocol(buyer)
|
||||
|
||||
@ -197,7 +201,9 @@ class TraderDemoProtocolSeller(val myAddress: HostAndPort,
|
||||
val PROSPECTUS_HASH = SecureHash.parse("decd098666b9657314870e192ced0c3519c2c9d395507a238338f8d003929de9")
|
||||
|
||||
object ANNOUNCING : ProgressTracker.Step("Announcing to the buyer node")
|
||||
|
||||
object SELF_ISSUING : ProgressTracker.Step("Got session ID back, issuing and timestamping some commercial paper")
|
||||
|
||||
object TRADING : ProgressTracker.Step("Starting the trade protocol")
|
||||
|
||||
// We vend a progress tracker that already knows there's going to be a TwoPartyTradingProtocol involved at some
|
||||
@ -216,27 +222,27 @@ class TraderDemoProtocolSeller(val myAddress: HostAndPort,
|
||||
|
||||
progressTracker.currentStep = SELF_ISSUING
|
||||
|
||||
val tsa = serviceHub.networkMapCache.timestampingNodes[0]
|
||||
val notary = serviceHub.networkMapCache.notaryNodes[0]
|
||||
val cpOwnerKey = serviceHub.keyManagementService.freshKey()
|
||||
val commercialPaper = selfIssueSomeCommercialPaper(cpOwnerKey.public, tsa)
|
||||
val commercialPaper = selfIssueSomeCommercialPaper(cpOwnerKey.public, notary)
|
||||
|
||||
progressTracker.currentStep = TRADING
|
||||
|
||||
val seller = TwoPartyTradeProtocol.Seller(otherSide, tsa, commercialPaper, 1000.DOLLARS, cpOwnerKey, sessionID,
|
||||
progressTracker.childrenFor[TRADING]!!)
|
||||
val seller = TwoPartyTradeProtocol.Seller(otherSide, notary, commercialPaper, 1000.DOLLARS, cpOwnerKey,
|
||||
sessionID, progressTracker.childrenFor[TRADING]!!)
|
||||
val tradeTX: SignedTransaction = subProtocol(seller)
|
||||
|
||||
logger.info("Sale completed - we have a happy customer!\n\nFinal transaction is:\n\n${Emoji.renderIfSupported(tradeTX.tx)}")
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
fun selfIssueSomeCommercialPaper(ownedBy: PublicKey, tsa: NodeInfo): StateAndRef<CommercialPaper.State> {
|
||||
fun selfIssueSomeCommercialPaper(ownedBy: PublicKey, notaryNode: NodeInfo): StateAndRef<CommercialPaper.State> {
|
||||
// Make a fake company that's issued its own paper.
|
||||
val keyPair = generateKeyPair()
|
||||
val party = Party("Bank of London", keyPair.public)
|
||||
|
||||
val issuance = run {
|
||||
val tx = CommercialPaper().generateIssue(party.ref(1, 2, 3), 1100.DOLLARS, Instant.now() + 10.days)
|
||||
val tx = CommercialPaper().generateIssue(party.ref(1, 2, 3), 1100.DOLLARS, Instant.now() + 10.days, notaryNode.identity)
|
||||
|
||||
// TODO: Consider moving these two steps below into generateIssue.
|
||||
|
||||
@ -244,25 +250,25 @@ class TraderDemoProtocolSeller(val myAddress: HostAndPort,
|
||||
tx.addAttachment(serviceHub.storageService.attachments.openAttachment(PROSPECTUS_HASH)!!)
|
||||
|
||||
// Timestamp it, all CP must be timestamped.
|
||||
tx.setTime(Instant.now(), tsa.identity, 30.seconds)
|
||||
val tsaSig = subProtocol(TimestampingProtocol(tsa, tx.toWireTransaction().serialized))
|
||||
tx.checkAndAddSignature(tsaSig)
|
||||
tx.setTime(Instant.now(), notaryNode.identity, 30.seconds)
|
||||
tx.signWith(keyPair)
|
||||
|
||||
val notarySig = subProtocol(NotaryProtocol(tx.toWireTransaction()))
|
||||
tx.addSignatureUnchecked(notarySig)
|
||||
tx.toSignedTransaction(true)
|
||||
}
|
||||
|
||||
serviceHub.recordTransactions(listOf(issuance))
|
||||
|
||||
val move = run {
|
||||
val tx = TransactionBuilder()
|
||||
CommercialPaper().generateMove(tx, issuance.tx.outRef(0), ownedBy)
|
||||
tx.signWith(keyPair)
|
||||
val notarySig = subProtocol(NotaryProtocol(tx.toWireTransaction()))
|
||||
tx.addSignatureUnchecked(notarySig)
|
||||
tx.toSignedTransaction(true)
|
||||
}
|
||||
|
||||
with(serviceHub.storageService) {
|
||||
validatedTransactions[issuance.id] = issuance
|
||||
validatedTransactions[move.id] = move
|
||||
}
|
||||
serviceHub.recordTransactions(listOf(move))
|
||||
|
||||
return move.tx.outRef(0)
|
||||
}
|
||||
|
@ -48,7 +48,7 @@ object AutoOfferProtocol {
|
||||
}
|
||||
|
||||
fun register(node: Node) {
|
||||
node.net.addMessageHandler("${TOPIC}.0") { msg, registration ->
|
||||
node.net.addMessageHandler("$TOPIC.0") { msg, registration ->
|
||||
val progressTracker = tracker()
|
||||
ANSIProgressRenderer.progressTracker = progressTracker
|
||||
progressTracker.currentStep = RECEIVED
|
||||
@ -57,7 +57,7 @@ object AutoOfferProtocol {
|
||||
progressTracker.currentStep = DEALING
|
||||
// TODO required as messaging layer does not currently queue messages that arrive before we expect them
|
||||
Thread.sleep(100)
|
||||
val seller = TwoPartyDealProtocol.Instigator(autoOfferMessage.otherSide, node.services.networkMapCache.timestampingNodes.first(),
|
||||
val seller = TwoPartyDealProtocol.Instigator(autoOfferMessage.otherSide, node.services.networkMapCache.notaryNodes.first(),
|
||||
autoOfferMessage.dealBeingOffered, node.services.keyManagementService.freshKey(), autoOfferMessage.otherSessionID, progressTracker.childrenFor[DEALING]!!)
|
||||
val future = node.smm.add("${TwoPartyDealProtocol.DEAL_TOPIC}.seller", seller)
|
||||
// This is required because we are doing child progress outside of a subprotocol. In future, we should just wrap things like this in a protocol to avoid it
|
||||
@ -93,19 +93,19 @@ object AutoOfferProtocol {
|
||||
|
||||
@Suspendable
|
||||
override fun call(): SignedTransaction {
|
||||
require(serviceHub.networkMapCache.timestampingNodes.isNotEmpty()) { "No timestamping nodes registered" }
|
||||
require(serviceHub.networkMapCache.notaryNodes.isNotEmpty()) { "No notary nodes registered" }
|
||||
val ourSessionID = random63BitValue()
|
||||
|
||||
val timestampingAuthority = serviceHub.networkMapCache.timestampingNodes.first()
|
||||
val notary = serviceHub.networkMapCache.notaryNodes.first()
|
||||
// need to pick which ever party is not us
|
||||
val otherParty = notUs(*dealToBeOffered.parties).single()
|
||||
val otherNode = (serviceHub.networkMapCache.nodeForPartyName(otherParty.name))
|
||||
val otherNode = (serviceHub.networkMapCache.getNodeByLegalName(otherParty.name))
|
||||
requireNotNull(otherNode) { "Cannot identify other party " + otherParty.name + ", know about: " + serviceHub.networkMapCache.partyNodes.map { it.identity } }
|
||||
val otherSide = otherNode!!.address
|
||||
progressTracker.currentStep = ANNOUNCING
|
||||
send(TOPIC, otherSide, 0, AutoOfferMessage(serviceHub.networkService.myAddress, ourSessionID, dealToBeOffered))
|
||||
progressTracker.currentStep = DEALING
|
||||
val stx = subProtocol(TwoPartyDealProtocol.Acceptor(otherSide, timestampingAuthority.identity, dealToBeOffered, ourSessionID, progressTracker.childrenFor[DEALING]!!))
|
||||
val stx = subProtocol(TwoPartyDealProtocol.Acceptor(otherSide, notary.identity, dealToBeOffered, ourSessionID, progressTracker.childrenFor[DEALING]!!))
|
||||
return stx
|
||||
}
|
||||
|
||||
|
@ -59,7 +59,7 @@ object UpdateBusinessDayProtocol {
|
||||
// This assumes we definitely have one key or the other
|
||||
fun otherParty(deal: DealState): NodeInfo {
|
||||
val ourKeys = serviceHub.keyManagementService.keys.keys
|
||||
return serviceHub.networkMapCache.nodeForPartyName(deal.parties.single { !ourKeys.contains(it.owningKey) }.name)!!
|
||||
return serviceHub.networkMapCache.getNodeByLegalName(deal.parties.single { !ourKeys.contains(it.owningKey) }.name)!!
|
||||
}
|
||||
|
||||
// TODO we should make this more object oriented when we can ask a state for it's contract
|
||||
@ -102,7 +102,7 @@ object UpdateBusinessDayProtocol {
|
||||
val deal: InterestRateSwap.State = dealStateAndRef.state
|
||||
val myOldParty = deal.parties.single { it.name == myName }
|
||||
val keyPair = serviceHub.keyManagementService.toKeyPair(myOldParty.owningKey)
|
||||
val participant = TwoPartyDealProtocol.Floater(party.address, sessionID, serviceHub.networkMapCache.timestampingNodes[0], dealStateAndRef,
|
||||
val participant = TwoPartyDealProtocol.Floater(party.address, sessionID, serviceHub.networkMapCache.notaryNodes[0], dealStateAndRef,
|
||||
keyPair,
|
||||
sessionID, progressTracker.childrenFor[FIXING]!!)
|
||||
val result = subProtocol(participant)
|
||||
@ -114,7 +114,7 @@ object UpdateBusinessDayProtocol {
|
||||
progressTracker.childrenFor[FIXING] = TwoPartyDealProtocol.Secondary.tracker()
|
||||
progressTracker.currentStep = FIXING
|
||||
|
||||
val participant = TwoPartyDealProtocol.Fixer(party.address, serviceHub.networkMapCache.timestampingNodes[0].identity, dealStateAndRef, sessionID, progressTracker.childrenFor[FIXING]!!)
|
||||
val participant = TwoPartyDealProtocol.Fixer(party.address, serviceHub.networkMapCache.notaryNodes[0].identity, dealStateAndRef, sessionID, progressTracker.childrenFor[FIXING]!!)
|
||||
val result = subProtocol(participant)
|
||||
return result.tx.outRef(0)
|
||||
}
|
||||
@ -124,7 +124,7 @@ object UpdateBusinessDayProtocol {
|
||||
|
||||
object Handler {
|
||||
fun register(node: Node) {
|
||||
node.net.addMessageHandler("${TOPIC}.0") { msg, registration ->
|
||||
node.net.addMessageHandler("$TOPIC.0") { msg, registration ->
|
||||
// Just to validate we got the message
|
||||
val updateBusinessDayMessage = msg.data.deserialize<UpdateBusinessDayMessage>()
|
||||
if ((node.services.clock as DemoClock).updateDate(updateBusinessDayMessage.date)) {
|
||||
|
94
src/main/kotlin/protocols/NotaryProtocol.kt
Normal file
94
src/main/kotlin/protocols/NotaryProtocol.kt
Normal file
@ -0,0 +1,94 @@
|
||||
package protocols
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import core.Party
|
||||
import core.TimestampCommand
|
||||
import core.WireTransaction
|
||||
import core.crypto.DigitalSignature
|
||||
import core.messaging.SingleMessageRecipient
|
||||
import core.node.NodeInfo
|
||||
import core.node.services.NotaryError
|
||||
import core.node.services.NotaryException
|
||||
import core.node.services.NotaryService
|
||||
import core.protocols.ProtocolLogic
|
||||
import core.random63BitValue
|
||||
import core.serialization.SerializedBytes
|
||||
import core.utilities.ProgressTracker
|
||||
import core.utilities.UntrustworthyData
|
||||
import java.security.PublicKey
|
||||
|
||||
/**
|
||||
* A protocol to be used for obtaining a signature from a [NotaryService] ascertaining the transaction
|
||||
* timestamp is correct and none of its inputs have been used in another completed transaction
|
||||
*
|
||||
* @throws NotaryException in case the any of the inputs to the transaction have been consumed
|
||||
* by another transaction or the timestamp is invalid
|
||||
*/
|
||||
class NotaryProtocol(private val wtx: WireTransaction,
|
||||
override val progressTracker: ProgressTracker = NotaryProtocol.tracker()) : ProtocolLogic<DigitalSignature.LegallyIdentifiable>() {
|
||||
companion object {
|
||||
val TOPIC = "platform.notary.request"
|
||||
|
||||
object REQUESTING : ProgressTracker.Step("Requesting signature by Notary service")
|
||||
|
||||
object VALIDATING : ProgressTracker.Step("Validating response from Notary service")
|
||||
|
||||
fun tracker() = ProgressTracker(REQUESTING, VALIDATING)
|
||||
}
|
||||
|
||||
lateinit var notaryNode: NodeInfo
|
||||
|
||||
@Suspendable
|
||||
override fun call(): DigitalSignature.LegallyIdentifiable {
|
||||
progressTracker.currentStep = REQUESTING
|
||||
notaryNode = findNotaryNode()
|
||||
|
||||
val sessionID = random63BitValue()
|
||||
val request = SignRequest(wtx.serialized, serviceHub.storageService.myLegalIdentity, serviceHub.networkService.myAddress, sessionID)
|
||||
val response = sendAndReceive<NotaryService.Result>(TOPIC, notaryNode.address, 0, sessionID, request)
|
||||
|
||||
val notaryResult = validateResponse(response)
|
||||
return notaryResult.sig ?: throw NotaryException(notaryResult.error!!)
|
||||
}
|
||||
|
||||
private fun validateResponse(response: UntrustworthyData<NotaryService.Result>): NotaryService.Result {
|
||||
progressTracker.currentStep = VALIDATING
|
||||
|
||||
response.validate {
|
||||
if (it.sig != null) validateSignature(it.sig, wtx.serialized)
|
||||
else if (it.error is NotaryError.Conflict) it.error.conflict.verified()
|
||||
else if (it.error !is NotaryError)
|
||||
throw IllegalStateException("Received invalid result from Notary service '${notaryNode.identity}'")
|
||||
return it
|
||||
}
|
||||
}
|
||||
|
||||
private fun validateSignature(sig: DigitalSignature.LegallyIdentifiable, data: SerializedBytes<WireTransaction>) {
|
||||
check(sig.signer == notaryNode.identity) { "Notary result not signed by the correct service" }
|
||||
sig.verifyWithECDSA(data)
|
||||
}
|
||||
|
||||
private fun findNotaryNode(): NodeInfo {
|
||||
var maybeNotaryKey: PublicKey? = null
|
||||
|
||||
val timestampCommand = wtx.commands.singleOrNull { it.value is TimestampCommand }
|
||||
if (timestampCommand != null) maybeNotaryKey = timestampCommand.signers.first()
|
||||
|
||||
for (stateRef in wtx.inputs) {
|
||||
val inputNotaryKey = serviceHub.loadState(stateRef).notary.owningKey
|
||||
if (maybeNotaryKey != null)
|
||||
check(maybeNotaryKey == inputNotaryKey) { "Input states and timestamp must have the same Notary" }
|
||||
else maybeNotaryKey = inputNotaryKey
|
||||
}
|
||||
|
||||
val notaryKey = maybeNotaryKey ?: throw IllegalStateException("Transaction does not specify a Notary")
|
||||
val notaryNode = serviceHub.networkMapCache.getNodeByPublicKey(notaryKey)
|
||||
return notaryNode ?: throw IllegalStateException("No Notary node can be found with the specified public key")
|
||||
}
|
||||
|
||||
/** TODO: The caller must authenticate instead of just specifying its identity */
|
||||
class SignRequest(val txBits: SerializedBytes<WireTransaction>,
|
||||
val callerIdentity: Party,
|
||||
replyTo: SingleMessageRecipient,
|
||||
sessionID: Long) : AbstractRequestMessage(replyTo, sessionID)
|
||||
}
|
@ -16,7 +16,6 @@ import core.utilities.trace
|
||||
import java.security.KeyPair
|
||||
import java.security.PublicKey
|
||||
import java.security.SignatureException
|
||||
import java.time.Instant
|
||||
|
||||
/**
|
||||
* This asset trading protocol implements a "delivery vs payment" type swap. It has two parties (B and S for buyer
|
||||
@ -44,18 +43,18 @@ import java.time.Instant
|
||||
object TwoPartyTradeProtocol {
|
||||
val TRADE_TOPIC = "platform.trade"
|
||||
|
||||
fun runSeller(smm: StateMachineManager, timestampingAuthority: NodeInfo,
|
||||
fun runSeller(smm: StateMachineManager, notary: NodeInfo,
|
||||
otherSide: SingleMessageRecipient, assetToSell: StateAndRef<OwnableState>, price: Amount,
|
||||
myKeyPair: KeyPair, buyerSessionID: Long): ListenableFuture<SignedTransaction> {
|
||||
val seller = Seller(otherSide, timestampingAuthority, assetToSell, price, myKeyPair, buyerSessionID)
|
||||
val seller = Seller(otherSide, notary, assetToSell, price, myKeyPair, buyerSessionID)
|
||||
return smm.add("${TRADE_TOPIC}.seller", seller)
|
||||
}
|
||||
|
||||
fun runBuyer(smm: StateMachineManager, timestampingAuthority: NodeInfo,
|
||||
fun runBuyer(smm: StateMachineManager, notaryNode: NodeInfo,
|
||||
otherSide: SingleMessageRecipient, acceptablePrice: Amount, typeToBuy: Class<out OwnableState>,
|
||||
sessionID: Long): ListenableFuture<SignedTransaction> {
|
||||
val buyer = Buyer(otherSide, timestampingAuthority.identity, acceptablePrice, typeToBuy, sessionID)
|
||||
return smm.add("${TRADE_TOPIC}.buyer", buyer)
|
||||
val buyer = Buyer(otherSide, notaryNode.identity, acceptablePrice, typeToBuy, sessionID)
|
||||
return smm.add("$TRADE_TOPIC.buyer", buyer)
|
||||
}
|
||||
|
||||
class UnacceptablePriceException(val givenPrice: Amount) : Exception()
|
||||
@ -71,10 +70,11 @@ object TwoPartyTradeProtocol {
|
||||
val sessionID: Long
|
||||
)
|
||||
|
||||
class SignaturesFromSeller(val timestampAuthoritySig: DigitalSignature.WithKey, val sellerSig: DigitalSignature.WithKey)
|
||||
class SignaturesFromSeller(val sellerSig: DigitalSignature.WithKey,
|
||||
val notarySig: DigitalSignature.WithKey)
|
||||
|
||||
open class Seller(val otherSide: SingleMessageRecipient,
|
||||
val timestampingAuthority: NodeInfo,
|
||||
val notaryNode: NodeInfo,
|
||||
val assetToSell: StateAndRef<OwnableState>,
|
||||
val price: Amount,
|
||||
val myKeyPair: KeyPair,
|
||||
@ -83,12 +83,16 @@ object TwoPartyTradeProtocol {
|
||||
|
||||
companion object {
|
||||
object AWAITING_PROPOSAL : ProgressTracker.Step("Awaiting transaction proposal")
|
||||
|
||||
object VERIFYING : ProgressTracker.Step("Verifying transaction proposal")
|
||||
|
||||
object SIGNING : ProgressTracker.Step("Signing transaction")
|
||||
object TIMESTAMPING : ProgressTracker.Step("Timestamping transaction")
|
||||
|
||||
object NOTARY : ProgressTracker.Step("Getting notary signature")
|
||||
|
||||
object SENDING_SIGS : ProgressTracker.Step("Sending transaction signatures to buyer")
|
||||
|
||||
fun tracker() = ProgressTracker(AWAITING_PROPOSAL, VERIFYING, SIGNING, TIMESTAMPING, SENDING_SIGS)
|
||||
fun tracker() = ProgressTracker(AWAITING_PROPOSAL, VERIFYING, SIGNING, NOTARY, SENDING_SIGS)
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
@ -97,15 +101,15 @@ object TwoPartyTradeProtocol {
|
||||
|
||||
// These two steps could be done in parallel, in theory. Our framework doesn't support that yet though.
|
||||
val ourSignature = signWithOurKey(partialTX)
|
||||
val tsaSig = timestamp(partialTX)
|
||||
val notarySignature = getNotarySignature(partialTX)
|
||||
|
||||
return sendSignatures(partialTX, ourSignature, tsaSig)
|
||||
return sendSignatures(partialTX, ourSignature, notarySignature)
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
private fun timestamp(partialTX: SignedTransaction): DigitalSignature.LegallyIdentifiable {
|
||||
progressTracker.currentStep = TIMESTAMPING
|
||||
return subProtocol(TimestampingProtocol(timestampingAuthority, partialTX.txBits))
|
||||
private fun getNotarySignature(stx: SignedTransaction): DigitalSignature.LegallyIdentifiable {
|
||||
progressTracker.currentStep = NOTARY
|
||||
return subProtocol(NotaryProtocol(stx.tx))
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
@ -126,7 +130,7 @@ object TwoPartyTradeProtocol {
|
||||
|
||||
// Check that the tx proposed by the buyer is valid.
|
||||
val missingSigs = it.verify(throwIfSignaturesAreMissing = false)
|
||||
if (missingSigs != setOf(myKeyPair.public, timestampingAuthority.identity.owningKey))
|
||||
if (missingSigs != setOf(myKeyPair.public, notaryNode.identity.owningKey))
|
||||
throw SignatureException("The set of missing signatures is not as expected: $missingSigs")
|
||||
|
||||
val wtx: WireTransaction = it.tx
|
||||
@ -162,7 +166,6 @@ object TwoPartyTradeProtocol {
|
||||
subProtocol(ResolveTransactionsProtocol(dependencyTxIDs, otherSide))
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
open fun signWithOurKey(partialTX: SignedTransaction): DigitalSignature.WithKey {
|
||||
progressTracker.currentStep = SIGNING
|
||||
return myKeyPair.signWithECDSA(partialTX.txBits)
|
||||
@ -170,26 +173,29 @@ object TwoPartyTradeProtocol {
|
||||
|
||||
@Suspendable
|
||||
private fun sendSignatures(partialTX: SignedTransaction, ourSignature: DigitalSignature.WithKey,
|
||||
tsaSig: DigitalSignature.LegallyIdentifiable): SignedTransaction {
|
||||
notarySignature: DigitalSignature.LegallyIdentifiable): SignedTransaction {
|
||||
progressTracker.currentStep = SENDING_SIGS
|
||||
val fullySigned = partialTX + tsaSig + ourSignature
|
||||
val fullySigned = partialTX + ourSignature + notarySignature
|
||||
|
||||
logger.trace { "Built finished transaction, sending back to secondary!" }
|
||||
|
||||
send(TRADE_TOPIC, otherSide, buyerSessionID, SignaturesFromSeller(tsaSig, ourSignature))
|
||||
send(TRADE_TOPIC, otherSide, buyerSessionID, SignaturesFromSeller(ourSignature, notarySignature))
|
||||
return fullySigned
|
||||
}
|
||||
}
|
||||
|
||||
open class Buyer(val otherSide: SingleMessageRecipient,
|
||||
val timestampingAuthority: Party,
|
||||
val notary: Party,
|
||||
val acceptablePrice: Amount,
|
||||
val typeToBuy: Class<out OwnableState>,
|
||||
val sessionID: Long) : ProtocolLogic<SignedTransaction>() {
|
||||
|
||||
object RECEIVING : ProgressTracker.Step("Waiting for seller trading info")
|
||||
|
||||
object VERIFYING : ProgressTracker.Step("Verifying seller assets")
|
||||
|
||||
object SIGNING : ProgressTracker.Step("Generating and signing transaction proposal")
|
||||
|
||||
object SWAPPING_SIGNATURES : ProgressTracker.Step("Swapping signatures with the seller")
|
||||
|
||||
override val progressTracker = ProgressTracker(RECEIVING, VERIFYING, SIGNING, SWAPPING_SIGNATURES)
|
||||
@ -207,7 +213,7 @@ object TwoPartyTradeProtocol {
|
||||
val signatures = swapSignaturesWithSeller(stx, tradeRequest.sessionID)
|
||||
|
||||
logger.trace { "Got signatures from seller, verifying ... " }
|
||||
val fullySigned = stx + signatures.timestampAuthoritySig + signatures.sellerSig
|
||||
val fullySigned = stx + signatures.sellerSig + signatures.notarySig
|
||||
fullySigned.verify()
|
||||
|
||||
logger.trace { "Signatures received are valid. Trade complete! :-)" }
|
||||
@ -281,7 +287,8 @@ object TwoPartyTradeProtocol {
|
||||
|
||||
// And add a request for timestamping: it may be that none of the contracts need this! But it can't hurt
|
||||
// to have one.
|
||||
ptx.setTime(Instant.now(), timestampingAuthority, 30.seconds)
|
||||
val currentTime = serviceHub.clock.instant()
|
||||
ptx.setTime(currentTime, notary, 30.seconds)
|
||||
return Pair(ptx, cashSigningPubKeys)
|
||||
}
|
||||
}
|
||||
|
@ -18,7 +18,8 @@ class CashTests {
|
||||
val inState = Cash.State(
|
||||
deposit = MEGA_CORP.ref(1),
|
||||
amount = 1000.DOLLARS,
|
||||
owner = DUMMY_PUBKEY_1
|
||||
owner = DUMMY_PUBKEY_1,
|
||||
notary = DUMMY_NOTARY
|
||||
)
|
||||
val outState = inState.copy(owner = DUMMY_PUBKEY_2)
|
||||
|
||||
@ -63,7 +64,7 @@ class CashTests {
|
||||
fun issueMoney() {
|
||||
// Check we can't "move" money into existence.
|
||||
transaction {
|
||||
input { DummyContract.State() }
|
||||
input { DummyContract.State(notary = DUMMY_NOTARY) }
|
||||
output { outState }
|
||||
arg(MINI_CORP_PUBKEY) { Cash.Commands.Move() }
|
||||
|
||||
@ -82,7 +83,8 @@ class CashTests {
|
||||
Cash.State(
|
||||
amount = 1000.DOLLARS,
|
||||
owner = DUMMY_PUBKEY_1,
|
||||
deposit = MINI_CORP.ref(12, 34)
|
||||
deposit = MINI_CORP.ref(12, 34),
|
||||
notary = DUMMY_NOTARY
|
||||
)
|
||||
}
|
||||
tweak {
|
||||
@ -95,7 +97,7 @@ class CashTests {
|
||||
|
||||
// Test generation works.
|
||||
val ptx = TransactionBuilder()
|
||||
Cash().generateIssue(ptx, 100.DOLLARS, MINI_CORP.ref(12, 34), owner = DUMMY_PUBKEY_1)
|
||||
Cash().generateIssue(ptx, 100.DOLLARS, MINI_CORP.ref(12, 34), owner = DUMMY_PUBKEY_1, notary = DUMMY_NOTARY)
|
||||
assertTrue(ptx.inputStates().isEmpty())
|
||||
val s = ptx.outputStates()[0] as Cash.State
|
||||
assertEquals(100.DOLLARS, s.amount)
|
||||
@ -332,7 +334,7 @@ class CashTests {
|
||||
fun multiCurrency() {
|
||||
// Check we can do an atomic currency trade tx.
|
||||
transaction {
|
||||
val pounds = Cash.State(MINI_CORP.ref(3, 4, 5), 658.POUNDS, DUMMY_PUBKEY_2)
|
||||
val pounds = Cash.State(MINI_CORP.ref(3, 4, 5), 658.POUNDS, DUMMY_PUBKEY_2, DUMMY_NOTARY)
|
||||
input { inState `owned by` DUMMY_PUBKEY_1 }
|
||||
input { pounds }
|
||||
output { inState `owned by` DUMMY_PUBKEY_2 }
|
||||
@ -352,7 +354,7 @@ class CashTests {
|
||||
|
||||
fun makeCash(amount: Amount, corp: Party, depositRef: Byte = 1) =
|
||||
StateAndRef(
|
||||
Cash.State(corp.ref(depositRef), amount, OUR_PUBKEY_1),
|
||||
Cash.State(corp.ref(depositRef), amount, OUR_PUBKEY_1, DUMMY_NOTARY),
|
||||
StateRef(SecureHash.randomSHA256(), Random().nextInt(32))
|
||||
)
|
||||
|
||||
@ -374,13 +376,13 @@ class CashTests {
|
||||
val wtx = makeSpend(100.DOLLARS, THEIR_PUBKEY_1)
|
||||
assertEquals(WALLET[0].ref, wtx.inputs[0])
|
||||
assertEquals(WALLET[0].state.copy(owner = THEIR_PUBKEY_1), wtx.outputs[0])
|
||||
assertEquals(OUR_PUBKEY_1, wtx.commands[0].signers[0])
|
||||
assertEquals(OUR_PUBKEY_1, wtx.commands.single { it.value is Cash.Commands.Move }.signers[0])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun generateSimpleSpendWithParties() {
|
||||
val tx = TransactionBuilder()
|
||||
Cash().generateSpend(tx, 80.DOLLARS, ALICE, WALLET, setOf(MINI_CORP))
|
||||
Cash().generateSpend(tx, 80.DOLLARS, ALICE_PUBKEY, WALLET, setOf(MINI_CORP))
|
||||
assertEquals(WALLET[2].ref, tx.inputStates()[0])
|
||||
}
|
||||
|
||||
@ -390,7 +392,7 @@ class CashTests {
|
||||
assertEquals(WALLET[0].ref, wtx.inputs[0])
|
||||
assertEquals(WALLET[0].state.copy(owner = THEIR_PUBKEY_1, amount = 10.DOLLARS), wtx.outputs[0])
|
||||
assertEquals(WALLET[0].state.copy(amount = 90.DOLLARS), wtx.outputs[1])
|
||||
assertEquals(OUR_PUBKEY_1, wtx.commands[0].signers[0])
|
||||
assertEquals(OUR_PUBKEY_1, wtx.commands.single { it.value is Cash.Commands.Move }.signers[0])
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -399,7 +401,7 @@ class CashTests {
|
||||
assertEquals(WALLET[0].ref, wtx.inputs[0])
|
||||
assertEquals(WALLET[1].ref, wtx.inputs[1])
|
||||
assertEquals(WALLET[0].state.copy(owner = THEIR_PUBKEY_1, amount = 500.DOLLARS), wtx.outputs[0])
|
||||
assertEquals(OUR_PUBKEY_1, wtx.commands[0].signers[0])
|
||||
assertEquals(OUR_PUBKEY_1, wtx.commands.single { it.value is Cash.Commands.Move }.signers[0])
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -410,7 +412,7 @@ class CashTests {
|
||||
assertEquals(WALLET[2].ref, wtx.inputs[2])
|
||||
assertEquals(WALLET[0].state.copy(owner = THEIR_PUBKEY_1, amount = 500.DOLLARS), wtx.outputs[0])
|
||||
assertEquals(WALLET[2].state.copy(owner = THEIR_PUBKEY_1), wtx.outputs[1])
|
||||
assertEquals(OUR_PUBKEY_1, wtx.commands[0].signers[0])
|
||||
assertEquals(OUR_PUBKEY_1, wtx.commands.single { it.value is Cash.Commands.Move }.signers[0])
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -2,15 +2,11 @@ package contracts
|
||||
|
||||
import core.*
|
||||
import core.crypto.SecureHash
|
||||
import core.node.services.DummyTimestampingAuthority
|
||||
import core.node.services.TimestampingError
|
||||
import core.testutils.*
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.Parameterized
|
||||
import java.time.Clock
|
||||
import java.time.Instant
|
||||
import java.time.ZoneOffset
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
@ -26,7 +22,8 @@ class JavaCommercialPaperTest() : ICommercialPaperTestTemplate {
|
||||
MEGA_CORP.ref(123),
|
||||
MEGA_CORP_PUBKEY,
|
||||
1000.DOLLARS,
|
||||
TEST_TX_TIME + 7.days
|
||||
TEST_TX_TIME + 7.days,
|
||||
DUMMY_NOTARY
|
||||
)
|
||||
|
||||
override fun getIssueCommand(): CommandData = JavaCommercialPaper.Commands.Issue()
|
||||
@ -39,7 +36,8 @@ class KotlinCommercialPaperTest() : ICommercialPaperTestTemplate {
|
||||
issuance = MEGA_CORP.ref(123),
|
||||
owner = MEGA_CORP_PUBKEY,
|
||||
faceValue = 1000.DOLLARS,
|
||||
maturityDate = TEST_TX_TIME + 7.days
|
||||
maturityDate = TEST_TX_TIME + 7.days,
|
||||
notary = DUMMY_NOTARY
|
||||
)
|
||||
|
||||
override fun getIssueCommand(): CommandData = CommercialPaper.Commands.Issue()
|
||||
@ -108,28 +106,6 @@ class CommercialPaperTestsGeneric {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `timestamp out of range`() {
|
||||
// Check what happens if the timestamp on the transaction itself defines a range that doesn't include true
|
||||
// time as measured by a TSA (which is running five hours ahead in this test).
|
||||
CommercialPaper().generateIssue(MINI_CORP.ref(123), 10000.DOLLARS, TEST_TX_TIME + 30.days).apply {
|
||||
setTime(TEST_TX_TIME, DummyTimestampingAuthority.identity, 30.seconds)
|
||||
signWith(MINI_CORP_KEY)
|
||||
assertFailsWith(TimestampingError.NotOnTimeException::class) {
|
||||
timestamp(DummyTimestamper(Clock.fixed(TEST_TX_TIME + 5.hours, ZoneOffset.UTC)))
|
||||
}
|
||||
}
|
||||
// Check that it also fails if true time is before the threshold (we are trying to timestamp too early).
|
||||
CommercialPaper().generateIssue(MINI_CORP.ref(123), 10000.DOLLARS, TEST_TX_TIME + 30.days).apply {
|
||||
setTime(TEST_TX_TIME, DummyTimestampingAuthority.identity, 30.seconds)
|
||||
signWith(MINI_CORP_KEY)
|
||||
assertFailsWith(TimestampingError.NotOnTimeException::class) {
|
||||
val tsaClock = Clock.fixed(TEST_TX_TIME - 5.hours, ZoneOffset.UTC)
|
||||
timestamp(DummyTimestamper(tsaClock), Clock.fixed(TEST_TX_TIME, ZoneOffset.UTC))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `issue cannot replace an existing state`() {
|
||||
transactionGroup {
|
||||
@ -166,30 +142,30 @@ class CommercialPaperTestsGeneric {
|
||||
fun `issue move and then redeem`() {
|
||||
// MiniCorp issues $10,000 of commercial paper, to mature in 30 days, owned initially by itself.
|
||||
val issueTX: LedgerTransaction = run {
|
||||
val ptx = CommercialPaper().generateIssue(MINI_CORP.ref(123), 10000.DOLLARS, TEST_TX_TIME + 30.days).apply {
|
||||
setTime(TEST_TX_TIME, DummyTimestampingAuthority.identity, 30.seconds)
|
||||
val ptx = CommercialPaper().generateIssue(MINI_CORP.ref(123), 10000.DOLLARS, TEST_TX_TIME + 30.days, DUMMY_NOTARY).apply {
|
||||
setTime(TEST_TX_TIME, DUMMY_NOTARY, 30.seconds)
|
||||
signWith(MINI_CORP_KEY)
|
||||
timestamp(DUMMY_TIMESTAMPER)
|
||||
signWith(DUMMY_NOTARY_KEY)
|
||||
}
|
||||
val stx = ptx.toSignedTransaction()
|
||||
stx.verifyToLedgerTransaction(MockIdentityService, attachments)
|
||||
}
|
||||
|
||||
val (alicesWalletTX, alicesWallet) = cashOutputsToWallet(
|
||||
3000.DOLLARS.CASH `owned by` ALICE,
|
||||
3000.DOLLARS.CASH `owned by` ALICE,
|
||||
3000.DOLLARS.CASH `owned by` ALICE
|
||||
3000.DOLLARS.CASH `owned by` ALICE_PUBKEY,
|
||||
3000.DOLLARS.CASH `owned by` ALICE_PUBKEY,
|
||||
3000.DOLLARS.CASH `owned by` ALICE_PUBKEY
|
||||
)
|
||||
|
||||
// Alice pays $9000 to MiniCorp to own some of their debt.
|
||||
val moveTX: LedgerTransaction = run {
|
||||
val ptx = TransactionBuilder()
|
||||
Cash().generateSpend(ptx, 9000.DOLLARS, MINI_CORP_PUBKEY, alicesWallet)
|
||||
CommercialPaper().generateMove(ptx, issueTX.outRef(0), ALICE)
|
||||
CommercialPaper().generateMove(ptx, issueTX.outRef(0), ALICE_PUBKEY)
|
||||
ptx.signWith(MINI_CORP_KEY)
|
||||
ptx.signWith(ALICE_KEY)
|
||||
val stx = ptx.toSignedTransaction()
|
||||
stx.verifyToLedgerTransaction(MockIdentityService, attachments)
|
||||
ptx.signWith(DUMMY_NOTARY_KEY)
|
||||
ptx.toSignedTransaction().verifyToLedgerTransaction(MockIdentityService, attachments)
|
||||
}
|
||||
|
||||
// Won't be validated.
|
||||
@ -200,11 +176,11 @@ class CommercialPaperTestsGeneric {
|
||||
|
||||
fun makeRedeemTX(time: Instant): LedgerTransaction {
|
||||
val ptx = TransactionBuilder()
|
||||
ptx.setTime(time, DummyTimestampingAuthority.identity, 30.seconds)
|
||||
ptx.setTime(time, DUMMY_NOTARY, 30.seconds)
|
||||
CommercialPaper().generateRedeem(ptx, moveTX.outRef(1), corpWallet)
|
||||
ptx.signWith(ALICE_KEY)
|
||||
ptx.signWith(MINI_CORP_KEY)
|
||||
ptx.timestamp(DUMMY_TIMESTAMPER)
|
||||
ptx.signWith(DUMMY_NOTARY_KEY)
|
||||
return ptx.toSignedTransaction().verifyToLedgerTransaction(MockIdentityService, attachments)
|
||||
}
|
||||
|
||||
@ -226,7 +202,7 @@ class CommercialPaperTestsGeneric {
|
||||
val someProfits = 1200.DOLLARS
|
||||
return transactionGroupFor() {
|
||||
roots {
|
||||
transaction(900.DOLLARS.CASH `owned by` ALICE label "alice's $900")
|
||||
transaction(900.DOLLARS.CASH `owned by` ALICE_PUBKEY label "alice's $900")
|
||||
transaction(someProfits.CASH `owned by` MEGA_CORP_PUBKEY label "some profits")
|
||||
}
|
||||
|
||||
@ -243,8 +219,8 @@ class CommercialPaperTestsGeneric {
|
||||
input("paper")
|
||||
input("alice's $900")
|
||||
output("borrowed $900") { 900.DOLLARS.CASH `owned by` MEGA_CORP_PUBKEY }
|
||||
output("alice's paper") { "paper".output `owned by` ALICE }
|
||||
arg(ALICE) { Cash.Commands.Move() }
|
||||
output("alice's paper") { "paper".output `owned by` ALICE_PUBKEY }
|
||||
arg(ALICE_PUBKEY) { Cash.Commands.Move() }
|
||||
arg(MEGA_CORP_PUBKEY) { thisTest.getMoveCommand() }
|
||||
}
|
||||
|
||||
@ -254,13 +230,13 @@ class CommercialPaperTestsGeneric {
|
||||
input("alice's paper")
|
||||
input("some profits")
|
||||
|
||||
output("Alice's profit") { aliceGetsBack.CASH `owned by` ALICE }
|
||||
output("Alice's profit") { aliceGetsBack.CASH `owned by` ALICE_PUBKEY }
|
||||
output("Change") { (someProfits - aliceGetsBack).CASH `owned by` MEGA_CORP_PUBKEY }
|
||||
if (!destroyPaperAtRedemption)
|
||||
output { "paper".output }
|
||||
|
||||
arg(MEGA_CORP_PUBKEY) { Cash.Commands.Move() }
|
||||
arg(ALICE) { thisTest.getRedeemCommand() }
|
||||
arg(ALICE_PUBKEY) { thisTest.getRedeemCommand() }
|
||||
|
||||
timestamp(redemptionTime)
|
||||
}
|
||||
|
@ -2,7 +2,6 @@ package contracts
|
||||
|
||||
import core.*
|
||||
import core.crypto.SecureHash
|
||||
import core.node.services.DummyTimestampingAuthority
|
||||
import core.testutils.*
|
||||
import org.junit.Test
|
||||
import java.time.Instant
|
||||
@ -19,7 +18,8 @@ class CrowdFundTests {
|
||||
closingTime = TEST_TX_TIME + 7.days
|
||||
),
|
||||
closed = false,
|
||||
pledges = ArrayList<CrowdFund.Pledge>()
|
||||
pledges = ArrayList<CrowdFund.Pledge>(),
|
||||
notary = DUMMY_NOTARY
|
||||
)
|
||||
|
||||
val attachments = MockStorageService().attachments
|
||||
@ -58,7 +58,7 @@ class CrowdFundTests {
|
||||
private fun raiseFunds(): TransactionGroupDSL<CrowdFund.State> {
|
||||
return transactionGroupFor {
|
||||
roots {
|
||||
transaction(1000.DOLLARS.CASH `owned by` ALICE label "alice's $1000")
|
||||
transaction(1000.DOLLARS.CASH `owned by` ALICE_PUBKEY label "alice's $1000")
|
||||
}
|
||||
|
||||
// 1. Create the funding opportunity
|
||||
@ -74,12 +74,12 @@ class CrowdFundTests {
|
||||
input("alice's $1000")
|
||||
output ("pledged opportunity") {
|
||||
CF_1.copy(
|
||||
pledges = CF_1.pledges + CrowdFund.Pledge(ALICE, 1000.DOLLARS)
|
||||
pledges = CF_1.pledges + CrowdFund.Pledge(ALICE_PUBKEY, 1000.DOLLARS)
|
||||
)
|
||||
}
|
||||
output { 1000.DOLLARS.CASH `owned by` MINI_CORP_PUBKEY }
|
||||
arg(ALICE) { Cash.Commands.Move() }
|
||||
arg(ALICE) { CrowdFund.Commands.Pledge() }
|
||||
arg(ALICE_PUBKEY) { Cash.Commands.Move() }
|
||||
arg(ALICE_PUBKEY) { CrowdFund.Commands.Pledge() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
}
|
||||
|
||||
@ -103,33 +103,31 @@ class CrowdFundTests {
|
||||
// MiniCorp registers a crowdfunding of $1,000, to close in 7 days.
|
||||
val registerTX: LedgerTransaction = run {
|
||||
// craftRegister returns a partial transaction
|
||||
val ptx = CrowdFund().generateRegister(MINI_CORP.ref(123), 1000.DOLLARS, "crowd funding", TEST_TX_TIME + 7.days).apply {
|
||||
setTime(TEST_TX_TIME, DummyTimestampingAuthority.identity, 30.seconds)
|
||||
val ptx = CrowdFund().generateRegister(MINI_CORP.ref(123), 1000.DOLLARS, "crowd funding", TEST_TX_TIME + 7.days, DUMMY_NOTARY).apply {
|
||||
setTime(TEST_TX_TIME, DUMMY_NOTARY, 30.seconds)
|
||||
signWith(MINI_CORP_KEY)
|
||||
timestamp(DUMMY_TIMESTAMPER)
|
||||
signWith(DUMMY_NOTARY_KEY)
|
||||
}
|
||||
val stx = ptx.toSignedTransaction()
|
||||
stx.verifyToLedgerTransaction(MockIdentityService, attachments)
|
||||
ptx.toSignedTransaction().verifyToLedgerTransaction(MockIdentityService, attachments)
|
||||
}
|
||||
|
||||
// let's give Alice some funds that she can invest
|
||||
val (aliceWalletTX, aliceWallet) = cashOutputsToWallet(
|
||||
200.DOLLARS.CASH `owned by` ALICE,
|
||||
500.DOLLARS.CASH `owned by` ALICE,
|
||||
300.DOLLARS.CASH `owned by` ALICE
|
||||
200.DOLLARS.CASH `owned by` ALICE_PUBKEY,
|
||||
500.DOLLARS.CASH `owned by` ALICE_PUBKEY,
|
||||
300.DOLLARS.CASH `owned by` ALICE_PUBKEY
|
||||
)
|
||||
|
||||
// Alice pays $1000 to MiniCorp to fund their campaign.
|
||||
val pledgeTX: LedgerTransaction = run {
|
||||
val ptx = TransactionBuilder()
|
||||
CrowdFund().generatePledge(ptx, registerTX.outRef(0), ALICE)
|
||||
CrowdFund().generatePledge(ptx, registerTX.outRef(0), ALICE_PUBKEY)
|
||||
Cash().generateSpend(ptx, 1000.DOLLARS, MINI_CORP_PUBKEY, aliceWallet)
|
||||
ptx.setTime(TEST_TX_TIME, DummyTimestampingAuthority.identity, 30.seconds)
|
||||
ptx.setTime(TEST_TX_TIME, DUMMY_NOTARY, 30.seconds)
|
||||
ptx.signWith(ALICE_KEY)
|
||||
ptx.timestamp(DUMMY_TIMESTAMPER)
|
||||
val stx = ptx.toSignedTransaction()
|
||||
ptx.signWith(DUMMY_NOTARY_KEY)
|
||||
// this verify passes - the transaction contains an output cash, necessary to verify the fund command
|
||||
stx.verifyToLedgerTransaction(MockIdentityService, attachments)
|
||||
ptx.toSignedTransaction().verifyToLedgerTransaction(MockIdentityService, attachments)
|
||||
}
|
||||
|
||||
// Won't be validated.
|
||||
@ -140,12 +138,11 @@ class CrowdFundTests {
|
||||
// MiniCorp closes their campaign.
|
||||
fun makeFundedTX(time: Instant): LedgerTransaction {
|
||||
val ptx = TransactionBuilder()
|
||||
ptx.setTime(time, DUMMY_TIMESTAMPER.identity, 30.seconds)
|
||||
ptx.setTime(time, DUMMY_NOTARY, 30.seconds)
|
||||
CrowdFund().generateClose(ptx, pledgeTX.outRef(0), miniCorpWallet)
|
||||
ptx.signWith(MINI_CORP_KEY)
|
||||
ptx.timestamp(DUMMY_TIMESTAMPER)
|
||||
val stx = ptx.toSignedTransaction()
|
||||
return stx.verifyToLedgerTransaction(MockIdentityService, attachments)
|
||||
ptx.signWith(DUMMY_NOTARY_KEY)
|
||||
return ptx.toSignedTransaction().verifyToLedgerTransaction(MockIdentityService, attachments)
|
||||
}
|
||||
|
||||
val tooEarlyClose = makeFundedTX(TEST_TX_TIME + 6.days)
|
||||
|
@ -1,7 +1,6 @@
|
||||
package contracts
|
||||
|
||||
import core.*
|
||||
import core.node.services.DummyTimestampingAuthority
|
||||
import core.testutils.*
|
||||
import org.junit.Test
|
||||
import java.math.BigDecimal
|
||||
@ -96,7 +95,7 @@ fun createDummyIRS(irsSelect: Int): InterestRateSwap.State {
|
||||
dailyInterestAmount = Expression("(CashAmount * InterestRate ) / (fixedLeg.notional.currency.currencyCode.equals('GBP')) ? 365 : 360")
|
||||
)
|
||||
|
||||
InterestRateSwap.State(fixedLeg = fixedLeg, floatingLeg = floatingLeg, calculation = calculation, common = common)
|
||||
InterestRateSwap.State(fixedLeg = fixedLeg, floatingLeg = floatingLeg, calculation = calculation, common = common, notary = DUMMY_NOTARY)
|
||||
}
|
||||
2 -> {
|
||||
// 10y swap, we pay 1.3% fixed 30/360 semi, rec 3m usd libor act/360 Q on 25m notional (mod foll/adj on both sides)
|
||||
@ -186,7 +185,7 @@ fun createDummyIRS(irsSelect: Int): InterestRateSwap.State {
|
||||
dailyInterestAmount = Expression("(CashAmount * InterestRate ) / (fixedLeg.notional.currency.currencyCode.equals('GBP')) ? 365 : 360")
|
||||
)
|
||||
|
||||
return InterestRateSwap.State(fixedLeg = fixedLeg, floatingLeg = floatingLeg, calculation = calculation, common = common)
|
||||
return InterestRateSwap.State(fixedLeg = fixedLeg, floatingLeg = floatingLeg, calculation = calculation, common = common, notary = DUMMY_NOTARY)
|
||||
|
||||
}
|
||||
else -> TODO("IRS number $irsSelect not defined")
|
||||
@ -203,7 +202,8 @@ class IRSTests {
|
||||
exampleIRS.fixedLeg,
|
||||
exampleIRS.floatingLeg,
|
||||
exampleIRS.calculation,
|
||||
exampleIRS.common
|
||||
exampleIRS.common,
|
||||
DUMMY_NOTARY
|
||||
)
|
||||
|
||||
val outState = inState.copy()
|
||||
@ -228,15 +228,14 @@ class IRSTests {
|
||||
fixedLeg = dummyIRS.fixedLeg,
|
||||
floatingLeg = dummyIRS.floatingLeg,
|
||||
calculation = dummyIRS.calculation,
|
||||
common = dummyIRS.common).apply {
|
||||
setTime(TEST_TX_TIME, DummyTimestampingAuthority.identity, 30.seconds)
|
||||
common = dummyIRS.common,
|
||||
notary = DUMMY_NOTARY).apply {
|
||||
setTime(TEST_TX_TIME, DUMMY_NOTARY, 30.seconds)
|
||||
signWith(MEGA_CORP_KEY)
|
||||
signWith(MINI_CORP_KEY)
|
||||
timestamp(DUMMY_TIMESTAMPER)
|
||||
signWith(DUMMY_NOTARY_KEY)
|
||||
}
|
||||
|
||||
val stx = gtx.toSignedTransaction()
|
||||
stx.verifyToLedgerTransaction(MockIdentityService, attachments)
|
||||
gtx.toSignedTransaction().verifyToLedgerTransaction(MockIdentityService, attachments)
|
||||
}
|
||||
return genTX
|
||||
}
|
||||
@ -286,7 +285,7 @@ class IRSTests {
|
||||
newCalculation = newCalculation.applyFixing(it.key, FixedRate(PercentageRatioUnit(it.value)))
|
||||
}
|
||||
|
||||
val newIRS = InterestRateSwap.State(irs.fixedLeg, irs.floatingLeg, newCalculation, irs.common)
|
||||
val newIRS = InterestRateSwap.State(irs.fixedLeg, irs.floatingLeg, newCalculation, irs.common, DUMMY_NOTARY)
|
||||
println(newIRS.exportIRSToCSV())
|
||||
}
|
||||
|
||||
@ -315,13 +314,12 @@ class IRSTests {
|
||||
val fixing = Pair(nextFixingDate, FixedRate("0.052".percent))
|
||||
InterestRateSwap().generateFix(tx, previousTXN.outRef(0), fixing)
|
||||
with(tx) {
|
||||
setTime(TEST_TX_TIME, DummyTimestampingAuthority.identity, 30.seconds)
|
||||
setTime(TEST_TX_TIME, DUMMY_NOTARY, 30.seconds)
|
||||
signWith(MEGA_CORP_KEY)
|
||||
signWith(MINI_CORP_KEY)
|
||||
timestamp(DUMMY_TIMESTAMPER)
|
||||
signWith(DUMMY_NOTARY_KEY)
|
||||
}
|
||||
val stx = tx.toSignedTransaction()
|
||||
stx.verifyToLedgerTransaction(MockIdentityService, attachments)
|
||||
tx.toSignedTransaction().verifyToLedgerTransaction(MockIdentityService, attachments)
|
||||
}
|
||||
currentIRS = previousTXN.outputs.filterIsInstance<InterestRateSwap.State>().single()
|
||||
println(currentIRS.prettyPrint())
|
||||
|
@ -1,18 +1,19 @@
|
||||
package core
|
||||
|
||||
import com.codahale.metrics.MetricRegistry
|
||||
import core.crypto.*
|
||||
import core.crypto.SecureHash
|
||||
import core.crypto.generateKeyPair
|
||||
import core.crypto.sha256
|
||||
import core.messaging.MessagingService
|
||||
import core.node.ServiceHub
|
||||
import core.node.services.*
|
||||
import core.node.storage.Checkpoint
|
||||
import core.node.storage.CheckpointStorage
|
||||
import core.node.subsystems.*
|
||||
import core.serialization.SerializedBytes
|
||||
import core.serialization.deserialize
|
||||
import core.node.services.AttachmentStorage
|
||||
import core.node.services.IdentityService
|
||||
import core.node.services.NetworkMapService
|
||||
import core.testing.MockNetworkMapCache
|
||||
import core.testutils.MockIdentityService
|
||||
import core.testutils.TEST_TX_TIME
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.File
|
||||
@ -21,32 +22,11 @@ import java.security.KeyPair
|
||||
import java.security.PrivateKey
|
||||
import java.security.PublicKey
|
||||
import java.time.Clock
|
||||
import java.time.Duration
|
||||
import java.time.ZoneId
|
||||
import java.util.*
|
||||
import java.util.concurrent.ConcurrentLinkedQueue
|
||||
import java.util.jar.JarInputStream
|
||||
import javax.annotation.concurrent.ThreadSafe
|
||||
|
||||
/**
|
||||
* A test/mock timestamping service that doesn't use any signatures or security. It timestamps with
|
||||
* the provided clock which defaults to [TEST_TX_TIME], an arbitrary point on the timeline.
|
||||
*/
|
||||
class DummyTimestamper(var clock: Clock = Clock.fixed(TEST_TX_TIME, ZoneId.systemDefault()),
|
||||
val tolerance: Duration = 30.seconds) : TimestamperService {
|
||||
override val identity = DummyTimestampingAuthority.identity
|
||||
|
||||
override fun timestamp(wtxBytes: SerializedBytes<WireTransaction>): DigitalSignature.LegallyIdentifiable {
|
||||
val wtx = wtxBytes.deserialize()
|
||||
val timestamp = wtx.commands.mapNotNull { it.value as? TimestampCommand }.single()
|
||||
if (timestamp.before!! until clock.instant() > tolerance)
|
||||
throw TimestampingError.NotOnTimeException()
|
||||
return DummyTimestampingAuthority.key.signWithECDSA(wtxBytes.bits, identity)
|
||||
}
|
||||
}
|
||||
|
||||
val DUMMY_TIMESTAMPER = DummyTimestamper()
|
||||
|
||||
class MockKeyManagementService(vararg initialKeys: KeyPair) : KeyManagementService {
|
||||
override val keys: MutableMap<PublicKey, PrivateKey>
|
||||
|
||||
|
@ -1,7 +1,6 @@
|
||||
package core
|
||||
|
||||
import contracts.Cash
|
||||
import core.crypto.SecureHash
|
||||
import core.testutils.*
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertEquals
|
||||
@ -9,7 +8,7 @@ import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertNotEquals
|
||||
|
||||
class TransactionGroupTests {
|
||||
val A_THOUSAND_POUNDS = Cash.State(MINI_CORP.ref(1, 2, 3), 1000.POUNDS, MINI_CORP_PUBKEY)
|
||||
val A_THOUSAND_POUNDS = Cash.State(MINI_CORP.ref(1, 2, 3), 1000.POUNDS, MINI_CORP_PUBKEY, DUMMY_NOTARY)
|
||||
|
||||
@Test
|
||||
fun success() {
|
||||
@ -20,13 +19,13 @@ class TransactionGroupTests {
|
||||
|
||||
transaction {
|
||||
input("£1000")
|
||||
output("alice's £1000") { A_THOUSAND_POUNDS `owned by` ALICE }
|
||||
output("alice's £1000") { A_THOUSAND_POUNDS `owned by` ALICE_PUBKEY }
|
||||
arg(MINI_CORP_PUBKEY) { Cash.Commands.Move() }
|
||||
}
|
||||
|
||||
transaction {
|
||||
input("alice's £1000")
|
||||
arg(ALICE) { Cash.Commands.Move() }
|
||||
arg(ALICE_PUBKEY) { Cash.Commands.Move() }
|
||||
arg(MINI_CORP_PUBKEY) { Cash.Commands.Exit(1000.POUNDS) }
|
||||
}
|
||||
|
||||
@ -44,7 +43,7 @@ class TransactionGroupTests {
|
||||
|
||||
val conflict1 = transaction {
|
||||
input("cash")
|
||||
val HALF = A_THOUSAND_POUNDS.copy(amount = 500.POUNDS) `owned by` BOB
|
||||
val HALF = A_THOUSAND_POUNDS.copy(amount = 500.POUNDS) `owned by` BOB_PUBKEY
|
||||
output { HALF }
|
||||
output { HALF }
|
||||
arg(MINI_CORP_PUBKEY) { Cash.Commands.Move() }
|
||||
@ -55,7 +54,7 @@ class TransactionGroupTests {
|
||||
// Alice tries to double spend back to herself.
|
||||
val conflict2 = transaction {
|
||||
input("cash")
|
||||
val HALF = A_THOUSAND_POUNDS.copy(amount = 500.POUNDS) `owned by` ALICE
|
||||
val HALF = A_THOUSAND_POUNDS.copy(amount = 500.POUNDS) `owned by` ALICE_PUBKEY
|
||||
output { HALF }
|
||||
output { HALF }
|
||||
arg(MINI_CORP_PUBKEY) { Cash.Commands.Move() }
|
||||
@ -82,23 +81,23 @@ class TransactionGroupTests {
|
||||
|
||||
transaction {
|
||||
input("cash")
|
||||
output { A_THOUSAND_POUNDS `owned by` BOB }
|
||||
output { A_THOUSAND_POUNDS `owned by` BOB_PUBKEY }
|
||||
}
|
||||
}
|
||||
|
||||
// We have to do this manually without the DSL because transactionGroup { } won't let us create a tx that
|
||||
// points nowhere.
|
||||
val ref = StateRef(SecureHash.randomSHA256(), 0)
|
||||
val input = generateStateRef()
|
||||
tg.txns += TransactionBuilder().apply {
|
||||
addInputState(ref)
|
||||
addInputState(input)
|
||||
addOutputState(A_THOUSAND_POUNDS)
|
||||
addCommand(Cash.Commands.Move(), BOB)
|
||||
addCommand(Cash.Commands.Move(), BOB_PUBKEY)
|
||||
}.toWireTransaction()
|
||||
|
||||
val e = assertFailsWith(TransactionResolutionException::class) {
|
||||
tg.verify()
|
||||
}
|
||||
assertEquals(e.hash, ref.txhash)
|
||||
assertEquals(e.hash, input.txhash)
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -132,19 +131,21 @@ class TransactionGroupTests {
|
||||
|
||||
transaction {
|
||||
input("£1000")
|
||||
output("alice's £1000") { A_THOUSAND_POUNDS `owned by` ALICE }
|
||||
output("alice's £1000") { A_THOUSAND_POUNDS `owned by` ALICE_PUBKEY }
|
||||
arg(MINI_CORP_PUBKEY) { Cash.Commands.Move() }
|
||||
}
|
||||
|
||||
transaction {
|
||||
input("alice's £1000")
|
||||
arg(ALICE) { Cash.Commands.Move() }
|
||||
arg(ALICE_PUBKEY) { Cash.Commands.Move() }
|
||||
arg(MINI_CORP_PUBKEY) { Cash.Commands.Exit(1000.POUNDS) }
|
||||
}
|
||||
}.signAll()
|
||||
|
||||
// Now go through the conversion -> verification path with them.
|
||||
val ltxns = signedTxns.map { it.verifyToLedgerTransaction(MockIdentityService, MockStorageService().attachments) }.toSet()
|
||||
val ltxns = signedTxns.map {
|
||||
it.verifyToLedgerTransaction(MockIdentityService, MockStorageService().attachments)
|
||||
}.toSet()
|
||||
TransactionGroup(ltxns, emptySet()).verify()
|
||||
}
|
||||
}
|
@ -7,8 +7,8 @@ import core.node.NodeConfiguration
|
||||
import core.node.NodeInfo
|
||||
import core.node.services.NetworkMapService
|
||||
import core.node.services.NodeAttachmentService
|
||||
import core.node.services.NotaryService
|
||||
import core.node.services.ServiceType
|
||||
import core.node.services.TimestamperService
|
||||
import core.serialization.OpaqueBytes
|
||||
import core.testing.MockNetwork
|
||||
import core.testutils.rootCauseExceptions
|
||||
@ -23,6 +23,7 @@ import java.nio.ByteBuffer
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.StandardOpenOption
|
||||
import java.security.KeyPair
|
||||
import java.util.jar.JarOutputStream
|
||||
import java.util.zip.ZipEntry
|
||||
import kotlin.test.assertEquals
|
||||
@ -90,8 +91,8 @@ class AttachmentTests {
|
||||
// Make a node that doesn't do sanity checking at load time.
|
||||
val n0 = network.createNode(null, -1, object : MockNetwork.Factory {
|
||||
override fun create(dir: Path, config: NodeConfiguration, network: MockNetwork, networkMapAddr: NodeInfo?,
|
||||
advertisedServices: Set<ServiceType>, id: Int): MockNetwork.MockNode {
|
||||
return object : MockNetwork.MockNode(dir, config, network, networkMapAddr, advertisedServices, id) {
|
||||
advertisedServices: Set<ServiceType>, id: Int, keyPair: KeyPair?): MockNetwork.MockNode {
|
||||
return object : MockNetwork.MockNode(dir, config, network, networkMapAddr, advertisedServices, id, keyPair) {
|
||||
override fun start(): MockNetwork.MockNode {
|
||||
super.start()
|
||||
(storage.attachments as NodeAttachmentService).checkAttachmentsOnLoad = false
|
||||
@ -99,7 +100,7 @@ class AttachmentTests {
|
||||
}
|
||||
}
|
||||
}
|
||||
}, NetworkMapService.Type, TimestamperService.Type)
|
||||
}, null, null, NetworkMapService.Type, NotaryService.Type)
|
||||
val n1 = network.createNode(n0.info)
|
||||
|
||||
// Insert an attachment into node zero's store directly.
|
||||
|
@ -64,17 +64,21 @@ class TwoPartyTradeProtocolTests {
|
||||
// allow interruption half way through.
|
||||
net = MockNetwork(true)
|
||||
transactionGroupFor<ContractState> {
|
||||
val (aliceNode, bobNode) = net.createTwoNodes()
|
||||
(bobNode.wallet as NodeWalletService).fillWithSomeTestCash(2000.DOLLARS)
|
||||
val alicesFakePaper = fillUpForSeller(false, aliceNode.info.identity, null).second
|
||||
val notaryNode = net.createNotaryNode(DUMMY_NOTARY.name, DUMMY_NOTARY_KEY)
|
||||
val aliceNode = net.createPartyNode(notaryNode.info, ALICE.name, ALICE_KEY)
|
||||
val bobNode = net.createPartyNode(notaryNode.info, BOB.name, BOB_KEY)
|
||||
|
||||
insertFakeTransactions(alicesFakePaper, aliceNode.services, aliceNode.storage.myLegalIdentityKey)
|
||||
(bobNode.wallet as NodeWalletService).fillWithSomeTestCash(DUMMY_NOTARY, 2000.DOLLARS)
|
||||
val alicesFakePaper = fillUpForSeller(false, aliceNode.storage.myLegalIdentity.owningKey,
|
||||
notaryNode.info.identity, null).second
|
||||
|
||||
insertFakeTransactions(alicesFakePaper, aliceNode.services, aliceNode.storage.myLegalIdentityKey, notaryNode.storage.myLegalIdentityKey)
|
||||
|
||||
val buyerSessionID = random63BitValue()
|
||||
|
||||
val aliceResult = TwoPartyTradeProtocol.runSeller(
|
||||
aliceNode.smm,
|
||||
aliceNode.info,
|
||||
notaryNode.info,
|
||||
bobNode.net.myAddress,
|
||||
lookup("alice's paper"),
|
||||
1000.DOLLARS,
|
||||
@ -83,7 +87,7 @@ class TwoPartyTradeProtocolTests {
|
||||
)
|
||||
val bobResult = TwoPartyTradeProtocol.runBuyer(
|
||||
bobNode.smm,
|
||||
aliceNode.info,
|
||||
notaryNode.info,
|
||||
aliceNode.net.myAddress,
|
||||
1000.DOLLARS,
|
||||
CommercialPaper.State::class.java,
|
||||
@ -105,25 +109,26 @@ class TwoPartyTradeProtocolTests {
|
||||
@Test
|
||||
fun `shutdown and restore`() {
|
||||
transactionGroupFor<ContractState> {
|
||||
var (aliceNode, bobNode) = net.createTwoNodes()
|
||||
val notaryNode = net.createNotaryNode(DUMMY_NOTARY.name, DUMMY_NOTARY_KEY)
|
||||
val aliceNode = net.createPartyNode(notaryNode.info, ALICE.name, ALICE_KEY)
|
||||
var bobNode = net.createPartyNode(notaryNode.info, BOB.name, BOB_KEY)
|
||||
|
||||
val aliceAddr = aliceNode.net.myAddress
|
||||
val bobAddr = bobNode.net.myAddress as InMemoryMessagingNetwork.Handle
|
||||
val networkMapAddr = aliceNode.info
|
||||
val timestamperAddr = aliceNode.info
|
||||
val networkMapAddr = notaryNode.info
|
||||
|
||||
// Clear network map registration messages through before we start
|
||||
net.runNetwork()
|
||||
|
||||
(bobNode.wallet as NodeWalletService).fillWithSomeTestCash(2000.DOLLARS)
|
||||
val alicesFakePaper = fillUpForSeller(false, timestamperAddr.identity, null).second
|
||||
net.runNetwork() // Clear network map registration messages
|
||||
|
||||
(bobNode.wallet as NodeWalletService).fillWithSomeTestCash(DUMMY_NOTARY, 2000.DOLLARS)
|
||||
val alicesFakePaper = fillUpForSeller(false, aliceNode.storage.myLegalIdentity.owningKey,
|
||||
notaryNode.info.identity, null).second
|
||||
insertFakeTransactions(alicesFakePaper, aliceNode.services, aliceNode.storage.myLegalIdentityKey)
|
||||
|
||||
val buyerSessionID = random63BitValue()
|
||||
|
||||
val aliceFuture = TwoPartyTradeProtocol.runSeller(
|
||||
aliceNode.smm,
|
||||
timestamperAddr,
|
||||
notaryNode.info,
|
||||
bobAddr,
|
||||
lookup("alice's paper"),
|
||||
1000.DOLLARS,
|
||||
@ -132,7 +137,7 @@ class TwoPartyTradeProtocolTests {
|
||||
)
|
||||
TwoPartyTradeProtocol.runBuyer(
|
||||
bobNode.smm,
|
||||
timestamperAddr,
|
||||
notaryNode.info,
|
||||
aliceAddr,
|
||||
1000.DOLLARS,
|
||||
CommercialPaper.State::class.java,
|
||||
@ -167,10 +172,10 @@ class TwoPartyTradeProtocolTests {
|
||||
// that Bob was waiting on before the reboot occurred.
|
||||
bobNode = net.createNode(networkMapAddr, bobAddr.id, object : MockNetwork.Factory {
|
||||
override fun create(dir: Path, config: NodeConfiguration, network: MockNetwork, networkMapAddr: NodeInfo?,
|
||||
advertisedServices: Set<ServiceType>, id: Int): MockNetwork.MockNode {
|
||||
return MockNetwork.MockNode(dir, config, network, networkMapAddr, advertisedServices, bobAddr.id)
|
||||
advertisedServices: Set<ServiceType>, id: Int, keyPair: KeyPair?): MockNetwork.MockNode {
|
||||
return MockNetwork.MockNode(dir, config, network, networkMapAddr, advertisedServices, bobAddr.id, BOB_KEY)
|
||||
}
|
||||
})
|
||||
}, BOB.name, BOB_KEY)
|
||||
|
||||
// Find the future representing the result of this state machine again.
|
||||
var bobFuture = bobNode.smm.findStateMachines(TwoPartyTradeProtocol.Buyer::class.java).single().second
|
||||
@ -187,12 +192,12 @@ class TwoPartyTradeProtocolTests {
|
||||
|
||||
// Creates a mock node with an overridden storage service that uses a RecordingMap, that lets us test the order
|
||||
// of gets and puts.
|
||||
private fun makeNodeWithTracking(name: String): MockNetwork.MockNode {
|
||||
private fun makeNodeWithTracking(networkMapAddr: NodeInfo?, name: String, keyPair: KeyPair): MockNetwork.MockNode {
|
||||
// Create a node in the mock network ...
|
||||
return net.createNode(null, nodeFactory = object : MockNetwork.Factory {
|
||||
return net.createNode(networkMapAddr, -1, object : MockNetwork.Factory {
|
||||
override fun create(dir: Path, config: NodeConfiguration, network: MockNetwork, networkMapAddr: NodeInfo?,
|
||||
advertisedServices: Set<ServiceType>, id: Int): MockNetwork.MockNode {
|
||||
return object : MockNetwork.MockNode(dir, config, network, networkMapAddr, advertisedServices, id) {
|
||||
advertisedServices: Set<ServiceType>, id: Int, keyPair: KeyPair?): MockNetwork.MockNode {
|
||||
return object : MockNetwork.MockNode(dir, config, network, networkMapAddr, advertisedServices, id, keyPair) {
|
||||
// That constructs the storage service object in a customised way ...
|
||||
override fun constructStorageService(attachments: NodeAttachmentService, checkpointStorage: CheckpointStorage, keypair: KeyPair, identity: Party): StorageServiceImpl {
|
||||
// To use RecordingMaps instead of ordinary HashMaps.
|
||||
@ -200,15 +205,15 @@ class TwoPartyTradeProtocolTests {
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}, name, keyPair)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun checkDependenciesOfSaleAssetAreResolved() {
|
||||
transactionGroupFor<ContractState> {
|
||||
val aliceNode = makeNodeWithTracking("alice")
|
||||
val timestamperAddr = aliceNode.info
|
||||
val bobNode = makeNodeWithTracking("bob")
|
||||
val notaryNode = net.createNotaryNode(DUMMY_NOTARY.name, DUMMY_NOTARY_KEY)
|
||||
val aliceNode = makeNodeWithTracking(notaryNode.info, ALICE.name, ALICE_KEY)
|
||||
val bobNode = makeNodeWithTracking(notaryNode.info, BOB.name, BOB_KEY)
|
||||
|
||||
// Insert a prospectus type attachment into the commercial paper transaction.
|
||||
val stream = ByteArrayOutputStream()
|
||||
@ -221,14 +226,17 @@ class TwoPartyTradeProtocolTests {
|
||||
|
||||
val bobsFakeCash = fillUpForBuyer(false, bobNode.keyManagement.freshKey().public).second
|
||||
val bobsSignedTxns = insertFakeTransactions(bobsFakeCash, bobNode.services)
|
||||
val alicesFakePaper = fillUpForSeller(false, timestamperAddr.identity, attachmentID).second
|
||||
val alicesFakePaper = fillUpForSeller(false, aliceNode.storage.myLegalIdentity.owningKey,
|
||||
notaryNode.info.identity, attachmentID).second
|
||||
val alicesSignedTxns = insertFakeTransactions(alicesFakePaper, aliceNode.services, aliceNode.storage.myLegalIdentityKey)
|
||||
|
||||
val buyerSessionID = random63BitValue()
|
||||
|
||||
net.runNetwork() // Clear network map registration messages
|
||||
|
||||
TwoPartyTradeProtocol.runSeller(
|
||||
aliceNode.smm,
|
||||
timestamperAddr,
|
||||
notaryNode.info,
|
||||
bobNode.net.myAddress,
|
||||
lookup("alice's paper"),
|
||||
1000.DOLLARS,
|
||||
@ -237,7 +245,7 @@ class TwoPartyTradeProtocolTests {
|
||||
)
|
||||
TwoPartyTradeProtocol.runBuyer(
|
||||
bobNode.smm,
|
||||
timestamperAddr,
|
||||
notaryNode.info,
|
||||
aliceNode.net.myAddress,
|
||||
1000.DOLLARS,
|
||||
CommercialPaper.State::class.java,
|
||||
@ -292,6 +300,10 @@ class TwoPartyTradeProtocolTests {
|
||||
// looking up the states again.
|
||||
RecordingMap.Get(bobsFakeCash[1].id),
|
||||
RecordingMap.Get(bobsFakeCash[2].id),
|
||||
RecordingMap.Get(alicesFakePaper[0].id),
|
||||
// Alice needs to look up the input states to find out which Notary they point to
|
||||
RecordingMap.Get(bobsFakeCash[1].id),
|
||||
RecordingMap.Get(bobsFakeCash[2].id),
|
||||
RecordingMap.Get(alicesFakePaper[0].id)
|
||||
)
|
||||
assertEquals(expected, records)
|
||||
@ -315,23 +327,27 @@ class TwoPartyTradeProtocolTests {
|
||||
|
||||
private fun TransactionGroupDSL<ContractState>.runWithError(bobError: Boolean, aliceError: Boolean,
|
||||
expectedMessageSubstring: String) {
|
||||
var (aliceNode, bobNode) = net.createTwoNodes()
|
||||
val notaryNode = net.createNotaryNode(DUMMY_NOTARY.name, DUMMY_NOTARY_KEY)
|
||||
val aliceNode = net.createPartyNode(notaryNode.info, ALICE.name, ALICE_KEY)
|
||||
val bobNode = net.createPartyNode(notaryNode.info, BOB.name, BOB_KEY)
|
||||
|
||||
val aliceAddr = aliceNode.net.myAddress
|
||||
val bobAddr = bobNode.net.myAddress as InMemoryMessagingNetwork.Handle
|
||||
val timestamperAddr = aliceNode.info
|
||||
|
||||
val bobKey = bobNode.keyManagement.freshKey()
|
||||
val bobsBadCash = fillUpForBuyer(bobError, bobKey.public).second
|
||||
val alicesFakePaper = fillUpForSeller(aliceError, timestamperAddr.identity, null).second
|
||||
val alicesFakePaper = fillUpForSeller(aliceError, aliceNode.storage.myLegalIdentity.owningKey, notaryNode.info.identity, null).second
|
||||
|
||||
insertFakeTransactions(bobsBadCash, bobNode.services, bobNode.storage.myLegalIdentityKey, bobKey)
|
||||
insertFakeTransactions(bobsBadCash, bobNode.services, bobNode.storage.myLegalIdentityKey, bobNode.storage.myLegalIdentityKey)
|
||||
insertFakeTransactions(alicesFakePaper, aliceNode.services, aliceNode.storage.myLegalIdentityKey)
|
||||
|
||||
val buyerSessionID = random63BitValue()
|
||||
|
||||
net.runNetwork() // Clear network map registration messages
|
||||
|
||||
val aliceResult = TwoPartyTradeProtocol.runSeller(
|
||||
aliceNode.smm,
|
||||
timestamperAddr,
|
||||
notaryNode.info,
|
||||
bobAddr,
|
||||
lookup("alice's paper"),
|
||||
1000.DOLLARS,
|
||||
@ -340,7 +356,7 @@ class TwoPartyTradeProtocolTests {
|
||||
)
|
||||
val bobResult = TwoPartyTradeProtocol.runBuyer(
|
||||
bobNode.smm,
|
||||
timestamperAddr,
|
||||
notaryNode.info,
|
||||
aliceAddr,
|
||||
1000.DOLLARS,
|
||||
CommercialPaper.State::class.java,
|
||||
@ -367,7 +383,7 @@ class TwoPartyTradeProtocolTests {
|
||||
return signed.associateBy { it.id }
|
||||
}
|
||||
|
||||
private fun TransactionGroupDSL<ContractState>.fillUpForBuyer(withError: Boolean, bobKey: PublicKey = BOB): Pair<Wallet, List<WireTransaction>> {
|
||||
private fun TransactionGroupDSL<ContractState>.fillUpForBuyer(withError: Boolean, owner: PublicKey = BOB_PUBKEY): Pair<Wallet, List<WireTransaction>> {
|
||||
// Bob (Buyer) has some cash he got from the Bank of Elbonia, Alice (Seller) has some commercial paper she
|
||||
// wants to sell to Bob.
|
||||
|
||||
@ -383,13 +399,13 @@ class TwoPartyTradeProtocolTests {
|
||||
// Bob gets some cash onto the ledger from BoE
|
||||
val bc1 = transaction {
|
||||
input("elbonian money 1")
|
||||
output("bob cash 1") { 800.DOLLARS.CASH `issued by` MEGA_CORP `owned by` bobKey }
|
||||
output("bob cash 1") { 800.DOLLARS.CASH `issued by` MEGA_CORP `owned by` owner }
|
||||
arg(MEGA_CORP_PUBKEY) { Cash.Commands.Move() }
|
||||
}
|
||||
|
||||
val bc2 = transaction {
|
||||
input("elbonian money 2")
|
||||
output("bob cash 2") { 300.DOLLARS.CASH `issued by` MEGA_CORP `owned by` bobKey }
|
||||
output("bob cash 2") { 300.DOLLARS.CASH `issued by` MEGA_CORP `owned by` owner }
|
||||
output { 700.DOLLARS.CASH `issued by` MEGA_CORP `owned by` MEGA_CORP_PUBKEY } // Change output.
|
||||
arg(MEGA_CORP_PUBKEY) { Cash.Commands.Move() }
|
||||
}
|
||||
@ -398,14 +414,14 @@ class TwoPartyTradeProtocolTests {
|
||||
return Pair(wallet, listOf(eb1, bc1, bc2))
|
||||
}
|
||||
|
||||
private fun TransactionGroupDSL<ContractState>.fillUpForSeller(withError: Boolean, timestamper: Party, attachmentID: SecureHash?): Pair<Wallet, List<WireTransaction>> {
|
||||
private fun TransactionGroupDSL<ContractState>.fillUpForSeller(withError: Boolean, owner: PublicKey, notary: Party, attachmentID: SecureHash?): Pair<Wallet, List<WireTransaction>> {
|
||||
val ap = transaction {
|
||||
output("alice's paper") {
|
||||
CommercialPaper.State(MEGA_CORP.ref(1, 2, 3), ALICE, 1200.DOLLARS, TEST_TX_TIME + 7.days)
|
||||
CommercialPaper.State(MEGA_CORP.ref(1, 2, 3), owner, 1200.DOLLARS, TEST_TX_TIME + 7.days, notary)
|
||||
}
|
||||
arg(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() }
|
||||
if (!withError)
|
||||
arg(timestamper.owningKey) { TimestampCommand(TEST_TX_TIME, 30.seconds) }
|
||||
arg(notary.owningKey) { TimestampCommand(TEST_TX_TIME, 30.seconds) }
|
||||
if (attachmentID != null)
|
||||
attachment(attachmentID)
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import core.*
|
||||
import core.crypto.SecureHash
|
||||
import core.node.services.AttachmentStorage
|
||||
import core.serialization.*
|
||||
import core.testutils.DUMMY_NOTARY
|
||||
import core.testutils.MEGA_CORP
|
||||
import org.apache.commons.io.IOUtils
|
||||
import org.junit.Test
|
||||
@ -19,7 +20,7 @@ import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertNotNull
|
||||
|
||||
interface DummyContractBackdoor {
|
||||
fun generateInitial(owner: PartyAndReference, magicNumber: Int): TransactionBuilder
|
||||
fun generateInitial(owner: PartyAndReference, magicNumber: Int, notary: Party): TransactionBuilder
|
||||
fun inspectState(state: ContractState): Int
|
||||
}
|
||||
|
||||
@ -183,7 +184,7 @@ class AttachmentClassLoaderTests {
|
||||
|
||||
@Test
|
||||
fun `test serialization of WireTransaction with statically loaded contract`() {
|
||||
val tx = DUMMY_PROGRAM_ID.generateInitial(MEGA_CORP.ref(0), 42)
|
||||
val tx = DUMMY_PROGRAM_ID.generateInitial(MEGA_CORP.ref(0), 42, DUMMY_NOTARY)
|
||||
val wireTransaction = tx.toWireTransaction()
|
||||
val bytes = wireTransaction.serialize()
|
||||
val copiedWireTransaction = bytes.deserialize()
|
||||
@ -197,7 +198,7 @@ class AttachmentClassLoaderTests {
|
||||
val child = ClassLoaderForTests()
|
||||
val contractClass = Class.forName("contracts.isolated.AnotherDummyContract", true, child)
|
||||
val contract = contractClass.newInstance() as DummyContractBackdoor
|
||||
val tx = contract.generateInitial(MEGA_CORP.ref(0), 42)
|
||||
val tx = contract.generateInitial(MEGA_CORP.ref(0), 42, DUMMY_NOTARY)
|
||||
val storage = MockAttachmentStorage()
|
||||
val kryo = createKryo()
|
||||
|
||||
@ -228,7 +229,7 @@ class AttachmentClassLoaderTests {
|
||||
val child = ClassLoaderForTests()
|
||||
val contractClass = Class.forName("contracts.isolated.AnotherDummyContract", true, child)
|
||||
val contract = contractClass.newInstance() as DummyContractBackdoor
|
||||
val tx = contract.generateInitial(MEGA_CORP.ref(0), 42)
|
||||
val tx = contract.generateInitial(MEGA_CORP.ref(0), 42, DUMMY_NOTARY)
|
||||
val storage = MockAttachmentStorage()
|
||||
val kryo = createKryo()
|
||||
|
||||
|
@ -61,7 +61,7 @@ class NodeInterestRatesTest {
|
||||
@Test fun `refuse to sign with no relevant commands`() {
|
||||
val tx = makeTX()
|
||||
assertFailsWith<IllegalArgumentException> { oracle.sign(tx.toWireTransaction()) }
|
||||
tx.addCommand(Cash.Commands.Move(), ALICE)
|
||||
tx.addCommand(Cash.Commands.Move(), ALICE_PUBKEY)
|
||||
assertFailsWith<IllegalArgumentException> { oracle.sign(tx.toWireTransaction()) }
|
||||
}
|
||||
|
||||
@ -89,7 +89,7 @@ class NodeInterestRatesTest {
|
||||
fun network() {
|
||||
val net = MockNetwork()
|
||||
val (n1, n2) = net.createTwoNodes()
|
||||
NodeInterestRates.Service(n2).oracle.knownFixes = TEST_DATA
|
||||
n2.interestRatesService.oracle.knownFixes = TEST_DATA
|
||||
|
||||
val tx = TransactionBuilder()
|
||||
val fixOf = NodeInterestRates.parseFixOf("LIBOR 2016-03-16 1M")
|
||||
@ -106,5 +106,5 @@ class NodeInterestRatesTest {
|
||||
assertEquals("0.678".bd, fix.value)
|
||||
}
|
||||
|
||||
private fun makeTX() = TransactionBuilder(outputs = mutableListOf(1000.DOLLARS.CASH `owned by` ALICE))
|
||||
private fun makeTX() = TransactionBuilder(outputs = mutableListOf(1000.DOLLARS.CASH `owned by` ALICE_PUBKEY))
|
||||
}
|
88
src/test/kotlin/core/node/services/NotaryServiceTests.kt
Normal file
88
src/test/kotlin/core/node/services/NotaryServiceTests.kt
Normal file
@ -0,0 +1,88 @@
|
||||
package core.node.services
|
||||
|
||||
import core.TransactionBuilder
|
||||
import core.seconds
|
||||
import core.testing.MockNetwork
|
||||
import core.testutils.DUMMY_NOTARY
|
||||
import core.testutils.DUMMY_NOTARY_KEY
|
||||
import core.testutils.issueState
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import protocols.NotaryProtocol
|
||||
import java.time.Instant
|
||||
import java.util.concurrent.ExecutionException
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class NotaryServiceTests {
|
||||
lateinit var net: MockNetwork
|
||||
lateinit var notaryNode: MockNetwork.MockNode
|
||||
lateinit var clientNode: MockNetwork.MockNode
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
// TODO: Move into MockNetwork
|
||||
net = MockNetwork()
|
||||
notaryNode = net.createNotaryNode(DUMMY_NOTARY.name, DUMMY_NOTARY_KEY)
|
||||
clientNode = net.createPartyNode(networkMapAddr = notaryNode.info)
|
||||
net.runNetwork() // Clear network map registration messages
|
||||
}
|
||||
|
||||
@Test fun `should sign a unique transaction with a valid timestamp`() {
|
||||
val inputState = issueState(clientNode)
|
||||
val tx = TransactionBuilder().withItems(inputState)
|
||||
tx.setTime(Instant.now(), DUMMY_NOTARY, 30.seconds)
|
||||
var wtx = tx.toWireTransaction()
|
||||
|
||||
val protocol = NotaryProtocol(wtx, NotaryProtocol.tracker())
|
||||
val future = clientNode.smm.add(NotaryProtocol.TOPIC, protocol)
|
||||
net.runNetwork()
|
||||
|
||||
val signature = future.get()
|
||||
signature.verifyWithECDSA(wtx.serialized)
|
||||
}
|
||||
|
||||
@Test fun `should sign a unique transaction without a timestamp`() {
|
||||
val inputState = issueState(clientNode)
|
||||
val wtx = TransactionBuilder().withItems(inputState).toWireTransaction()
|
||||
|
||||
val protocol = NotaryProtocol(wtx, NotaryProtocol.tracker())
|
||||
val future = clientNode.smm.add(NotaryProtocol.TOPIC, protocol)
|
||||
net.runNetwork()
|
||||
|
||||
val signature = future.get()
|
||||
signature.verifyWithECDSA(wtx.serialized)
|
||||
}
|
||||
|
||||
@Test fun `should report error for transaction with an invalid timestamp`() {
|
||||
val inputState = issueState(clientNode)
|
||||
val tx = TransactionBuilder().withItems(inputState)
|
||||
tx.setTime(Instant.now().plusSeconds(3600), DUMMY_NOTARY, 30.seconds)
|
||||
var wtx = tx.toWireTransaction()
|
||||
|
||||
val protocol = NotaryProtocol(wtx, NotaryProtocol.tracker())
|
||||
val future = clientNode.smm.add(NotaryProtocol.TOPIC, protocol)
|
||||
net.runNetwork()
|
||||
|
||||
val ex = assertFailsWith(ExecutionException::class) { future.get() }
|
||||
val error = (ex.cause as NotaryException).error
|
||||
assertTrue(error is NotaryError.TimestampInvalid)
|
||||
}
|
||||
|
||||
@Test fun `should report conflict for a duplicate transaction`() {
|
||||
val inputState = issueState(clientNode)
|
||||
val wtx = TransactionBuilder().withItems(inputState).toWireTransaction()
|
||||
|
||||
val firstSpend = NotaryProtocol(wtx)
|
||||
val secondSpend = NotaryProtocol(wtx)
|
||||
clientNode.smm.add("${NotaryProtocol.TOPIC}.first", firstSpend)
|
||||
val future = clientNode.smm.add("${NotaryProtocol.TOPIC}.second", secondSpend)
|
||||
net.runNetwork()
|
||||
|
||||
val ex = assertFailsWith(ExecutionException::class) { future.get() }
|
||||
val notaryError = (ex.cause as NotaryException).error as NotaryError.Conflict
|
||||
assertEquals(notaryError.tx, wtx)
|
||||
notaryError.conflict.verified()
|
||||
}
|
||||
}
|
33
src/test/kotlin/core/node/services/TimestampCheckerTests.kt
Normal file
33
src/test/kotlin/core/node/services/TimestampCheckerTests.kt
Normal file
@ -0,0 +1,33 @@
|
||||
package core.node.services
|
||||
|
||||
import core.TimestampCommand
|
||||
import core.seconds
|
||||
import org.junit.Test
|
||||
import java.time.Clock
|
||||
import java.time.Instant
|
||||
import java.time.ZoneId
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class TimestampCheckerTests {
|
||||
val clock = Clock.fixed(Instant.now(), ZoneId.systemDefault())
|
||||
val timestampChecker = TimestampChecker(clock, tolerance = 30.seconds)
|
||||
|
||||
@Test
|
||||
fun `should return true for valid timestamp`() {
|
||||
val now = clock.instant()
|
||||
val timestampPast = TimestampCommand(now - 60.seconds, now - 29.seconds)
|
||||
val timestampFuture = TimestampCommand(now + 29.seconds, now + 60.seconds)
|
||||
assertTrue { timestampChecker.isValid(timestampPast) }
|
||||
assertTrue { timestampChecker.isValid(timestampFuture) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should return false for invalid timestamp`() {
|
||||
val now = clock.instant()
|
||||
val timestampPast = TimestampCommand(now - 60.seconds, now - 31.seconds)
|
||||
val timestampFuture = TimestampCommand(now + 31.seconds, now + 60.seconds)
|
||||
assertFalse { timestampChecker.isValid(timestampPast) }
|
||||
assertFalse { timestampChecker.isValid(timestampFuture) }
|
||||
}
|
||||
}
|
@ -1,128 +0,0 @@
|
||||
package core.node.services
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import core.*
|
||||
import core.crypto.SecureHash
|
||||
import core.node.NodeInfo
|
||||
import core.protocols.ProtocolLogic
|
||||
import core.serialization.serialize
|
||||
import core.testing.MockNetwork
|
||||
import core.testutils.CASH
|
||||
import core.utilities.BriefLogFormatter
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import protocols.TimestampingProtocol
|
||||
import java.time.Clock
|
||||
import java.time.Instant
|
||||
import java.time.ZoneId
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class TimestamperNodeServiceTest {
|
||||
lateinit var network: MockNetwork
|
||||
|
||||
init {
|
||||
BriefLogFormatter.initVerbose("dlg.timestamping.request")
|
||||
}
|
||||
|
||||
val ptx = TransactionBuilder().apply {
|
||||
addInputState(StateRef(SecureHash.randomSHA256(), 0))
|
||||
addOutputState(100.DOLLARS.CASH)
|
||||
}
|
||||
|
||||
val clock = Clock.fixed(Instant.now(), ZoneId.systemDefault())
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
network = MockNetwork()
|
||||
}
|
||||
|
||||
class TestPSM(val server: NodeInfo, val now: Instant) : ProtocolLogic<Boolean>() {
|
||||
@Suspendable
|
||||
override fun call(): Boolean {
|
||||
val ptx = TransactionBuilder().apply {
|
||||
addInputState(StateRef(SecureHash.randomSHA256(), 0))
|
||||
addOutputState(100.DOLLARS.CASH)
|
||||
}
|
||||
ptx.addCommand(TimestampCommand(now - 20.seconds, now + 20.seconds), server.identity.owningKey)
|
||||
val wtx = ptx.toWireTransaction()
|
||||
// This line will invoke sendAndReceive to interact with the network.
|
||||
val sig = subProtocol(TimestampingProtocol(server, wtx.serialized))
|
||||
ptx.checkAndAddSignature(sig)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun successWithNetwork() {
|
||||
val timestamperNode = network.createNode(null, advertisedServices = TimestamperService.Type)
|
||||
val logName = TimestampingProtocol.TOPIC
|
||||
val psm = TestPSM(timestamperNode.info, clock.instant())
|
||||
val future = timestamperNode.smm.add(logName, psm)
|
||||
|
||||
network.runNetwork()
|
||||
assertTrue(future.isDone)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun wrongCommands() {
|
||||
val timestamperNode = network.createNode(null, advertisedServices = TimestamperService.Type)
|
||||
val timestamperKey = timestamperNode.services.storageService.myLegalIdentity.owningKey
|
||||
val service = timestamperNode.inNodeTimestampingService!!
|
||||
|
||||
// Zero commands is not OK.
|
||||
assertFailsWith(TimestampingError.RequiresExactlyOneCommand::class) {
|
||||
val wtx = ptx.toWireTransaction()
|
||||
service.processRequest(TimestampingProtocol.Request(wtx.serialize(), timestamperNode.info.address, Long.MIN_VALUE))
|
||||
}
|
||||
// More than one command is not OK.
|
||||
assertFailsWith(TimestampingError.RequiresExactlyOneCommand::class) {
|
||||
ptx.addCommand(TimestampCommand(clock.instant(), 30.seconds), timestamperKey)
|
||||
ptx.addCommand(TimestampCommand(clock.instant(), 40.seconds), timestamperKey)
|
||||
val wtx = ptx.toWireTransaction()
|
||||
service.processRequest(TimestampingProtocol.Request(wtx.serialize(), timestamperNode.info.address, Long.MIN_VALUE))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun tooEarly() {
|
||||
val timestamperNode = network.createNode(null, advertisedServices = TimestamperService.Type)
|
||||
val timestamperKey = timestamperNode.services.storageService.myLegalIdentity.owningKey
|
||||
val service = timestamperNode.inNodeTimestampingService!!
|
||||
|
||||
assertFailsWith(TimestampingError.NotOnTimeException::class) {
|
||||
val now = clock.instant()
|
||||
ptx.addCommand(TimestampCommand(now - 60.seconds, now - 40.seconds), timestamperKey)
|
||||
val wtx = ptx.toWireTransaction()
|
||||
service.processRequest(TimestampingProtocol.Request(wtx.serialize(), timestamperNode.info.address, Long.MIN_VALUE))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun tooLate() {
|
||||
val timestamperNode = network.createNode(null, advertisedServices = TimestamperService.Type)
|
||||
val timestamperKey = timestamperNode.services.storageService.myLegalIdentity.owningKey
|
||||
val service = timestamperNode.inNodeTimestampingService!!
|
||||
|
||||
assertFailsWith(TimestampingError.NotOnTimeException::class) {
|
||||
val now = clock.instant()
|
||||
ptx.addCommand(TimestampCommand(now - 60.seconds, now - 40.seconds), timestamperKey)
|
||||
val wtx = ptx.toWireTransaction()
|
||||
service.processRequest(TimestampingProtocol.Request(wtx.serialize(), timestamperNode.info.address, Long.MIN_VALUE))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun success() {
|
||||
val timestamperNode = network.createNode(null, advertisedServices = TimestamperService.Type)
|
||||
val timestamperKey = timestamperNode.services.storageService.myLegalIdentity.owningKey
|
||||
val service = timestamperNode.inNodeTimestampingService!!
|
||||
|
||||
val now = clock.instant()
|
||||
ptx.addCommand(TimestampCommand(now - 20.seconds, now + 20.seconds), timestamperKey)
|
||||
val wtx = ptx.toWireTransaction()
|
||||
val sig = service.processRequest(TimestampingProtocol.Request(wtx.serialize(), timestamperNode.info.address, Long.MIN_VALUE))
|
||||
ptx.checkAndAddSignature(sig)
|
||||
ptx.toSignedTransaction(false).verifySignatures()
|
||||
}
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
package core.node.services
|
||||
|
||||
import core.TransactionBuilder
|
||||
import core.testutils.MEGA_CORP
|
||||
import core.testutils.generateStateRef
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
|
||||
class UniquenessProviderTests {
|
||||
val identity = MEGA_CORP
|
||||
|
||||
@Test fun `should commit a transaction with unused inputs without exception`() {
|
||||
val provider = InMemoryUniquenessProvider()
|
||||
val inputState = generateStateRef()
|
||||
val tx = TransactionBuilder().withItems(inputState).toWireTransaction()
|
||||
provider.commit(tx, identity)
|
||||
}
|
||||
|
||||
@Test fun `should report a conflict for a transaction with previously used inputs`() {
|
||||
val provider = InMemoryUniquenessProvider()
|
||||
val inputState = generateStateRef()
|
||||
|
||||
val tx1 = TransactionBuilder().withItems(inputState).toWireTransaction()
|
||||
provider.commit(tx1, identity)
|
||||
|
||||
val tx2 = TransactionBuilder().withItems(inputState).toWireTransaction()
|
||||
val ex = assertFailsWith<UniquenessException> { provider.commit(tx2, identity) }
|
||||
|
||||
val consumingTx = ex.error.stateHistory[inputState]!!
|
||||
assertEquals(consumingTx.id, tx1.id)
|
||||
assertEquals(consumingTx.inputIndex, tx1.inputs.indexOf(inputState))
|
||||
assertEquals(consumingTx.requestingParty, identity)
|
||||
}
|
||||
}
|
@ -2,7 +2,6 @@ package core.node.subsystems
|
||||
|
||||
import core.messaging.Message
|
||||
import core.messaging.MessageRecipients
|
||||
import core.node.subsystems.ArtemisMessagingService
|
||||
import core.testutils.freeLocalHostAndPort
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.After
|
||||
|
@ -2,7 +2,6 @@ package core.node.subsystems
|
||||
|
||||
import contracts.Cash
|
||||
import core.*
|
||||
import core.node.subsystems.NodeWalletService
|
||||
import core.node.ServiceHub
|
||||
import core.testutils.*
|
||||
import core.utilities.BriefLogFormatter
|
||||
@ -37,7 +36,7 @@ class NodeWalletServiceTest {
|
||||
|
||||
kms.nextKeys += Array(3) { ALICE_KEY }
|
||||
// Fix the PRNG so that we get the same splits every time.
|
||||
wallet.fillWithSomeTestCash(100.DOLLARS, 3, 3, Random(0L))
|
||||
wallet.fillWithSomeTestCash(DUMMY_NOTARY, 100.DOLLARS, 3, 3, Random(0L))
|
||||
|
||||
val w = wallet.currentWallet
|
||||
assertEquals(3, w.states.size)
|
||||
@ -46,7 +45,7 @@ class NodeWalletServiceTest {
|
||||
assertEquals(services.storageService.myLegalIdentity, state.deposit.party)
|
||||
assertEquals(services.storageService.myLegalIdentityKey.public, state.deposit.party.owningKey)
|
||||
assertEquals(29.01.DOLLARS, state.amount)
|
||||
assertEquals(ALICE, state.owner)
|
||||
assertEquals(ALICE_PUBKEY, state.owner)
|
||||
|
||||
assertEquals(33.34.DOLLARS, (w.states[2].state as Cash.State).amount)
|
||||
assertEquals(35.61.DOLLARS, (w.states[1].state as Cash.State).amount)
|
||||
@ -59,20 +58,20 @@ class NodeWalletServiceTest {
|
||||
// A tx that sends us money.
|
||||
val freshKey = services.keyManagementService.freshKey()
|
||||
val usefulTX = TransactionBuilder().apply {
|
||||
Cash().generateIssue(this, 100.DOLLARS, MEGA_CORP.ref(1), freshKey.public)
|
||||
Cash().generateIssue(this, 100.DOLLARS, MEGA_CORP.ref(1), freshKey.public, DUMMY_NOTARY)
|
||||
signWith(MEGA_CORP_KEY)
|
||||
}.toSignedTransaction()
|
||||
val myOutput = usefulTX.verifyToLedgerTransaction(MockIdentityService, MockStorageService().attachments).outRef<Cash.State>(0)
|
||||
|
||||
// A tx that spends our money.
|
||||
val spendTX = TransactionBuilder().apply {
|
||||
Cash().generateSpend(this, 80.DOLLARS, BOB, listOf(myOutput))
|
||||
Cash().generateSpend(this, 80.DOLLARS, BOB_PUBKEY, listOf(myOutput))
|
||||
signWith(freshKey)
|
||||
}.toSignedTransaction()
|
||||
|
||||
// A tx that doesn't send us anything.
|
||||
val irrelevantTX = TransactionBuilder().apply {
|
||||
Cash().generateIssue(this, 100.DOLLARS, MEGA_CORP.ref(1), BOB_KEY.public)
|
||||
Cash().generateIssue(this, 100.DOLLARS, MEGA_CORP.ref(1), BOB_KEY.public, DUMMY_NOTARY)
|
||||
signWith(MEGA_CORP_KEY)
|
||||
}.toSignedTransaction()
|
||||
|
||||
|
@ -2,7 +2,6 @@ package core.serialization
|
||||
|
||||
import contracts.Cash
|
||||
import core.*
|
||||
import core.crypto.SecureHash
|
||||
import core.testutils.*
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
@ -14,10 +13,10 @@ class TransactionSerializationTests {
|
||||
// Simple TX that takes 1000 pounds from me and sends 600 to someone else (with 400 change).
|
||||
// It refers to a fake TX/state that we don't bother creating here.
|
||||
val depositRef = MINI_CORP.ref(1)
|
||||
val outputState = Cash.State(depositRef, 600.POUNDS, DUMMY_PUBKEY_1)
|
||||
val changeState = Cash.State(depositRef, 400.POUNDS, TestUtils.keypair.public)
|
||||
val outputState = Cash.State(depositRef, 600.POUNDS, DUMMY_PUBKEY_1, DUMMY_NOTARY)
|
||||
val changeState = Cash.State(depositRef, 400.POUNDS, TestUtils.keypair.public, DUMMY_NOTARY)
|
||||
|
||||
val fakeStateRef = StateRef(SecureHash.sha256("fake tx id"), 0)
|
||||
val fakeStateRef = generateStateRef()
|
||||
lateinit var tx: TransactionBuilder
|
||||
|
||||
@Before
|
||||
@ -33,27 +32,18 @@ class TransactionSerializationTests {
|
||||
val signedTX = tx.toSignedTransaction()
|
||||
|
||||
// Now check that the signature we just made verifies.
|
||||
signedTX.verify()
|
||||
signedTX.verifySignatures()
|
||||
|
||||
// Corrupt the data and ensure the signature catches the problem.
|
||||
signedTX.txBits.bits[5] = 0
|
||||
assertFailsWith(SignatureException::class) {
|
||||
signedTX.verify()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun tooManyKeys() {
|
||||
assertFailsWith(IllegalStateException::class) {
|
||||
tx.signWith(TestUtils.keypair)
|
||||
tx.signWith(TestUtils.keypair2)
|
||||
tx.toSignedTransaction()
|
||||
signedTX.verifySignatures()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun wrongKeys() {
|
||||
// Can't convert if we don't have enough signatures.
|
||||
// Can't convert if we don't have signatures for all commands
|
||||
assertFailsWith(IllegalStateException::class) {
|
||||
tx.toSignedTransaction()
|
||||
}
|
||||
@ -78,14 +68,14 @@ class TransactionSerializationTests {
|
||||
|
||||
@Test
|
||||
fun timestamp() {
|
||||
tx.setTime(TEST_TX_TIME, DUMMY_TIMESTAMPER.identity, 30.seconds)
|
||||
tx.timestamp(DUMMY_TIMESTAMPER)
|
||||
tx.setTime(TEST_TX_TIME, DUMMY_NOTARY, 30.seconds)
|
||||
tx.signWith(TestUtils.keypair)
|
||||
tx.signWith(DUMMY_NOTARY_KEY)
|
||||
val stx = tx.toSignedTransaction()
|
||||
val ltx = stx.verifyToLedgerTransaction(MockIdentityService, MockStorageService().attachments)
|
||||
assertEquals(tx.commands().map { it.value }, ltx.commands.map { it.value })
|
||||
assertEquals(tx.inputStates(), ltx.inputs)
|
||||
assertEquals(tx.outputStates(), ltx.outputs)
|
||||
assertEquals(TEST_TX_TIME, ltx.commands.getTimestampBy(DUMMY_TIMESTAMPER.identity)!!.midpoint)
|
||||
assertEquals(TEST_TX_TIME, ltx.commands.getTimestampBy(DUMMY_NOTARY)!!.midpoint)
|
||||
}
|
||||
}
|
@ -7,7 +7,7 @@ import com.google.common.net.HostAndPort
|
||||
import contracts.*
|
||||
import core.*
|
||||
import core.crypto.*
|
||||
import core.node.services.DummyTimestampingAuthority
|
||||
import core.node.AbstractNode
|
||||
import core.serialization.serialize
|
||||
import core.testing.MockIdentityService
|
||||
import core.visualiser.GraphVisualiser
|
||||
@ -57,17 +57,22 @@ val DUMMY_PUBKEY_1 = DummyPublicKey("x1")
|
||||
val DUMMY_PUBKEY_2 = DummyPublicKey("x2")
|
||||
|
||||
val ALICE_KEY = generateKeyPair()
|
||||
val ALICE = ALICE_KEY.public
|
||||
val ALICE_PUBKEY = ALICE_KEY.public
|
||||
val ALICE = Party("Alice", ALICE_PUBKEY)
|
||||
|
||||
val BOB_KEY = generateKeyPair()
|
||||
val BOB = BOB_KEY.public
|
||||
val BOB_PUBKEY = BOB_KEY.public
|
||||
val BOB = Party("Bob", BOB_PUBKEY)
|
||||
|
||||
val MEGA_CORP = Party("MegaCorp", MEGA_CORP_PUBKEY)
|
||||
val MINI_CORP = Party("MiniCorp", MINI_CORP_PUBKEY)
|
||||
|
||||
val ALL_TEST_KEYS = listOf(MEGA_CORP_KEY, MINI_CORP_KEY, ALICE_KEY, BOB_KEY, DummyTimestampingAuthority.key)
|
||||
val DUMMY_NOTARY_KEY = generateKeyPair()
|
||||
val DUMMY_NOTARY = Party("Notary Service", DUMMY_NOTARY_KEY.public)
|
||||
|
||||
val MockIdentityService = MockIdentityService(listOf(MEGA_CORP, MINI_CORP, DUMMY_TIMESTAMPER.identity))
|
||||
val ALL_TEST_KEYS = listOf(MEGA_CORP_KEY, MINI_CORP_KEY, ALICE_KEY, BOB_KEY, DUMMY_NOTARY_KEY)
|
||||
|
||||
val MockIdentityService = MockIdentityService(listOf(MEGA_CORP, MINI_CORP, DUMMY_NOTARY))
|
||||
|
||||
// In a real system this would be a persistent map of hash to bytecode and we'd instantiate the object as needed inside
|
||||
// a sandbox. For unit tests we just have a hard-coded list.
|
||||
@ -80,6 +85,18 @@ val TEST_PROGRAM_MAP: Map<Contract, Class<out Contract>> = mapOf(
|
||||
IRS_PROGRAM_ID to InterestRateSwap::class.java
|
||||
)
|
||||
|
||||
fun generateState(notary: Party = DUMMY_NOTARY) = DummyContract.State(Random().nextInt(), notary)
|
||||
fun generateStateRef() = StateRef(SecureHash.randomSHA256(), 0)
|
||||
|
||||
fun issueState(node: AbstractNode, notary: Party = DUMMY_NOTARY): StateRef {
|
||||
val tx = DummyContract().generateInitial(node.info.identity.ref(0), Random().nextInt(), DUMMY_NOTARY)
|
||||
tx.signWith(node.storage.myLegalIdentityKey)
|
||||
tx.signWith(DUMMY_NOTARY_KEY)
|
||||
val stx = tx.toSignedTransaction()
|
||||
node.services.recordTransactions(listOf(stx))
|
||||
return StateRef(stx.id, 0)
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Defines a simple DSL for building pseudo-transactions (not the same as the wire protocol) for testing purposes.
|
||||
@ -107,7 +124,7 @@ infix fun CommercialPaper.State.`owned by`(owner: PublicKey) = this.copy(owner =
|
||||
infix fun ICommercialPaperState.`owned by`(new_owner: PublicKey) = this.withOwner(new_owner)
|
||||
|
||||
// Allows you to write 100.DOLLARS.CASH
|
||||
val Amount.CASH: Cash.State get() = Cash.State(MINI_CORP.ref(1, 2, 3), this, NullPublicKey)
|
||||
val Amount.CASH: Cash.State get() = Cash.State(MINI_CORP.ref(1, 2, 3), this, NullPublicKey, DUMMY_NOTARY)
|
||||
|
||||
class LabeledOutput(val label: String?, val state: ContractState) {
|
||||
override fun toString() = state.toString() + (if (label != null) " ($label)" else "")
|
||||
@ -143,7 +160,7 @@ abstract class AbstractTransactionForTest {
|
||||
}
|
||||
|
||||
fun timestamp(data: TimestampCommand) {
|
||||
commands.add(Command(data, DUMMY_TIMESTAMPER.identity.owningKey))
|
||||
commands.add(Command(data, DUMMY_NOTARY.owningKey))
|
||||
}
|
||||
|
||||
// Forbid patterns like: transaction { ... transaction { ... } }
|
||||
|
Binary file not shown.
Loading…
Reference in New Issue
Block a user