mirror of
https://github.com/corda/corda.git
synced 2024-12-18 20:47:57 +00:00
Initial import
This commit is contained in:
commit
c60db5544b
52
.gitignore
vendored
Normal file
52
.gitignore
vendored
Normal file
@ -0,0 +1,52 @@
|
||||
# Created by .ignore support plugin (hsz.mobi)
|
||||
|
||||
.gradle
|
||||
|
||||
### JetBrains template
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio
|
||||
|
||||
*.iml
|
||||
|
||||
## Directory-based project format:
|
||||
.idea/
|
||||
# if you remove the above rule, at least ignore the following:
|
||||
|
||||
# User-specific stuff:
|
||||
# .idea/workspace.xml
|
||||
# .idea/tasks.xml
|
||||
# .idea/dictionaries
|
||||
|
||||
# Sensitive or high-churn files:
|
||||
# .idea/dataSources.ids
|
||||
# .idea/dataSources.xml
|
||||
# .idea/sqlDataSources.xml
|
||||
# .idea/dynamic.xml
|
||||
# .idea/uiDesigner.xml
|
||||
|
||||
# Gradle:
|
||||
# .idea/gradle.xml
|
||||
# .idea/libraries
|
||||
|
||||
# Mongo Explorer plugin:
|
||||
# .idea/mongoSettings.xml
|
||||
|
||||
## File-based project format:
|
||||
*.ipr
|
||||
*.iws
|
||||
|
||||
## Plugin-specific files:
|
||||
|
||||
# IntelliJ
|
||||
/out/
|
||||
|
||||
# mpeltonen/sbt-idea plugin
|
||||
.idea_modules/
|
||||
|
||||
# JIRA plugin
|
||||
atlassian-ide-plugin.xml
|
||||
|
||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
|
31
build.gradle
Normal file
31
build.gradle
Normal file
@ -0,0 +1,31 @@
|
||||
group 'com.r3cev.experimental'
|
||||
version '1.0-SNAPSHOT'
|
||||
|
||||
apply plugin: 'java'
|
||||
apply plugin: 'kotlin'
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
jcenter()
|
||||
}
|
||||
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.0.0-beta-1103'
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
testCompile group: 'junit', name: 'junit', version: '4.11'
|
||||
compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||
compile "com.google.guava:guava:18.0"
|
||||
compile "org.funktionale:funktionale:0.6_1.0.0-beta"
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main.java.srcDirs += 'src'
|
||||
}
|
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
6
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
6
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
#Thu Oct 29 18:30:55 CET 2015
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-2.2-all.zip
|
164
gradlew
vendored
Executable file
164
gradlew
vendored
Executable file
@ -0,0 +1,164 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
##############################################################################
|
||||
##
|
||||
## Gradle start up script for UN*X
|
||||
##
|
||||
##############################################################################
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS=""
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=`basename "$0"`
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD="maximum"
|
||||
|
||||
warn ( ) {
|
||||
echo "$*"
|
||||
}
|
||||
|
||||
die ( ) {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
}
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
case "`uname`" in
|
||||
CYGWIN* )
|
||||
cygwin=true
|
||||
;;
|
||||
Darwin* )
|
||||
darwin=true
|
||||
;;
|
||||
MINGW* )
|
||||
msys=true
|
||||
;;
|
||||
esac
|
||||
|
||||
# For Cygwin, ensure paths are in UNIX format before anything is touched.
|
||||
if $cygwin ; then
|
||||
[ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
|
||||
fi
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
# Resolve links: $0 may be a link
|
||||
PRG="$0"
|
||||
# Need this for relative symlinks.
|
||||
while [ -h "$PRG" ] ; do
|
||||
ls=`ls -ld "$PRG"`
|
||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||
if expr "$link" : '/.*' > /dev/null; then
|
||||
PRG="$link"
|
||||
else
|
||||
PRG=`dirname "$PRG"`"/$link"
|
||||
fi
|
||||
done
|
||||
SAVED="`pwd`"
|
||||
cd "`dirname \"$PRG\"`/" >&-
|
||||
APP_HOME="`pwd -P`"
|
||||
cd "$SAVED" >&-
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD="java"
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
|
||||
MAX_FD_LIMIT=`ulimit -H -n`
|
||||
if [ $? -eq 0 ] ; then
|
||||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||
MAX_FD="$MAX_FD_LIMIT"
|
||||
fi
|
||||
ulimit -n $MAX_FD
|
||||
if [ $? -ne 0 ] ; then
|
||||
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||
fi
|
||||
else
|
||||
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||
fi
|
||||
fi
|
||||
|
||||
# For Darwin, add options to specify how the application appears in the dock
|
||||
if $darwin; then
|
||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||
fi
|
||||
|
||||
# For Cygwin, switch paths to Windows format before running java
|
||||
if $cygwin ; then
|
||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||
|
||||
# We build the pattern for arguments to be converted via cygpath
|
||||
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||
SEP=""
|
||||
for dir in $ROOTDIRSRAW ; do
|
||||
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||
SEP="|"
|
||||
done
|
||||
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||
# Add a user-defined pattern to the cygpath arguments
|
||||
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||
fi
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
i=0
|
||||
for arg in "$@" ; do
|
||||
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||
|
||||
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||
else
|
||||
eval `echo args$i`="\"$arg\""
|
||||
fi
|
||||
i=$((i+1))
|
||||
done
|
||||
case $i in
|
||||
(0) set -- ;;
|
||||
(1) set -- "$args0" ;;
|
||||
(2) set -- "$args0" "$args1" ;;
|
||||
(3) set -- "$args0" "$args1" "$args2" ;;
|
||||
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
|
||||
function splitJvmOpts() {
|
||||
JVM_OPTS=("$@")
|
||||
}
|
||||
eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
|
||||
JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
|
||||
|
||||
exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
|
90
gradlew.bat
vendored
Normal file
90
gradlew.bat
vendored
Normal file
@ -0,0 +1,90 @@
|
||||
@if "%DEBUG%" == "" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS=
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto init
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto init
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:init
|
||||
@rem Get command-line arguments, handling Windowz variants
|
||||
|
||||
if not "%OS%" == "Windows_NT" goto win9xME_args
|
||||
if "%@eval[2+2]" == "4" goto 4NT_args
|
||||
|
||||
:win9xME_args
|
||||
@rem Slurp the command line arguments.
|
||||
set CMD_LINE_ARGS=
|
||||
set _SKIP=2
|
||||
|
||||
:win9xME_args_slurp
|
||||
if "x%~1" == "x" goto execute
|
||||
|
||||
set CMD_LINE_ARGS=%*
|
||||
goto execute
|
||||
|
||||
:4NT_args
|
||||
@rem Get arguments from the 4NT Shell from JP Software
|
||||
set CMD_LINE_ARGS=%$
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||
exit /b 1
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
195
notes/Design ideas.md
Normal file
195
notes/Design ideas.md
Normal file
@ -0,0 +1,195 @@
|
||||
General design scratchpad
|
||||
|
||||
Do we need blocks at all? Blocks are an artifact of proof-of-work, which isn't acceptable on private block chains
|
||||
due to the excessive energy usage, unclear incentives model and so on. They're also very useful for SPV operation,
|
||||
but we have no such requirements here.
|
||||
|
||||
Possible alternative, blend of ideas from:
|
||||
|
||||
* Google Spanner
|
||||
* Hawk
|
||||
* Bitcoin
|
||||
* Ethereum
|
||||
* Intel/TCG
|
||||
|
||||
+ some of my own ideas
|
||||
|
||||
|
||||
|
||||
# Blockless operation
|
||||
|
||||
* A set of timestampers are set up around the world with clocks synchronised to GPS time (the most accurate clock
|
||||
available as it's constantly recalibrated against the US Naval Observatory atomic clock). Public timestampers
|
||||
are available already and can be easily used in the prototyping phase, but as they're intended for low traffic
|
||||
applications eventually we'd want our own.
|
||||
|
||||
There is a standard protocol for timestamp servers (RFC 3161). It appears to include everything that we might want
|
||||
and little more, i.e. it's a good place to start. A more modern version of it with the same features can be easily
|
||||
generated later.
|
||||
|
||||
* All transactions submitted to the global network must be timestamped by a recognised TSP (i.e. signed by a root cert
|
||||
owned by R3).
|
||||
|
||||
* Transactions are ordered according to these timestamps. They are assumed to be precise enough that conflicts where
|
||||
two transactions have actually equal times can ~never happen: a trivial resolution algorithm (e.g. based on whichever
|
||||
hash is lower) can be used in case that ever happens by fluke.
|
||||
|
||||
* If need be, clock uncertainty can be measured and overlapping intervals can result in conflict/reject states, as in
|
||||
Spanner's TrueTime. The timestamping protocol (RFC 3161) exposes clock uncertainty.
|
||||
|
||||
* Transactions are timestamped as a group. This ensures that if multiple transactions are needed to carry out a trade,
|
||||
individual transactions cannot be extracted by a malicious party and executed independently as the original bundle
|
||||
will always be able to win, when broadcast.
|
||||
|
||||
* Nodes listen to a broadcast feed of timestamped transactions. They know how to roll back transactions and replay
|
||||
new ones in case of conflict, but this is done independent of any block construct.
|
||||
|
||||
* Nodes that are catching up simply download all the transactions from peers that occur after the time they shut down.
|
||||
They can be sure they didn't miss any by asking peers to calculate a UTXO set summary at a given time and then
|
||||
verifying it against their own local calculations (this is slow, but shouldn't normally flag any issues so it can
|
||||
be done asynchronously).
|
||||
|
||||
* Individual transactions/UTXOs can specify time bounds, e.g. "30 seconds". A node compares a transaction timestamp
|
||||
to its own local clock and applies the specified bound to the local clock: if the transaction is out of bounds and
|
||||
the node isn't catching up, then it is dropped. This prevents people timestamping a malicious transaction X and
|
||||
keeping it private, then broadcasting a publicly timestamped transaction Y, then overriding Y with X long after the
|
||||
underlying trade has become irreversible. Because time bounds are specified on a _per transaction_ basis, it is
|
||||
arbitrarily controllable: traders that want very, very fast clearing can specify a small time boundary and it's up
|
||||
to them to ensure their own systems are capable of getting an accurate trusted timestamp and broadcasting it within
|
||||
that tight bound. Traders that care less, e.g. because the trade represents physical movement of real goods, can use
|
||||
a much larger time bound and get more robustness against transient network/hardware hiccups.
|
||||
|
||||
* Like in Ethereum, transactions can update stored state (contracts? receipts? what is the right term?)
|
||||
|
||||
This can be called transaction-chains. All transactions are public.
|
||||
|
||||
For political expedience, we may wish to impose a (not strictly necessary) block quantisation anyway, so the popular
|
||||
term 'block chain' can be applied and also for auditing/reporting convenience.
|
||||
|
||||
# Privacy
|
||||
|
||||
* Transactions can have two halves: the public side and the private side. The public side is a "normal" transaction that
|
||||
includes a program of sufficient power to verify various kinds of signatures and proofs. The optional private side
|
||||
is an arbitrary program which is executed by a third party. Various techniques are used to lower the trust required
|
||||
in the third parties. We can call these notaries.
|
||||
|
||||
* It's up to the contract designer to decide how much they rely on notaries - if at all. They are technically not
|
||||
required at all: the system would work (and scale) without them. But they can be used to improve privacy.
|
||||
|
||||
* Simplest "dummy" notary is just a machine that signs the output of the program to state it ran it properly. The notary
|
||||
is trusted to execute the program correctly and privately. The signature is checked by the public side. This allows
|
||||
traders to perform e.g. a Dutch auction with only the final results being reflected on the public network.
|
||||
|
||||
* Next best is an SGX based notary. This can provide both privacy and assurance that the code is executed correctly,
|
||||
assuming Intel is trustworthy. Note: it's a safe assumption that if R3 becomes very popular with financial networks,
|
||||
intelligence agencies will attempt to gain covert access to it given the NSA/GCHQ hacking of Western Union and clear
|
||||
interest in SWIFT data. Thus care must be used to ensure the (entirely unprovable) SGX computers are not interdicted
|
||||
during delivery.
|
||||
|
||||
* In addition, zero knowledge proofs can be considered as a supplement to SGX. They can give extra assurance against
|
||||
corrupted notaries calculating incorrect results. However, unlike SGX, they cannot reduce the amount of information
|
||||
the notary sees, and thus they are strictly a "backup". In addition they have _severe_ caveats, in particular, a
|
||||
complex and expensive setup phase that must be executed for each contract (in fact for each version of each contract),
|
||||
and execution of the private side is extremely slow.
|
||||
|
||||
This makes them suitable only for contracts that are basically finalised and in which the highest levels of assurance
|
||||
are required, and fast or frequent trading is not required. The technology may well improve over time.
|
||||
|
||||
* In some cases homomorphic encryption could be used as a privacy supplement to SGX.
|
||||
|
||||
# Scaling
|
||||
|
||||
* Global broadcast systems are frequently attacked for 'not scaling'. But this is an absolute statement in a world of
|
||||
tradeoffs: technically speaking the NASDAQ is a broadcast system as you can subscribe to data feeds via e.g. OPRA.
|
||||
Some of these feeds can reach millions of messages per second. Nonetheless, financial firms are capable of digesting
|
||||
them without issue. Even the largest feeds have finite traffic and predictable growth patterns.
|
||||
|
||||
* We can assume powerful hardware, as the primary users of this system would be financial institutions. There is no
|
||||
requirement to run on people's laptops, outside of testing/devnet scenarios. For instance it's safe to assume SSD
|
||||
based storage: we can simply tell institutions that want to get on the network to buy a proper server.
|
||||
|
||||
* There is no requirement for lightweight/mobile clients, unlike in Bitcoin.
|
||||
|
||||
* Transaction checking is highly parallelisable.
|
||||
|
||||
* Therefore, as long as transactions are kept computationally cheap, there should be no problem reaching even very high
|
||||
levels of traffic.
|
||||
|
||||
Conclusion: scaling in a Bitcoin style manner should not be a problem, even if high level languages like Java or Kotlin
|
||||
are in use.
|
||||
|
||||
# Programmability
|
||||
|
||||
* The public side of a transaction must use a globally agreed execution environment, like the EVM is for Ethereum.
|
||||
The private sides can run anything: as the public side checks a proof of execution of the private side, there is
|
||||
no requirement that the private side use any particular language or runtime.
|
||||
|
||||
* Inventing a custom VM and language doesn't make sense: there is only one special requirement that is different
|
||||
to most VMs and that's the ability to impose hard CPU usage limits. But existing VMs can be extended to deliver
|
||||
this functionality much more easily than entirely new VMs+languages can be created.
|
||||
|
||||
* For prototyping and possibly for production use, we should use the JVM:
|
||||
|
||||
* Sandboxing already available, easy to use
|
||||
* Several languages available, developers are familiar
|
||||
* If host environment also runs on the JVM, no time wasted on interop issues, see the Ethereum ABI issues
|
||||
* HotSpot already has a CPU/memory tracking API and can interrupt threads (but lacks the ability to hard shut down
|
||||
malicious code)
|
||||
* Code annotations can be used to customise whatever languages are used for contract-specific use cases.
|
||||
* Can be forced to run in interpreted mode at first, but if we need the extra performance later due to high traffic
|
||||
the JIT compiler will automatically make contract code fast.
|
||||
* Has industrial strength debugging/monitoring tools.
|
||||
* Banks are already deeply familiar with it.
|
||||
|
||||
|
||||
|
||||
# Transaction design
|
||||
|
||||
Use a vaguely bitcoin-like design with "states" which are consumed and generated by "contracts" (programs). Everyone
|
||||
runs the same programs simultaneously in order to verify state transitions. Transactions consist of input states,
|
||||
output states and "commands" which represent signed auxiliary inputs to the transitions.
|
||||
|
||||
|
||||
|
||||
------
|
||||
|
||||
# Useful technologies
|
||||
|
||||
FIX SBE is a very (very) efficient binary encoding designed for HFT:
|
||||
|
||||
http://real-logic.github.io/simple-binary-encoding/
|
||||
|
||||
It's mostly analogous to protocol buffers but imposes some additional constraints and has an uglier API, in return for
|
||||
much higher performance. It probably isn't useful during the prototyping phase. But it may be a useful optimisation
|
||||
later.
|
||||
|
||||
CopyCat is an implementation of Raft (similar to Paxos), as an embeddable framework. Raft/Paxos type algorithms are not
|
||||
suitable as the basis for a global distributed ledger due to tiny throughput, but may be useful as a subcomponent of
|
||||
other algorithms. For instance possibly a multi-step contract protocol could use Raft/Paxos between a limited number of
|
||||
counterparties to synchronise changes.
|
||||
|
||||
http://kuujo.github.io/copycat/user-manual/introduction/
|
||||
|
||||
|
||||
|
||||
------
|
||||
|
||||
|
||||
# Prototyping
|
||||
|
||||
Stream 1:
|
||||
|
||||
1. Implement a simple star topology for message routing (full p2p can come later). Ensure it's got a clean modular API.
|
||||
2. Implement a simple chat app on top of it. This will be useful later for sending commands to faucets, bots, etc.
|
||||
3. Add auto-update
|
||||
4. Design a basic transaction/transaction bundle abstraction and implement timestamping of the bundles. Make chat lines
|
||||
into "transactions", so they are digitally signed and timestamped properly.
|
||||
5. Implement detection of conflicts and rollbacks.
|
||||
|
||||
|
||||
|
||||
Stream 2: Design straw-man contracts and data structures (in Java or Kotlin) for
|
||||
|
||||
1. payments
|
||||
2. simplified bond auctions
|
||||
3. maybe a CDS
|
19
notes/Example contracts.md
Normal file
19
notes/Example contracts.md
Normal file
@ -0,0 +1,19 @@
|
||||
# Simple payment
|
||||
|
||||
CashState:
|
||||
- Issuing institution
|
||||
- Deposit reference (pointer into internal ledger)
|
||||
- Currency code
|
||||
- Claim size (initial state = size of original deposit)
|
||||
- Public key of current owner
|
||||
|
||||
ExitCashState:
|
||||
- Amount to reduce claim size by
|
||||
- Signature signed by ownerPubKey
|
||||
|
||||
State transition function (contract):
|
||||
1. If input states contains an ExitCashState, set reduceByAmount=state.amount
|
||||
1. For all proposed output states, they must all be instances of CashState
|
||||
For all proposed input states, they must all be instances of CashState
|
||||
2. Sum claim sizes in all predecessor states. Sum claim sizes in all successor states
|
||||
3. Accept if outputSum == inputSum - reduceByAmount
|
19
notes/Open questions.md
Normal file
19
notes/Open questions.md
Normal file
@ -0,0 +1,19 @@
|
||||
How to represent pointers to states in the type system? Opaque or exposed as hashes?
|
||||
|
||||
# Create states vs check states?
|
||||
|
||||
1. Derive output states entirely from input states + signed commands, *or*
|
||||
2. Be given the output states and check they're valid
|
||||
|
||||
The advantage of 1 is that it feels safer: you can't forget to check something in the output state by accident. On
|
||||
the other hand, then it's up to the platform to validate equality between the states (probably by serializing them
|
||||
and comparing bit strings), and that would make unit testing harder as the generic machinery can't give good error
|
||||
messages for a given mismatch. Also it means you can't do an equivalent of OP_RETURN and insert extra no-op states
|
||||
in the output list that are ignored by all the input contracts. Does that matter if extensibility/tagging is built in
|
||||
more elegantly? Is it better to prevent this for the usual spam reasons?
|
||||
|
||||
The advantage of 2 is that it seems somehow more extensible: old contracts would ignore fields added to new states if
|
||||
they didn't understand them (or is that a disadvantage?)
|
||||
|
||||
# What precisely is signed at each point?
|
||||
|
2
settings.gradle
Normal file
2
settings.gradle
Normal file
@ -0,0 +1,2 @@
|
||||
rootProject.name = 'playground'
|
||||
|
82
src/Cash.kt
Normal file
82
src/Cash.kt
Normal file
@ -0,0 +1,82 @@
|
||||
import java.security.PublicKey
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Cash
|
||||
|
||||
|
||||
// TODO: Implement multi-issuer case.
|
||||
// TODO: Does multi-currency also make sense? Probably?
|
||||
// TODO: Implement a generate function.
|
||||
|
||||
// Just a fake program identifier for now. In a real system it could be, for instance, the hash of the program bytecode.
|
||||
val CASH_PROGRAM_ID = SecureHash.sha256("cash")
|
||||
|
||||
/** A state representing a claim on the cash reserves of some institution */
|
||||
data class CashState(
|
||||
/** The institution that has this original cash deposit (propagated) */
|
||||
val issuingInstitution: Institution,
|
||||
|
||||
/** Whatever internal ID the bank needs in order to locate that deposit, may be encrypted (propagated) */
|
||||
val depositReference: ByteArray,
|
||||
|
||||
val amount: Amount,
|
||||
|
||||
/** There must be a MoveCommand signed by this key to claim the amount */
|
||||
val owner: PublicKey
|
||||
) : ContractState {
|
||||
override val programRef = CASH_PROGRAM_ID
|
||||
}
|
||||
|
||||
/** A command proving ownership of some input states, the signature covers the output states. */
|
||||
class MoveCashCommand : Command
|
||||
/** A command stating that money has been withdrawn from the shared ledger and is now accounted for in some other way */
|
||||
class ExitCashCommand(val amount: Amount) : Command
|
||||
|
||||
class CashContract : Contract {
|
||||
override fun verify(inStates: List<ContractState>, outStates: List<ContractState>, args: List<VerifiedSignedCommand>) {
|
||||
// Select all input states that are cash states and ensure they are all denominated in the same currency and
|
||||
// issued by the same issuer.
|
||||
val inputs = inStates.filterIsInstance<CashState>()
|
||||
val inputMoney = inputs.sumBy { it.amount.pennies }
|
||||
|
||||
requireThat {
|
||||
"there is at least one cash input" by inputs.isNotEmpty()
|
||||
"all inputs use the same currency" by (inputs.groupBy { it.amount.currency }.size == 1)
|
||||
"all inputs come from the same issuer" by (inputs.groupBy { it.issuingInstitution }.size == 1)
|
||||
"some money is actually moving" by (inputMoney > 0)
|
||||
}
|
||||
|
||||
val issuer = inputs.first().issuingInstitution
|
||||
val currency = inputs.first().amount.currency
|
||||
val depositReference = inputs.first().depositReference
|
||||
|
||||
// Select all the output states that are cash states. There may be zero if all money is being withdrawn.
|
||||
// If there are any though, check that the currencies and issuers match the inputs.
|
||||
val outputs = outStates.filterIsInstance<CashState>()
|
||||
val outputMoney = outputs.sumBy { it.amount.pennies }
|
||||
requireThat {
|
||||
"all outputs use the currency of the inputs" by outputs.all { it.amount.currency == currency }
|
||||
"all outputs claim against the issuer of the inputs" by outputs.all { it.issuingInstitution == issuer }
|
||||
"all outputs use the same deposit reference as the inputs" by outputs.all { it.depositReference == depositReference }
|
||||
}
|
||||
|
||||
// If we have any commands, find the one that came from the issuer of the original cash deposit and
|
||||
// check if it's an exit command.
|
||||
val issuerCommand = args.find { it.signingInstitution == issuer }?.command as? ExitCashCommand
|
||||
val amountExitingLedger = issuerCommand?.amount?.pennies ?: 0
|
||||
requireThat("the value exiting the ledger is not more than the input value", amountExitingLedger <= outputMoney)
|
||||
|
||||
// Verify the books balance.
|
||||
requireThat("the amounts balance", inputMoney == outputMoney + amountExitingLedger)
|
||||
|
||||
// Now check the digital signatures on the move commands. Every input has an owning public key, and we must
|
||||
// see a signature from each of those keys. The actual signatures have been verified against the transaction
|
||||
// data by the platform before execution.
|
||||
val owningPubKeys = inputs.map { it.owner }.toSortedSet()
|
||||
val keysThatSigned = args.filter { it.command is MoveCashCommand }.map { it.signer }.toSortedSet()
|
||||
requireThat("the owning keys are the same as the signing keys", owningPubKeys == keysThatSigned)
|
||||
|
||||
// Accept.
|
||||
}
|
||||
}
|
36
src/Crypto.kt
Normal file
36
src/Crypto.kt
Normal file
@ -0,0 +1,36 @@
|
||||
import com.google.common.io.BaseEncoding
|
||||
import java.security.MessageDigest
|
||||
import java.security.PublicKey
|
||||
|
||||
// "sealed" here means there can't be any subclasses other than the ones defined here.
|
||||
sealed class SecureHash(val bits: ByteArray) {
|
||||
class SHA256(bits: ByteArray) : SecureHash(bits) {
|
||||
init { require(bits.size == 32) }
|
||||
}
|
||||
|
||||
// Like static methods in Java, except the 'companion' is a singleton that can have state.
|
||||
companion object {
|
||||
fun parse(str: String) = BaseEncoding.base16().decode(str.toLowerCase()).let {
|
||||
when (it.size) {
|
||||
32 -> SecureHash.SHA256(it)
|
||||
else -> throw IllegalArgumentException("Provided string is not 32 bytes in base 16 (hex): $str")
|
||||
}
|
||||
}
|
||||
|
||||
fun sha256(bits: ByteArray) = SHA256(MessageDigest.getInstance("SHA-256").digest(bits))
|
||||
fun sha256(str: String) = sha256(str.toByteArray())
|
||||
}
|
||||
|
||||
// In future, maybe SHA3, truncated hashes etc.
|
||||
}
|
||||
|
||||
/**
|
||||
* A wrapper around a digital signature. The covering field is a generic tag usable by whatever is interpreting the
|
||||
* signature.
|
||||
*/
|
||||
sealed class DigitalSignature(val bits: ByteArray, val covering: Int) {
|
||||
/** A digital signature that identifies who the public key is owned by */
|
||||
open class WithKey(val by: PublicKey, bits: ByteArray, covering: Int) : DigitalSignature(bits, covering)
|
||||
class LegallyIdentifiable(val signer: Institution, bits: ByteArray, covering: Int) : WithKey(signer.owningKey, bits, covering)
|
||||
}
|
||||
|
5
src/Main.kt
Normal file
5
src/Main.kt
Normal file
@ -0,0 +1,5 @@
|
||||
// TODO: Make a decision on basic timestamping approach (use external TSAs? use fake TSA?)
|
||||
// TODO: Think about how to expose time to the contract
|
||||
// TODO: Pick a serialisation solution for the prototype (kryo?)
|
||||
// TODO: Store transactions to a database, design some conflict detection and rollback
|
||||
// TODO: Implement a basic UI that shows contracts and very basic network layer (with chat?)
|
113
src/Structures.kt
Normal file
113
src/Structures.kt
Normal file
@ -0,0 +1,113 @@
|
||||
import java.security.PublicKey
|
||||
import java.security.Timestamp
|
||||
import java.util.*
|
||||
|
||||
data class Amount(val pennies: Int, val currency: Currency) {
|
||||
init {
|
||||
// Negative amounts are of course a vital part of any ledger, but negative values are only valid in certain
|
||||
// contexts: you cannot send a negative amount of cash, but you can (sometimes) have a negative balance.
|
||||
// TODO: Think about how positive-only vs positive-or-negative amounts can be represented in the type system.
|
||||
require(pennies >= 0) { "Negative amounts are not allowed: $pennies" }
|
||||
}
|
||||
|
||||
operator fun plus(other: Amount): Amount {
|
||||
require(other.currency == currency)
|
||||
return Amount(pennies + other.pennies, currency)
|
||||
}
|
||||
|
||||
operator fun minus(other: Amount): Amount {
|
||||
require(other.currency == currency)
|
||||
return Amount(pennies - other.pennies, currency)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A contract state (or just "state") contains opaque data used by a contract program. It can be thought of as a disk
|
||||
* file that the program can use to persist data across transactions. States are immutable: once created they are never
|
||||
* updated, instead, any changes must generate a new successor state.
|
||||
*/
|
||||
interface ContractState {
|
||||
/**
|
||||
* Refers to a bytecode program that has previously been published to the network. This contract program
|
||||
* will be executed any time this state is used in an input. It must accept in order for the
|
||||
* transaction to proceed.
|
||||
*/
|
||||
val programRef: SecureHash
|
||||
}
|
||||
|
||||
/**
|
||||
* A stateref is a pointer to a state, normally a hash but this version is opaque.
|
||||
*/
|
||||
class ContractStateRef(private val hash: SecureHash.SHA256)
|
||||
|
||||
/**
|
||||
* A transaction wraps the data needed to calculate one or more successor states from a set of input states.
|
||||
* The data here is provided in lightly processed form to the verify method of each input states contract program.
|
||||
* Specifically, the input state refs are dereferenced into real [ContractState]s and the args are signature checked
|
||||
* and institutions are looked up (if known).
|
||||
*/
|
||||
class Transaction(
|
||||
/** Arbitrary data passed to the program of each input state. */
|
||||
val args: List<SignedCommand>,
|
||||
/** The input states which will be consumed/invalidated by the execution of this transaction. */
|
||||
val inputStates: List<ContractStateRef>,
|
||||
/** The states that will be generated by the execution of this transaction. */
|
||||
val outputStates: List<ContractState>
|
||||
)
|
||||
|
||||
/**
|
||||
* A transition groups one or more transactions together, and combines them with a signed timestamp. A transaction
|
||||
* may not stand independent of a transition and all transactions are applied or reverted together as a unit.
|
||||
*
|
||||
* Consider the following attack: a malicious actor extracts a single transaction like "pay $X to me" from a transition
|
||||
* and then broadcasts it with a fresh timestamp. This cannot work because the original transition will always take
|
||||
* priority over the later attempt as it has an earlier timestamp. As long as both are visible, the first transition
|
||||
* will always win.
|
||||
*/
|
||||
data class Transition(
|
||||
val tx: Transaction,
|
||||
|
||||
/** Timestamp of the serialised transaction as fetched from a timestamping authority (RFC 3161) */
|
||||
val signedTimestamp: Timestamp
|
||||
)
|
||||
|
||||
class Institution(
|
||||
val name: String,
|
||||
val owningKey: PublicKey
|
||||
) {
|
||||
override fun toString() = name
|
||||
}
|
||||
|
||||
interface Command
|
||||
|
||||
/** Provided as an input to a contract; converted to a [VerifiedSignedCommand] by the platform before execution. */
|
||||
data class SignedCommand(
|
||||
/** Signature over this object to prove who it came from */
|
||||
val commandDataSignature: DigitalSignature.WithKey,
|
||||
|
||||
/** Command data, deserialized to an implementation of [Command] */
|
||||
val serialized: ByteArray,
|
||||
/** Identifies what command the serialized data contains (should maybe be a hash too) */
|
||||
val classID: String,
|
||||
/** Hash of a derivative of the transaction data, so this command can only ever apply to one transaction */
|
||||
val txBindingHash: SecureHash.SHA256
|
||||
)
|
||||
|
||||
/** Obtained from a [SignedCommand], deserialised and signature checked */
|
||||
data class VerifiedSignedCommand(
|
||||
val signer: PublicKey,
|
||||
/** If the public key was recognised, the looked up institution is available here, otherwise it's null */
|
||||
val signingInstitution: Institution?,
|
||||
val command: Command
|
||||
)
|
||||
|
||||
/**
|
||||
* Implemented by a program that implements business logic on the shared ledger. All participants run this code for
|
||||
* every [Transaction] they see on the network, for every input state. All input states must accept the transaction
|
||||
* for it to be accepted: failure of any aborts the entire thing.
|
||||
*/
|
||||
interface Contract {
|
||||
/** Must throw an exception if there's a problem that should prevent state transition. */
|
||||
fun verify(inStates: List<ContractState>, outStates: List<ContractState>, args: List<VerifiedSignedCommand>)
|
||||
}
|
||||
|
38
src/Utils.kt
Normal file
38
src/Utils.kt
Normal file
@ -0,0 +1,38 @@
|
||||
import java.math.BigInteger
|
||||
import java.security.PublicKey
|
||||
import java.util.*
|
||||
import kotlin.test.assertTrue
|
||||
import kotlin.test.fail
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// REQUIREMENTS
|
||||
|
||||
fun requireThat(message: String, expression: Boolean) {
|
||||
if (!expression) throw IllegalArgumentException(message)
|
||||
}
|
||||
|
||||
// To understand how requireThat works, read the section "type safe builders" on the Kotlin website:
|
||||
//
|
||||
// https://kotlinlang.org/docs/reference/type-safe-builders.html
|
||||
object Requirements {
|
||||
infix fun String.by(expr: Boolean) {
|
||||
if (!expr) throw IllegalArgumentException("Failed requirement: $this")
|
||||
}
|
||||
}
|
||||
fun requireThat(body: Requirements.() -> Unit) {
|
||||
Requirements.body()
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// CURRENCIES (convenience accessors)
|
||||
|
||||
val USD = Currency.getInstance("USD")
|
||||
val GBP = Currency.getInstance("GBP")
|
||||
val CHF = Currency.getInstance("CHF")
|
||||
|
||||
val Int.DOLLARS: Amount get() = Amount(this, USD)
|
||||
val Int.POUNDS: Amount get() = Amount(this, GBP)
|
||||
val Int.SWISS_FRANCS: Amount get() = Amount(this, CHF)
|
123
tests/CashTests.kt
Normal file
123
tests/CashTests.kt
Normal file
@ -0,0 +1,123 @@
|
||||
import org.junit.Test
|
||||
|
||||
class CashTests {
|
||||
val inState = CashState(
|
||||
issuingInstitution = MEGA_CORP,
|
||||
depositReference = byteArrayOf(1),
|
||||
amount = 1000.DOLLARS,
|
||||
owner = DUMMY_PUBKEY_1
|
||||
)
|
||||
val inState2 = inState.copy(
|
||||
amount = 150.POUNDS,
|
||||
owner = DUMMY_PUBKEY_2
|
||||
)
|
||||
val outState = inState.copy(owner = DUMMY_PUBKEY_2)
|
||||
|
||||
@Test
|
||||
fun trivial() {
|
||||
CashContract().let {
|
||||
transaction {
|
||||
it `fails requirement` "there is at least one cash input"
|
||||
}
|
||||
transaction {
|
||||
input { inState.copy(amount = 0.DOLLARS) }
|
||||
it `fails requirement` "some money is actually moving"
|
||||
}
|
||||
|
||||
transaction {
|
||||
input { inState }
|
||||
it `fails requirement` "the amounts balance"
|
||||
|
||||
transaction {
|
||||
output { outState.copy(amount = 2000.DOLLARS )}
|
||||
it `fails requirement` "the amounts balance"
|
||||
}
|
||||
transaction {
|
||||
output { outState }
|
||||
// No command arguments
|
||||
it `fails requirement` "the owning keys are the same as the signing keys"
|
||||
}
|
||||
transaction {
|
||||
output { outState }
|
||||
arg(DUMMY_PUBKEY_2) { MoveCashCommand() }
|
||||
it `fails requirement` "the owning keys are the same as the signing keys"
|
||||
}
|
||||
transaction {
|
||||
output { outState }
|
||||
arg(DUMMY_PUBKEY_1) { MoveCashCommand() }
|
||||
it.accepts()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun mismatches() {
|
||||
CashContract().let {
|
||||
transaction {
|
||||
input { inState }
|
||||
output { outState.copy(issuingInstitution = MINI_CORP) }
|
||||
it `fails requirement` "all outputs claim against the issuer of the inputs"
|
||||
}
|
||||
transaction {
|
||||
input { inState }
|
||||
output { outState.copy(issuingInstitution = MEGA_CORP) }
|
||||
output { outState.copy(issuingInstitution = MINI_CORP) }
|
||||
it `fails requirement` "all outputs claim against the issuer of the inputs"
|
||||
}
|
||||
transaction {
|
||||
input { inState }
|
||||
output { outState.copy(depositReference = byteArrayOf(0)) }
|
||||
output { outState.copy(depositReference = byteArrayOf(1)) }
|
||||
it `fails requirement` "all outputs use the same deposit reference as the inputs"
|
||||
}
|
||||
transaction {
|
||||
input { inState }
|
||||
output { outState.copy(amount = 800.DOLLARS) }
|
||||
output { outState.copy(amount = 200.POUNDS) }
|
||||
it `fails requirement` "all outputs use the currency of the inputs"
|
||||
}
|
||||
transaction {
|
||||
input { inState }
|
||||
input { inState2 }
|
||||
output { outState.copy(amount = 1150.DOLLARS) }
|
||||
it `fails requirement` "all inputs use the same currency"
|
||||
}
|
||||
transaction {
|
||||
input { inState }
|
||||
input { inState.copy(issuingInstitution = MINI_CORP) }
|
||||
output { outState }
|
||||
it `fails requirement` "all inputs come from the same issuer"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun exitLedger() {
|
||||
CashContract().let {
|
||||
transaction {
|
||||
input { inState }
|
||||
output { outState.copy(amount = inState.amount - 200.DOLLARS) }
|
||||
|
||||
transaction {
|
||||
arg(MEGA_CORP_KEY) {
|
||||
ExitCashCommand(100.DOLLARS)
|
||||
}
|
||||
it `fails requirement` "the amounts balance"
|
||||
}
|
||||
|
||||
transaction {
|
||||
arg(MEGA_CORP_KEY) {
|
||||
ExitCashCommand(200.DOLLARS)
|
||||
}
|
||||
it `fails requirement` "the owning keys are the same as the signing keys" // No move command.
|
||||
|
||||
transaction {
|
||||
arg(DUMMY_PUBKEY_1) { MoveCashCommand() }
|
||||
it.accepts()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
72
tests/TestUtils.kt
Normal file
72
tests/TestUtils.kt
Normal file
@ -0,0 +1,72 @@
|
||||
import java.math.BigInteger
|
||||
import java.security.PublicKey
|
||||
import kotlin.test.assertTrue
|
||||
import kotlin.test.fail
|
||||
|
||||
class DummyPublicKey(private val s: String) : PublicKey, Comparable<PublicKey> {
|
||||
override fun getAlgorithm() = "DUMMY"
|
||||
override fun getEncoded() = s.toByteArray()
|
||||
override fun getFormat() = "ASN.1"
|
||||
override fun compareTo(other: PublicKey): Int = BigInteger(encoded).compareTo(BigInteger(other.encoded))
|
||||
}
|
||||
|
||||
// A few dummy values for testing.
|
||||
val MEGA_CORP_KEY = DummyPublicKey("mini")
|
||||
val MINI_CORP_KEY = DummyPublicKey("mega")
|
||||
val DUMMY_PUBKEY_1 = DummyPublicKey("x1")
|
||||
val DUMMY_PUBKEY_2 = DummyPublicKey("x2")
|
||||
val MEGA_CORP = Institution("MegaCorp", MEGA_CORP_KEY)
|
||||
val MINI_CORP = Institution("MiniCorp", MINI_CORP_KEY)
|
||||
|
||||
val keyToCorpMap: Map<PublicKey, Institution> = mapOf(
|
||||
MEGA_CORP_KEY to MEGA_CORP,
|
||||
MINI_CORP_KEY to MINI_CORP
|
||||
)
|
||||
|
||||
// DSL for building pseudo-transactions (not the same as the wire protocol) for testing purposes.
|
||||
|
||||
// Corresponds to the args to Contract.verify
|
||||
data class TransactionForTest(
|
||||
private val inStates: MutableList<ContractState> = arrayListOf(),
|
||||
private val outStates: MutableList<ContractState> = arrayListOf(),
|
||||
private val args: MutableList<VerifiedSignedCommand> = arrayListOf()
|
||||
) {
|
||||
fun input(s: () -> ContractState) = inStates.add(s())
|
||||
fun output(s: () -> ContractState) = outStates.add(s())
|
||||
fun arg(key: PublicKey, c: () -> Command) = args.add(VerifiedSignedCommand(key, keyToCorpMap[key], c()))
|
||||
|
||||
infix fun Contract.`fails requirement`(msg: String) {
|
||||
try {
|
||||
verify(inStates, outStates, args)
|
||||
} catch(e: Exception) {
|
||||
val m = e.message
|
||||
if (m == null)
|
||||
fail("Threw exception without a message")
|
||||
else
|
||||
assertTrue(m.contains(msg), "Error was actually: $m")
|
||||
}
|
||||
}
|
||||
|
||||
// which is uglier?? :)
|
||||
fun Contract.fails_requirement(msg: String) = this.`fails requirement`(msg)
|
||||
|
||||
fun Contract.accepts() {
|
||||
verify(inStates, outStates, args)
|
||||
}
|
||||
|
||||
// Allow customisation of partial transactions.
|
||||
fun transaction(body: TransactionForTest.() -> Unit): TransactionForTest {
|
||||
val tx = TransactionForTest()
|
||||
tx.inStates.addAll(inStates)
|
||||
tx.outStates.addAll(outStates)
|
||||
tx.args.addAll(args)
|
||||
tx.body()
|
||||
return tx
|
||||
}
|
||||
}
|
||||
|
||||
fun transaction(body: TransactionForTest.() -> Unit): TransactionForTest {
|
||||
val tx = TransactionForTest()
|
||||
tx.body()
|
||||
return tx
|
||||
}
|
Loading…
Reference in New Issue
Block a user