From 10aaedc9fc82ba787d46e51ffbc6caf511234297 Mon Sep 17 00:00:00 2001 From: nargas-ritu Date: Tue, 17 Jan 2023 12:04:35 +0000 Subject: [PATCH 001/126] NOTICK: Branch creation for 4.11 --- .../src/main/kotlin/net/corda/common/logging/Constants.kt | 2 +- constants.properties | 2 +- docker/src/bash/example-mini-network.sh | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/common/logging/src/main/kotlin/net/corda/common/logging/Constants.kt b/common/logging/src/main/kotlin/net/corda/common/logging/Constants.kt index d3c6b7ee22..569cf90441 100644 --- a/common/logging/src/main/kotlin/net/corda/common/logging/Constants.kt +++ b/common/logging/src/main/kotlin/net/corda/common/logging/Constants.kt @@ -9,4 +9,4 @@ package net.corda.common.logging * (originally added to source control for ease of use) */ -internal const val CURRENT_MAJOR_RELEASE = "4.10-SNAPSHOT" \ No newline at end of file +internal const val CURRENT_MAJOR_RELEASE = "4.11-SNAPSHOT" diff --git a/constants.properties b/constants.properties index 9a01adc17b..823aaadac3 100644 --- a/constants.properties +++ b/constants.properties @@ -3,7 +3,7 @@ # their own projects. So don't get fancy with syntax! # Fancy syntax - multi pass ${whatever} replacement -cordaVersion=4.10 +cordaVersion=4.11 versionSuffix=SNAPSHOT gradlePluginsVersion=5.0.12 kotlinVersion=1.2.71 diff --git a/docker/src/bash/example-mini-network.sh b/docker/src/bash/example-mini-network.sh index 85a2b450e6..f8e628a7e1 100755 --- a/docker/src/bash/example-mini-network.sh +++ b/docker/src/bash/example-mini-network.sh @@ -1,8 +1,8 @@ #!/usr/bin/env bash NODE_LIST=("dockerNode1" "dockerNode2" "dockerNode3") NETWORK_NAME=mininet -CORDAPP_VERSION="4.10-SNAPSHOT" -DOCKER_IMAGE_VERSION="corda-zulu-4.10-snapshot" +CORDAPP_VERSION="4.11-SNAPSHOT" +DOCKER_IMAGE_VERSION="corda-zulu-4.11-snapshot" mkdir cordapps rm -f cordapps/* From f8896ef706246026d60ff2138a2242d03238e261 Mon Sep 17 00:00:00 2001 From: Jose Coll Date: Tue, 24 Jan 2023 11:42:34 +0000 Subject: [PATCH 002/126] Bump platform version. --- core/src/main/kotlin/net/corda/core/internal/CordaUtils.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/kotlin/net/corda/core/internal/CordaUtils.kt b/core/src/main/kotlin/net/corda/core/internal/CordaUtils.kt index 72608450cb..d431fd4259 100644 --- a/core/src/main/kotlin/net/corda/core/internal/CordaUtils.kt +++ b/core/src/main/kotlin/net/corda/core/internal/CordaUtils.kt @@ -30,7 +30,7 @@ import java.util.jar.JarInputStream // When incrementing platformVersion make sure to update PLATFORM_VERSION in constants.properties as well. -const val PLATFORM_VERSION = 12 +const val PLATFORM_VERSION = 13 fun ServicesForResolution.ensureMinimumPlatformVersion(requiredMinPlatformVersion: Int, feature: String) { checkMinimumPlatformVersion(networkParameters.minimumPlatformVersion, requiredMinPlatformVersion, feature) From 6e4768cd02865392b0bbf6d5e02111389a04df10 Mon Sep 17 00:00:00 2001 From: Jose Coll Date: Tue, 24 Jan 2023 11:54:56 +0000 Subject: [PATCH 003/126] Bump platform version. --- constants.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/constants.properties b/constants.properties index 823aaadac3..081f63417c 100644 --- a/constants.properties +++ b/constants.properties @@ -12,7 +12,7 @@ java8MinUpdateVersion=171 # When incrementing platformVersion make sure to update # # net.corda.core.internal.CordaUtilsKt.PLATFORM_VERSION as well. # # ***************************************************************# -platformVersion=12 +platformVersion=13 openTelemetryVersion=1.20.1 openTelemetrySemConvVersion=1.20.1-alpha guavaVersion=28.0-jre From 2ce0409bd25000621e4d7e7a555dd11952af40a9 Mon Sep 17 00:00:00 2001 From: Connel McGovern <100574906+mcgovc@users.noreply.github.com> Date: Fri, 17 Feb 2023 14:58:14 +0000 Subject: [PATCH 004/126] INFRA-2021: Temp fix for Snyk Delta Moving the position of the Snyk Delta to execute as the first stage as there seems to be issues with running the under the hood 'snykResolvedDepsJson' task after other gradle tasks have been executed. To be investigated further but this should unblock and PR's hitting the Snyk Delta stage. --- .ci/dev/pr-code-checks/Jenkinsfile | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/.ci/dev/pr-code-checks/Jenkinsfile b/.ci/dev/pr-code-checks/Jenkinsfile index b88093b862..f3deaac7b2 100644 --- a/.ci/dev/pr-code-checks/Jenkinsfile +++ b/.ci/dev/pr-code-checks/Jenkinsfile @@ -20,19 +20,6 @@ pipeline { } stages { - stage('Detekt check') { - steps { - authenticateGradleWrapper() - sh "./gradlew --no-daemon clean detekt" - } - } - - stage('Compilation warnings check') { - steps { - sh "./gradlew --no-daemon -Pcompilation.warningsAsErrors=true compileAll" - } - } - stage('Snyk Delta') { agent { docker { @@ -51,6 +38,19 @@ pipeline { snykDeltaScan(env.SNYK_API_TOKEN, env.C4_OS_SNYK_ORG_ID) } } + + stage('Detekt check') { + steps { + authenticateGradleWrapper() + sh "./gradlew --no-daemon clean detekt" + } + } + + stage('Compilation warnings check') { + steps { + sh "./gradlew --no-daemon -Pcompilation.warningsAsErrors=true compileAll" + } + } stage('No API change check') { steps { From 835ea723eaa18fd84f8ef5f827be294e3f056604 Mon Sep 17 00:00:00 2001 From: Connel McGovern Date: Tue, 21 Feb 2023 11:40:39 +0000 Subject: [PATCH 005/126] Revert "INFRA-2021: Temp fix for Snyk Delta " This reverts commit 2ce0409bd25000621e4d7e7a555dd11952af40a9. --- .ci/dev/pr-code-checks/Jenkinsfile | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/.ci/dev/pr-code-checks/Jenkinsfile b/.ci/dev/pr-code-checks/Jenkinsfile index f3deaac7b2..b88093b862 100644 --- a/.ci/dev/pr-code-checks/Jenkinsfile +++ b/.ci/dev/pr-code-checks/Jenkinsfile @@ -20,6 +20,19 @@ pipeline { } stages { + stage('Detekt check') { + steps { + authenticateGradleWrapper() + sh "./gradlew --no-daemon clean detekt" + } + } + + stage('Compilation warnings check') { + steps { + sh "./gradlew --no-daemon -Pcompilation.warningsAsErrors=true compileAll" + } + } + stage('Snyk Delta') { agent { docker { @@ -38,19 +51,6 @@ pipeline { snykDeltaScan(env.SNYK_API_TOKEN, env.C4_OS_SNYK_ORG_ID) } } - - stage('Detekt check') { - steps { - authenticateGradleWrapper() - sh "./gradlew --no-daemon clean detekt" - } - } - - stage('Compilation warnings check') { - steps { - sh "./gradlew --no-daemon -Pcompilation.warningsAsErrors=true compileAll" - } - } stage('No API change check') { steps { From 5b4fce52ce51ebd5608b7f666323371f18ba6a88 Mon Sep 17 00:00:00 2001 From: Connel McGovern Date: Tue, 21 Feb 2023 11:48:37 +0000 Subject: [PATCH 006/126] INFRA-2021: Adding in authenticate Gradle wrapper to Snyk Delta (seperate agent) --- .ci/dev/pr-code-checks/Jenkinsfile | 1 + 1 file changed, 1 insertion(+) diff --git a/.ci/dev/pr-code-checks/Jenkinsfile b/.ci/dev/pr-code-checks/Jenkinsfile index b88093b862..500038507e 100644 --- a/.ci/dev/pr-code-checks/Jenkinsfile +++ b/.ci/dev/pr-code-checks/Jenkinsfile @@ -48,6 +48,7 @@ pipeline { } steps { sh 'mkdir -p ${GRADLE_USER_HOME}' + authenticateGradleWrapper() snykDeltaScan(env.SNYK_API_TOKEN, env.C4_OS_SNYK_ORG_ID) } } From fe4e607dfddcfd9dc7a9b05b2e40bb7bddd8282d Mon Sep 17 00:00:00 2001 From: Connel McGovern Date: Tue, 21 Feb 2023 11:54:38 +0000 Subject: [PATCH 007/126] INFRA-2021: Adding in authenticate Gradle wrapper to Snyk Delta --- .ci/dev/pr-code-checks/Jenkinsfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/dev/pr-code-checks/Jenkinsfile b/.ci/dev/pr-code-checks/Jenkinsfile index 500038507e..c8b0a0c41b 100644 --- a/.ci/dev/pr-code-checks/Jenkinsfile +++ b/.ci/dev/pr-code-checks/Jenkinsfile @@ -48,7 +48,7 @@ pipeline { } steps { sh 'mkdir -p ${GRADLE_USER_HOME}' - authenticateGradleWrapper() + authenticateGradleWrapper() snykDeltaScan(env.SNYK_API_TOKEN, env.C4_OS_SNYK_ORG_ID) } } From 1a0d354903ff6036bc65199347445a9c6370c3d7 Mon Sep 17 00:00:00 2001 From: Mahmoud Almahroum <84918112+mahmoudal993@users.noreply.github.com> Date: Fri, 3 Mar 2023 15:10:25 +0000 Subject: [PATCH 008/126] ENT-8983 Upgrade H2 and liquibase to latest version (#7298) --- constants.properties | 6 +- lib/quasar.jar | Bin 1283239 -> 0 bytes .../internal/persistence/SchemaMigration.kt | 102 ++++++++++++------ .../IdenityServiceKeyRotationMigrationTest.kt | 5 +- .../net/corda/testing/node/MockServices.kt | 2 +- .../databasesnapshots/4.5.1/persistence.mv.db | Bin 110592 -> 0 bytes 6 files changed, 76 insertions(+), 39 deletions(-) delete mode 100644 lib/quasar.jar diff --git a/constants.properties b/constants.properties index 081f63417c..36a44261f0 100644 --- a/constants.properties +++ b/constants.properties @@ -77,9 +77,9 @@ mockitoKotlinVersion=1.6.0 hamkrestVersion=1.7.0.0 joptSimpleVersion=5.0.2 jansiVersion=1.18 -hibernateVersion=5.4.32.Final +hibernateVersion=5.6.5.Final # h2Version - Update docs if renamed or removed. -h2Version=1.4.199 +h2Version=2.1.212 rxjavaVersion=1.3.8 dokkaVersion=0.10.1 eddsaVersion=0.3.0 @@ -88,7 +88,7 @@ commonsCollectionsVersion=4.3 beanutilsVersion=1.9.4 shiroVersion=1.10.0 hikariVersion=3.3.1 -liquibaseVersion=3.6.3 +liquibaseVersion=4.18.0 dockerComposeRuleVersion=1.5.0 seleniumVersion=3.141.59 ghostdriverVersion=2.1.0 diff --git a/lib/quasar.jar b/lib/quasar.jar deleted file mode 100644 index 6f1e8c2fca8dd348735883bda2b326d8b0da3c6e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1283239 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%0AoWD4W5Fdd(%Cme=)4Wp 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%0DAL z-4~!eHWr&rbLHZea|O|zm9td;EZU`y>tL5~;wC)VH$gW{^Z++xk{7zAJ$0jpTG1Pz zbos#(TUGziTtE-y@YFnjxlhivQhrfd*$VyPN1taD+E82^T{7psXiGJCMjhjJ79=0t zR4G{nPuhK2}vk7nTFumelS%R@yW(`GJ zE$|rs>g-+F*^6x-2unu1C@@dks=Y;E-o^b)O^r8?>`#e44pLM6JX+!`u<>y-#huFKE@ZP`Lo+$aseZ)3Bc5Z}1pFo2WdiYJv^%=+E*jUzF z1g|ueQ?p~vK*7<`DN^oQo?_HDs|pHv;wdm!`5(IQzN~P;+71>ALi$Gwv z4H%65a_!wRj+T3tO3G-Pdl?I(1w0}jbm?)lEAg7;`YG65l=1y>7MU?#j1`owWf>o2 zT3`!$D~bpWXQed1gV{sYQnCPKs^&#ziyM~kRbG&y|H$4!i*i=q=7MplgM;N3H|AZZDz zAr%E}BwGuV!_0|5_S^IPut}an`dJOqdQt)t3OKmW8ng{XQ{8p%S|D{!sLZcW zLw9CM`4--CrH|i)0b?0WBl5Qb{cWxt2c79L$Q<)c_0s^XPxfNJKGuL_?Wj!r;|$Z! zIM`DC@_>l!t!m|!J&b#_Z9ZYjq&|@LY^!iM?PRU0dMP#5^O3#Q4)TEc5=wKk;kk;O zomEU|*}!Be{0o7}dKl zMfIraaIGC)%f3FoTtE@B400y)@TuWZky@?-y@8*PUx0tQ%yusB@hJ3#P($&gY&usO z!Rfr@R$t;%;zDA(!IWH(@1oJ%ET@ErK`pLnOWO;pAT7i?p!WW3VCtg=HlG|{&`1Xt z(bwL=>N$kXoq`Tx-R0?RYFwPK6Mje&H!r1A8m?pP@g^=uJ5Y!IhxTgO_xVgt4e9i@Es z^@mu}2lZ&CDbB4^d`4dYqg=Ly#0kwSoA(M|I(F0un5|Krtr_;K+OvP`fqO4{uR^2~ zAnQp3)L@+5vI;g{dX91JA~RRZ zqeW0#)kf9L*1O8*+q7;+GTZ?R)+J+3o)gbn3N+F-gU+>5R z=R`shY~i5fS!~_l&W2&s^~HmH%Oh;DuU3H<{d))0jabBbI!`d+&LC(afUcdLz_mZN zpAJJStgq9p|1;q7W)E%+_as#O$NxD1fP3SfUykhCQi+{Qxd_bH0g?-gavksh5Fqbv zhFx$IgyemH$G!117>0944(L%u$8ey?&wg+<6E+=cYyeDxe_}5ijrJ&onu{p%L`-?L=4x#Dxi%cwiYg*Sfzg zn>jVgrIDsfmxi1!I(Fhns7oVH=kkQuK^qMl+P7=tt*-Upm93gq>HC8%ThH_jrdL1A zOSG-)9-JCpeiit&0qW*@Htim)b5kX*wW0y~4>)S!UPFRjrD;VGtjjk{u~hgAe$AJy z)CsiZuMXoLi+NKSpp3@<%9q_b;nv=Hx4}0q>7Wcig|8m`UcRRhMV{FXmc>YCezm&6 ztR6S8;Iu547cK&|=ZD&Ip{Q{) zBHfBW5IIUww`HSuXkH$5Fo@bB#UKZ_b?_}#!NfAFkn%8s;-&ifB_yID;b1HneUIT# zZhAQjYO&z9^-y6yvcKc1H~Xa0`8E`8A}M@*j{zXR6J=EW#f^UF6-#EVES?21Iv8l2 zSZHA?*7W>>?o2{-&n$LZbd(OQV#)|t_o96}AlSAM&}j%vlC)=tWx}|0+e| zt4dJ%6PXh=IJA_W5h>~(mlGs4k5wW2a?SIkoYmX$B;f)4+&~K7l-Y&rp~#gC>+m9c z&8N@^2)t-09tid31Gocq&4ub13a=A2OwwRN9m$7>l zK@xFQBPOT+`5On#-sC`QVgGsHz{>#GXHTaC^X{_Cxgbe5jFw57YaNz?byHy0h+Q*| zXJw*wzXbQf64 z$a%gC_0nM{covx-B5&JV)Gk(TWOGS{hUlMB`hM|@gY1YTM;W;=T-FcKu7d7#k8^aS zX*m%hqL=Nw1r@D?&X&Z!K9rG_b=rf-({TsZykt(_wT9aT(t%#U=(-=s%)M<-*z{|x z2Fhhn=%zuG&cqSd#Bn`maz2_c$;v6Xp-UEu6`I?&bL_tNWqYMZLPM=-}EdJg*;c&;Pbz#mjLX?iUj<4f}ddUn9t4|+l&#= zDyU2=w}W2Ls2rMgZ3VJ~m9mV{N<_UezR=FoN;qu=HgmM?5r&M~V#hpEbrQ2^AkxbP zjFN_rmO#PwEzgC3x%|!k>p`F6)O8=GdK|ZwxGcCs;tMB$7fm4kev1O}0VCrYBxI1e zye4SiKNJQ8hYu$5$=Tf+dnt$*2|Mjy&O!twzeZ6lj}fX*>gtES;P?Cm1L8`hlT+Hk zzl!@%vh~Eye_Yen@hB8o+ORfQ(tD_P@fR#dvcV>;^~aD1Pd;b zY}8P#udI4sU+ep4fGpADl-J3#>BTcDdP>j3rm)@YT2Bp0pTJ`%%w<7aTYX!bF4y38 z7Qh1D=VLUN@;shBxP&}^^rA!L1;3!n7ZrPrKK3sS`5gxe41a|^37IT(Y|FQ{bqG7 z83BYgLdIy%#AFWzow0|3fpv3FjJ!QbF)ibHZe4I~eW1eUki_lpwkXST^Y~NBVuC)e za|1$`#y_T*Q_0eK-hXu)joX%1kplrl&goHerUQO_fhF_3krdcDnK!BxiqmQ5rN>*wG+~IMzg8@p0L=7iK_I^G22op~U^` zf?;|mS!cnxaN(S}a8AmP10-Yn1;T(p4sZfiQJL)4V9ezrWRM=K5HpCMz%zm+@kPmE z^wurha?F0Nt%2O5w)oWD$FS=UH)}~smYK@3Qe2zd+Jvz98plvcv@)b{hXwXykvyOS zJ!Wss9ChXBZ^eya0|qD;}^;vHdMyT-0DUi}=*R zDKjnlbg%Z+VOU(_=V&wh+f;(u;^r#}m7X@X9*!Y1xVEqs+#umkXMMs<=?68*uZ&UG zIR<1m6IjFXq27_6`^P2WmmHS``sTK=wTT0j{VGxWxsHy6vU}j0nERc3e|ZvZKWP4X zjgC7L`W`mIP0Ur;=_T~jknu%_Nj`+LgFK+9&Hr!P2yZ4>&QsCiMYsaILERrB*0z9A zzkQ?Fzu*)R(LthN4U`%w97dB4>WS&CIq+8iVI;vFNV2}Ek$fODiMLV?d~&jf$W%Rl zc=_MGxBN|ZV+6n18V$Xxr?rQ;BXUF_PP?b=UY-QhW?Sb ze!1Rc;e2;+eLkwo1uMgJ`ArZ_AgP7lkz%h1Yx~?UmQp~dsNyyoa=yv_8c$6y%@rB1 z@9}D~B`}LHxq64eGXpuP**Q!Wc<}=itquR)k477f){pla@u7Cf|Acs9r-E)ywXSC*V`t^FA zplWAr%qnzYffLR7x26=#8p2pUx~DmuS?nOMXbPxf*TfXWtm`Y{>A21wVwgX~RJjEv zSMPj5I{ z1nUOhgNW&U$zQ+a1I0c1Ifv(yESSKfX1G4sFLEacUZ7{|-oF79Hzzz!&O0)!mfimV zu8(%iHr;>5)yH2B&W<=l(Ea$mSW52>6;|9HDm;!{?%W^uG(GoW^?pOl5Ab)uJ>Ptd z(EsrCw)VA|6>w6-LwLIIsnsX&c#+0<^6f5zYac9R zCfR4Y)tqg%>PrT5(c_WjqAww-K6KK*og3bB|1+~(pB8nxHcYK)+>b`f5O22CIvb^ z@F#Fg%BSep-*kBG(11TiK#9drSqctcW|yd4U|OZD>#&u;%{d~W5aCg*m~_30#Il*R z%!c8ZkS(I*wTK&v6wZcmm>_UN5Ol;C#KZ{lh$IK5OwjTeCi`Q?w7P`Q`{qpW_9$VF z6`4SPqKMCtaSnhNekDYszqLUEraW)dlwbo8$?oGR!}=qoWTUBQ^<>SmjInWO%o_tU zC&;5gH&~Ts&7(JJrE!N>4P$+RD-8uu)2?lmMrM(o*1e4Rdo*bcHKMiM)7Ho9G32h% z7lv-pjobzFeH3ZUdq$97GdW}VsM4GEB2TW(nu7fZ)teNC4{~F;^J(HwO@^`ciGzKs zHK!0ziTo=y$7)8r{WI4GG@``&v~7=t7KYbi(EV#6pHReVlxrynT@)hHi1E_;!2<$W znSi>=o1PdYc`>{sWU?~||FRcJeZce*gFlSj-%>E@!=h^yw(K~jSxbEYq@56|) zKV}QzF({&8;jZ5M9)As1UMQT(!ip8haZ@3z2*hikhD-a;`&8MsIAhUu9a_}ajXD5( zCH!91U#4mugn9+I&{UP zoOLZqyC_Vn^1PA%Hrupx^2SJw8$NI^Op)Stf%?{ppY6cJnu62%{>Ef|Krx-@THo`c zXUWMjHP6#Of3~i{_%Ts4gPeBFQFZsQPNDX=@3Lg5k@C{lLm5C|3co(AuFR{XYnsul zVj_yi#CYMoNqyjN(K1M>{01@IBL9rBdEATxB&t%W^ zXkOsYDp9yVP2m|?rc2BrE8oB{uZG#h^$Tx@Tb!NWX!lc~5@mL2t!w*!lC9D1bEkw6giqS4W`Y^>!OK1CfG87bl<;OHzLr&CI z@%9QLf_@cd|6%J3stTtxx1G!=g!@5WjPsMf5NdswvSytNTHwsa2&AGHz375;+aF;R z*M$x{WTr&V0H8X7;S$ydZyZ`zqOJ|_I?*jhUl}#MVcDW`2IG=e=o$U9kXf4FMb+KTXkgnOcX z`cJcgf|Jbz3eEC0$)usj5zzVlBRIx@RJKEZ=LAM=Xq*U*O@7r+h6I7IBXVxk22uD(!?nkwm;=Pdkudl+cqMSpE_f;E3 zVOM3DZgmA+>7uq3FsMb1i+a@}YJ$J=Z?xsD#FpkF-B6xwn9H5!r~W(pyzgSSmZ)8i z7>{4NM~qw$a<$;Y1YRvKyFEr=67Z*CZ=MnrF+%1u>9=V$Y5Gq>nQkt)w=QT}I@T@* z;wlBA+=C~yGyJedWyYUkTtCnakRTcG60(Sp(m;|@_?N}NIA%EQGK~0?i+Vx76c+Rx z+^o&e+bLSyD%(nooHPkG+<0V)eGZSCVRDPobk1PJ_Pi8Y+_Vsz$PBcdL)2JxEwNx5 z!UeEYP@}-<&_U-!nei6f&}Q3ER??e@d0dKPE%~#W6rKuuo67F=>WY|Xq>$m+mZm4% zYhOrr{%0Q7K3cSsLszl)VtXfcr9R3g7hJEbn7lKiGm?+%ud!d;W@i#*rM^f?^zvi= zQI9PsJY#PX@k6r`CTAvnB4_@wmc~_DTOQ??<3Z0#s@qil&2YV2BVLsV43RQ3W*79X zqeh7W_|3<^P&Wc$@I2TIT#=a8oS#*eU!1(t3<=J4EsFr&Vims@vLerAgj!&Ym%#6O`YQenIt(~nz44!cUGu;@}R{VvU|WG;c3(0?s&3G_xMt9@Vz zq<C3aQm+G(6Of zc&ONfnGk~tYd`cxE29KWmnsQvuhH3-6eq7=WuurHx$?fB1RqZ#8c(@04A#99ZlZ)5 zDL#j0x}j|c-NLbRrM9}7c6YML==7tBZ6U7IacD|OJETX`0=wNMyA_hXyFvb=RDT8X z%+YiGi|1nxF~Ks3qrI3|N2vrC#>5l{bsSPQ8BdWsA%x301?FIqG?ZqCZ3vS{vT3R< z0ZJ7IUBxiHF!vl}B3Y0*2O@b_-S}xgq20xcvu@HhXb7k-PFWs*q|Y1Uv$I}?S5vPw zrXbV+{}Q!_XT(HI9YpOP{Nfsla&0ipuDnK}yLkrH3nwv(__1jwpEYrc@;4i6A!0$K z5!bki;;esY$|>tILxpfVFCN4TEaW&#k>(HP`wI%EYCdSqao23Un@ zAzjk;FHO;gsmFRY1%+b)rj!l=NLSwBn$En`6>5K!N>ILQ#&lkSr9Ke87g4b{Qt^d; z%$!rJV!S7N(I__sl>+@mp7pCEed7xf29z4Yqd11|?ZBDTeF+;(^4>~EqU}{d;`~*G zog%}!c?oXUi>}@b{x~H(>J2IblK?8-EL;zMZd6X=^G z15m3*h}9}d`FEi=X7d7Sc-C8GeCbV8i}P7Pn+kRM;@V&E7*7g_H2PqUj(r&Q`Vd%7 z_6l|V&#PUsZ>C!|W}W}oR?J$p>z$#Jn7PNIw$G8DK=D{8?!ZvJN%%U&V3)5cP5F)x zktvO&I>c4Tf*Qq7xK^dlCAQ8bwK62OHq2w+g@v4~DF#jFRxoK+I%Qbs+$3uJQ=L0` z58syc*uu}U66F-$U~(#%8t7lR5H%*f+VcEUFgX^ZLiWOf_dUPXDTx=yV!cJSQ0eYv z$|Y8f-)>U)9BVU~C=F&o8dKmZI3OXc9*!KtL~fBPFKom0AJIh2q|uPSOD2)T;7mKeseYZLh- zYG)Um6Y?wwxfh4gCTCH!4=hMtL*|C1o-7-}mexEh>6Oa5dVVNasQblZdOoNLiQou09^+<_K($PhSQiWryI50zUf`Qh1S zVP*;Nv=z5jNqntJtdavL<-?=2VlhhPK(Vxe8uqyp(k_~JF*CElm$5O&}O?}-=bClJd*)(w-_=*&QBa>!7;BmfeMtp zoITU0RpL7?jZC52lwq(ojWNsVoZL=iE+;XrqG2dVI}U?%D`)~(Lt>g4B%Y3E(@TD| zfdOr4RT+=8QL;%;o9e_IfF>((amuS^p)f#}lMvph{?K3BD)!2cOuo>{9 z>Tp~N{&^~jI+>|i8$qGIyTkWVN@m5F1g10ZXz%MOo0jFvNZHO>((5gv0%3otioEQl z9UkW9$MP0yvb1AL2R#ZnK4NGPt@J<>^_a=jNR)?VLl{OS*p3id<@yXN=LTy~|=AK+*paNz`p@sw)lcG3xSr#?ev8Uu3J5O=C zyv)-gbsa(WaA6T2v!%_~{CteA9V>&dx|Q`TQTA}HGn7Y$)X*};PNnghy=u*o5+7QZ z7qk9)-kF=Q4zq_}_bMu)40+D(!q3Iur3L!$yhv(AXPW@^ z+DGQxM`oQ`TiN=y&GxuMS1-mN9#XDJM!{3)JED3M-PR|gKzHC{YYd^jQ}_)%ve0H? zqdl9ZkaaxxJfwS|x)#J9$`a$QJE76GRycj?gLrSiS?@T6liX3V|KuD9-k@`8ZiSzI zt7}$&<6gFI?OWbpG>Z3tvRiuy$GBs(T6>6y_CRpA-Z%!kHypvs4N;l~F+ax)Fx1@V z#IN}|6Zt<0(m*za350v_D2gdRD;x$ivn*5a90oWizZKvL_^6e=5OztbZ-F!p%i~-G z72^>*iDbOWuwp0l49j?YEStg6#-lLoFiEvJJ2^YjT4SnCC(c?07Z;osowSHblH<&XzU+7>0O(f zVK;5maO#(H-Fzw8Bg8rLh`iW?6>h!i)%sJdLoQe(3~pu{VW}kvY)x)G?K4;m%s!bZIDY1bqM zto=xF%gzAWI|y+fl$DLMxt? zJ=F_-Ib5V1G<>K^9MMN;wNuuHW3^M_f~lkdSvul)D-w0V-|@%54`)~_eF;%28F5|= zBH;185#c5onhUssD9T;FPM52!sal(4Y%5Aggzp=&qN8Q(o1Q1jL8<5o%%>$dn=vg5 zxvS3-YikLJvXk^x{TmPNoz_tYDZUA^h25x5O0X$iBVepvSpHTNG7OtVBC!qh6ytK7 z%6tPmTHDP|r@gG6Nv3n2p|&92!Xp!Ig0VPj_*pq5*KGIce#%ctzWU4Y*rms;l(T*G zd~85%4?^{YFglA`7<3(`EIjj#%+wEF-3q{~YEU>Q&cy?{MF4y8LBuF1fC_%;Vi?Ph zF?x|kiSuMw&rfiA1!EYd4;FC+a{$>(u#5rV_Je9YkKo$I0fiWM-zZ8otijgWOvElN zqds2YK3*X29bIt5z57+Zl(r?0BYP56;~oX_tRa7!Twxnw(|uCaMCUvyu%ba&O+%z( z0H*JK(*{?(xEsSq*a#!Oism-K!hNy<^(rC&BI{S;GndtT^}Gl4Zt{+jCdvn~AfOMbQ&PuVrD%^^dAP z@l+?FUqRA(9JZS9k5OlKd5ZRmCGE-bM0r(Mnt3VFytN}ixN%0%)wu;6aWN*8vU@<> z#1%DcE+Jw-)j^7*?8%X2Lh(G6=)FNPOYG2RHwKp)W$)z{jI=fqCuEy&s$6xUEsKe;3IF1uRW|r!+Yb3?54$qhrHVbwgfl)X|?0 zQ|s}yC&FWj=cx0jI(FVc__rfV>Ba5IZ&brBIu9~ioYgyL3&~^9N1+;;t|f15J(sx4hBsrgMxGM@f*uTN8y{`?br9BSM)sln zyJxy@u?l||nz)WY{t$rW-2oyiJV*>2N| z`g)E3R71OJZ?PCT;>I)j3PBKm7j(R^jF~pg8MM8QILfZbZPqFkPnqVO6n^5(t$EfOu(OW-3Fyx0aw_X6;;-Wv4=6TGhrVpZir64fp znPTDS1MPagApFmZ0gFgLxY>EX5!Xixt-@bAoHBti=$Eq%+i$RT;ee!-3x=k`ALy;h zzwvk#{gbov0dxM6{*K$v?-_DjIW!ks1sMN1cWBA`;IYm4!FbL7q4!Y< zNbsxfpL@LwIO+ZYCMXLSA3H~Qg6ej=pM9_7dMwn$DTWi<(uibwWqh9PwPg=wEl7vC zpas31e{4+()D6*T_3gbf@^Z{OT@x3={zMgtU2x>YoF8zXat|#AK))7irx(tU#*7e2 zlkIcZ#cq=KCfaOjs_|>hgxRh_E4t8`8Q2^hJDA`oU;jk#d?yr|VR=ursEC_Vl`(T? zu*lH#ZY5;`ZOrFwih}QKnkHl0RlAvy>rr)xPO#S!`%~%KDJ=*HvHrwif2m*=2Y`ot zU^o{3RLM-GMTM}s*|Yp+TN6K{|LhHZ4~oDK)ur}{05gP-8G~5=9n6T-J%w5t=$v^y_K9Q+>hBA@6VJ<&!T(U2<9g$wKlasyb?sDnDRujVv=w!;v|#| z6tiKz84vnQeM~}3$cV(zx$ia@N5_K#nSPCp|6b)*BZ9fanZc%}$g4a>MIK;Y7t9GH z=RvrXXywh*wp71WAKw5AxdhSM47abFP`+u=&G~TS4lpaFd9gYsE_qV?6Bn2?DwzRs zqw$oT+zSxx&dw=h>$xO`!rITfIiibhdz)(SSk&5+7w4aUu zF$uB(kCSmG9uw18XjO*@GGhk;WfBz#i+3r{lh7tLx|pa02UHGbDPCO~8Y)99h`4sX zbwzS(w3dpSFd6o&BCXfk5eIQ{5#`w6-e9w?ad~f2xm$7xeiDUsQbn6`21ND4LRHj> z^;|B?D@fvD+a#&HRCSa$>MiR;*HLYJ8RF9s^^DRx)1QOa1RD3apW_iXI zL-SyVf2LEg8z^ze*ykwf(~I8wSK7$M)Vt1%(=e4}AN?6Y-P^3|`QB=O6U{jt-pR-V z&$RVRB=$%T3|a(lI-ENb&W#c0(vW?5NY<|>xjm4)ffOm;H$CnMuJW&y)oz93bQAz- zN9v}UlQyEOkr%0n7b+ZU)qe^M9Th^RAf#+=aPiFW!lm(9OS9TRg926cP4}(7(aWuu*@NQ;nZ;0_ zW#(N3E7g1;64cv1(rv$U;XTX!a2^ZjpT?#+-nKW^#sR8f&)W=p+W!9 zkZ(wcPbBo$%Pl!o=)+=@P@BFT+odK5HCQ^v6B=%3mHD8-2zMJ?G7`Pi+>q4Vm{q`L zarbQSiDL5I|4j}`#eLtn{bghXf-l zZp5$fG#SgbLN3)f=Su7oMYhQz>r{~y%7|(uc(oGTYVm&y&6Ub~w*wCP7=9(XChiq- zdsm(apca@V=Tmn<{080~?h3eJC3>j((Wv_1C2>=Eai@@TMUJr|2bd8eQxX@#q zsPRsexJQbdqyKv$M)<{!M^T?y{2x?qRVHcG=d;X%$7i|cJP=z)=}!Lz@5TNfQdarS z`$FuFJ$`KCBwY)*RWoQC$Y`v)4T8IEhe-qJ0q1(?PCIebjd8tm*Q}{IHSE_5U9>@Z z$bTq~;J`5}4v_GWv8W#zGB+y3KfZVws-Tk&g3&?$8_B5)%I#u?UjKx<^SXhathRUp zKy!~F6->~CZDmp>)>NIs&xdCHPQPp~w(K82J7PxZ1&;GVH49dfOOS>i!l;xVdq%9tWV8208a`lTO>cIUED92nv`AjG>*i0_Dqz!?dF zI}8ec2rPl}%T`X1blW)PUS{htl^BYQ9V#DRvXaHus_H!RytoAB;ygTRR6c`@+U=b3 z|1MVTs=9^9V$0=w;vZreCOoQaYn9Y2;bddd6))diRaZHeg%_a37eZMUeCLhg3wXUH zyzX)yS2>UK?Av*^og$lVku}$d`u|vvpt~It|Mlup&7d`Pvnv{G( zY=)5h_{$Vap&Qk@T}1l7$jo;@hM_n7TxfjN{}#BXRpK%lGK>BuBUQ_ZY&Wh#kKHVA z=>}*e3oTRM*n>yVtsF#i1#1)4DKN6Ue)D=+&E9^$s9=HFV!vn`yp1|bhc=qvpG&za zo2iE%akci(nS{Q9m|`;+r@kfo$W&7Imj88b8$+XECeqt7sUxsfuvFNjaf{ffAT=R< zTBaW_86IuuY1^{}^V{l6FrKbB!R7d~7c4aDg9+Pr2mR^NYSFegN`P6#SHV}%Tyn6y z5c7S0j&gl2bA4VD8m}*Xer|Syb_NFgYJX&Z(>5S~LkEul+CW8rx%!i=_an%a)J%Hy+#!F zin~Sr`m06$D*DT1eu8NaA@QAi`kN_vymR4n&HxNt&9$pW^e?epXYAuwR^F+Sfjq2*ua=P>rwtrLwZD(e(Ds8%q~M&t#jA2wF$*k5co+ z$G&dDy1p3~)#kz~q?`66cpM@(lS{UHJBv-h-=ZMn+iw~ddza}R%MRn&!8lXpdhZ<_ zF(#|#DcuCYqn5(h2*Lz+Psm^?zghMs^Q!SBfst`+$A8$lJDU)=bZ$89K8fzIv#%Tv z>n|C0IIag{x|_~eE6hf#{&ljpuerBP40gQKl|g8Ii3>-#JKIUF5hSf?214b*ui$Fj z6X`A?{Prp|y?MdxJMW>zevU@X=!FUHyypACa+mag_QL|Z`7q^SjIsxAG`s8%;S+4V zrmsJHBu?Vv`$Ar7K!G`6NuR?BWzlA}it3AOd?n}&zuJF2X|p#(;qCl*O3;1`6kcZ* zS%CEa>uU80cT92-D`f;e=))0>@_9AOHQzd-t$)(?0!`_RvdY(UaO$3l*~GO!qLT59 zitq#khzVeyDX~RsvQKIESZB)IunejRA@1=Ymr6oX z5OloQU|HfEbBtwqCWhV1o#dh359M5GvXxouEF0e1V?dZ2Jq-F_w4O}@d4}3%g8yEnDf>Dkuo%w4=X%w1f#^R_?tc$~_SJ4}Qh=X$ZYobB#C`t_ge_I~uA z=>s)_2Ow%nFFkgD>W*xiEYxy<$7zz{MGJ(UD z;s{W_;&2_GS(i(6N453aA4ZoCjdwKZb^u`Tk%v|y_c@I?R=is7N93Z(Z%K($>diyj~1U;Yx96ibRqyHh08B7ntYf<5^Q`cGz*J54@gEn-Lof2Xr> z;?9d4@U&1@Jd7d{Bd?QVEWIy;Yz9;a;IIl7PB8= zFR&_K5;ojQXgQZ{7z!Oo&n$zGA297GGG0t$YS)ZsbW(g-3M2(`7cR~5P@!PH@k6ko zdY_FVO~ne-+doeQv#!`rAIO$EyKXNvT$rG5dB?$e!A~~uBOg(D2$ATHNdwp&>h{hf5 zR3<3L4l1%xR8(n(!P>z%!lp}BI5HiIO4`Vssn)4cW|B`6oXugJ60hD01*x4`(qpPV zOMX!-?Dm8Sqzy4M4`<#lxVO9&GB&>&iIiY*hoN9z2fOv#q(oi5Bd#=Oal2)w3XbQ! zwZ-HsU5I9?S+m3_W+3mZO;r|r2#DLU&D8!M`H#g+ckajN(I9`vNjaBDYroASFv_FZ zwSdJ6I8DkP(g8u98)tNcPiEC_41QvO4TNRdPh~@<%Z*BfMCRgq<}4S23ystARStRT zso4T+&IfC?hqQ7HhL=YScQ!>$CDgWwW0!mAVK{T94;vA4yp0K)r^PFo=WjYJUX`BC z#`%4?=JYy4zm;KJCZBs>INx>b!#!R3W$X=bXj2DbNM{gW(B0>Wxzq&6HV@3m+n%&| zTPGFbclK7E)9EOqed-if8S7II_^nZ^5Xr9Dtqf(@+Al{GdwNl1S<-@$10Z{$I^51Q zlvscB$6P~lQ|E(EC&B9Oys&wzlHaQ{OeUOxJU8v07;Ev^3)p1YL=}H_0idRf*wL>M z5JSm+QDQ(4NQ+JkwP{}7gF7R%yOa2SD`YQkMuYD?2NA8 zUMHtO`2!YiyzvxcUp!?#0lY@=R&Ts4yN9k+!elfUxFZ;DzQ6{D51lq5Yw5RgbvoVb z?l82uK$$>&!4+SsFKS+knUJHn5xET$be~M=+8c?VHBo|Y+r3Oy+C`0R721Q^H^=J0 z`BF5vJ5iVxTw_fQSDvVQiGk~q%m-a4uSvxGAH=6X4q)Hq3j|O)&%gg&B2{EGOjw0) zbe*(>J`!d5Gt94r692hJ%B>{?Wm=bQkw+3B&#{wWH5L7c&Ac?9HGE$e z%16pY__@8upKAO|Q=Nb>wISJqB7Pn&Z?c|M=^>HQl(>N&pQ$+#`5^R@Hkgn#|MmmE zr!DB($BIY3Fkku7xkOIyn#uS zN`e*k9FM{{n`;?sx{kGdVpdAl=rsR>1%uk>7}^gE>Te95@jAS#lglIP_~jZJyq)1r zikT%gD}O1k8eA`(rEjg8FDu+MoodYO5w=&g&?}dZR`MB8fo$go7cXvs)zbF|r{O92 zl&RT>H@RYitXX-B9VlDO!Jb)4BQe(cOqBo&n!W0W8@Ur@{s08UGC#B<_tbrOGfg(I zjQLoP5c3vT@^A@i~b*@h;Vv?i`fUj^>82&t0@7ey&mgXMr0i>VA=a-%Fu_kel-ku-guOg#kM^r(5Of26t5faTilajZz3P z4Zj`8`aD_5UP<2T8cYTzSbg=;RY!j)i~a;n$#WWATI$@yM1{p5EPM#+-2KM>7iZd~ zpg+_Q^)9&Q@_<>+TVo4}$vMgKE33DCRcQ|{=>kW^oE#*D?vNagY~NkW z?2D*!0p&VBv;&6R4+`=`#cS$X`RyUFRSzg)`Ia_s(Km0>PpsP5c3(arTz{liNflugo2<5zf1sc_`dI=cH@@?^XSV6M zyMN^^8tSqPnc5Et0?A%L*l`8_h(bw2{pBzI4*Gy!2J~|rt5z@34dMZ1dq&sN@1`oN zl^JKB3A~|~(gtJ%1%pczj@#yqx+Op^dOu3j+;I{fsr)|36yqmP$;+P!QclUb+FOA= zP6;yBB&_}J>D$(&Pfj^X1OzDqTb;4P9}^c^-ONj$dQElro3eLAlya#94AM%ZlZwQr zh1#lyVV#I@#{AqPAkUH1x@Wqe#zgk0F^)Bwv7p9Kw@5NhinJ=34kX=qvM>|<@gefE zZ)EYJHlk!xM7cvM)K3Ui=i?^4qN=WMjUeRtN4>ruUU>3PQZ6K;=`}U$yrsnCZI6r^ z<%>0`Cwr$)MrbT^JWMWeDn!rK-QH+y7Kdsf1y(OA>6#8;*=JP6%=;bO;(fz0JZ|xlBpN(YX$CIH0t#2R zhtA@XF%&hA4=`WJ9zlCGS?_-#{%g9c<5+-o_YbTC{0|ey_&=w+s?Kg^s+P`XMy9e3 zCPx36^rofiDWE7JhVL!W(=OM|KN1x_DnYlFnHBA8=t#vvX@JRDb{b5A#MsER>%{ht z>vSO)Iv2S?*WaxKyjbox^lKA`-*=hmTW`mat7 z0Prc7@vC}v(+#Ru&DH|@Xh^P#gc7Zh8)LGmzw6E({)5g2YseVP8UBm>Z?UZ$Hk!w; zR>Hb7BuVBgxj&j_s3u6U`}G5XChXhYOS2Q|@MZNjDSZ72PF@be0K4ajTLq}$I&FX% z8$(BdAfS~X^ZGn|j|#1Yb}OmxXLP+?4vh+a1MUN*zdn#|_k{_CL&RAVj;j_v)lmI{ z4qe(wGuaBbn^epAl>}g)-FqI#^izRFGk3)kdrqUn=2G&W9Y(F(9h-iv6K*o$w~u@4 z+Q(;$C(5B$@?pKh3H$mxXH|d3F$aE+B8*?YZz5Hl{~-*@_od`Z+wSETWBkd zOXzr=$A7qO%O(DRNn-|KfKJq^z}1sgn@D2(nR%z9FxFpsqN1}S|2sJU`U^sqIl#E2 zEob6410_a|(g{n$2%~J;KFf_37+C$2;w+L1k_MCWzuN3B16oXVq(?tQ>cz82M5-Gq zgF@>2CuK(YaVY9s)5~8e6ca2W3c(jCnMXe8E%*ndRaIPss+z0-iTfX16K7NpsXB;d zF@B=wrLiW6=RMF5vc-lJe-epdUG>Ge0>ZhS%I%q6k=nx6S<)Y;fr9;$Un6AnZxWkt z2FVYwNc94k0Z>!;O6CY*lMamGV&;=5?pa=sk_)GA9=(we__pB-sh9(k9cKCWA*QLr z@JuS30C=Bu!1LR~rq~zs(Kk5lu(GOxDgp zj$hv`0+38B^JNGHZ?8fKlrfWf8`c8on%d9rKrXfktL6i zF{(VoBq@uP%N!GfCtpT4+I8C9+uVbPb%>l0b?0bd(s7|}=%!PIvt3&$CAp>v}hG~v!;;~&eI%z>EP&%R|wM2}G%Obup5GQ)_4 z8ydaP)bH`fF=q{zi&t|;Hj5~YpI+^b=h{aV8dsfMZ;IdnA0~?0obkxN1EsLhgQ^4zk#n(KB`E z?NGYIB9C8IE`f46sZLfa;t{>b8}z~=?LYqwJ}k-B3{w8jmmvS)Vp;wlVn@Zq(#+J& z*34Pe$i?P=`&C>#AAGPNYUml!qlL*CnC!oKlsG(bWNA@g`i6rP!8nXla|g7%ff3B~ zt?1k{)5m@jIcuK}zZwT48>OY4GPJ2js;QdO-9`?RM@GfgwNiE8w<%XCw03k6jdG{d zp~+%YagTCSHb(*(-^Hf z2ovh>6DH6%;SmVyZN$SyC-&Om%*@EB&|y(W^xH{Asfl%YZn}dl-8W$W)Q8zXVW6^{ zPtLRa&-4EU#sykFh!q^ku$M%G13||el#msh_B-KgwU==g@&RrMLUgSEUGth4Ux}m0 z3~q@eko zLL8TW^*e+HE|Xl**}5h^#m!m|!OqWbP3>gm#Vo3Kq=bh`(|HdABy10g1muK*gmsY* z--&my4pDiDD*f(0sBn{#md>?QiNEIzxCOG55awa(&|otW@P7EJ1q9W{ zH{y~gfBz5Fj`50^rNJpA0QM=}sb#^^X(Vk>hPvVZX4 zelQ#+EYsTKNIIR&>48 zpL$PRbVAvrM%>Ge(EO1CYeeAAPUDW8&#&3c6h6wm9MHln`?{Uee{avI#th+ zWmv$Xs(1mQby^ZRQOg4yV8?+cwoy4$=?-eERo3J?4nrMZ>ya}c2`3E-pgNsKdNcKd zChyYT;uEv(+65AJ1#E~iTb(KxAyz1w8kpWL z!pn5V83a5F^p0B8sT`11KB+}B( z&S}%eHkx8F?_e8Jw(a7X1B$$WYgvSeZpotk%f`}m^V>`f12aY%*>-77;}%!x7f$P(nBwOoXMD?7MMxzd6P0tc7Re;nXJc<M8IS2og{F=*Z>^38g+ zolc*wmBgmOH6X=#154>$cB#dLqI0U>v>zetGH`?}}?M*8k~4 z-4pv=hK5$4P&R(^<^%2eB};2i*k1tfTG{KLW1+QO9}rXDt&PL>jESq|i~!#yS5(8n zB_M0BudKOa-bIS^(>g0Cfy*6HTe)j^kXvqFcYf@eokYjcqb+Ju(vfAoY)jiZo_RM| z0oO5%?+9yo%lD@EiRx|-wti*l-blW#$ccW{9NJ95FY?Z?27-2i{(+FyAFU2D%{=Mu zY1-@ln_&*aWb84xhJ_=UDZhEjJD)D8*$zKsjP=2jV#PCwj?&Q>EadsVL&Eu}me0a) z2Aulz4)b$G*dJ;l29VHPZoKxWw7r)#p1^CCW*oF2;|<&fPuPYp@I{9?f$vrLS&YRV zlK|#ZIo)tvUS%2OyOhW%wu4TP;DsY7e#HKgQ_=K-Ly!1~%$QqAS|n!*I}N$)dNo{% z@bpH;!PelAJIs>0=6v(H=3TD*@eSR{{=6wK)R=;hNgMi=E*@*EKHtd`)DO<5avtxw zGLJ$9_iy3?)$`@VCDroWntnD$v&_MI#oDy>!-Teuw7bRtd6>EcH z)F~IAf#9pb`Y7V2WI>CemYXrpYxAW$faXu}b{TaSLW~~tq`Yd6DoM|Mrwq-B|8mzd zB|@>7yGaOdC|@&#kKQIjLg7{!$s?ur#Fd9tVy{s~vTnx2p{NAxZ{+8j~-1#63s+pNl(Z%)f~wZxyop`La+4$r{|k-KdFH zN@NO=)?DQ2Q>B$jCB^Fe440H0n`P#cb#50{lMba{?2~tiN7z*ih_^z0zWxwLv<29% z$t>~8N`SfQyF;-z$$&u_?-$gc$P{Zdj(eQm>8!z_>;alwsj%Zu&$DZ_Uh&+T4XWP% zkFj?QuPs`(wX z`!7@59cHK*s^&Y(7w9=UWvK(q4gm?VVyvC!Gf-WXwXL8cwY&vzk6=ot#|ukso^U7x zj3nRwf`FZy^0VWN08zc!XMa$2dD*>COal+r@N-|kC^zI)HC-|Kz$}~KqRB?Gz!L1E zBr*^_iI*wOhwUW@tw5;371Cny=(BGX8=+6NeGu)H0w0ui?3#3;}^?%7hXKe-3fInO+4~;>oncaUh*F<4+9cnouh$b) z?VT|Vw>5j+wmUj0;&97ngf}}0TDpojuYs0(PoMR>7d#*0dW0?Zi6X=G2I-~sxApqA zX93hAD2|?rsk=4g7Il^2i2iYt2lF3~ByQI|=4e8}P=8D9;f?yPvdJ+e@RllNe40_KZH7k&Rz&D>Z7 z*2(-+I12nUU^M@GJtAS_=;YvRZEWLY{QEx(NJSkvOaWvbBFfUHh-Anj%GI|eI^FdW zUTXt!HUa!e3qo!yQ1puoAWUmmps^O*EvCfseeDc|;ygFg6dN`CIiu2Z2Govu?{IljNV8j84_M^I?n@)8q@$ z#F$Cek_@f7AVIJgSCKv}p`xDG&tY*cq2rFTlG-8ps6&L~{IPSCeW~aT;{w%&U~~Jv zS{%_`ad+=EXO(nZadypMir?rixb5&MbM@%UCPP6f{;~uUaqi?rn$T2C1ka;#IX**1 zKE-0cDY-_JbzsScpU`=EJZL%;svvD3HcB-+jWeAMn$Tg7b($E^gCs+T`789-c_@%E z5Xnj|ft~42#^hwVj*=LVd`}8C5SgxFmM3^g;&QsvoR#0Ixu`7giPV@XA^9LZ%FkMf zN1F&O{PWuF1Vg#E#s-K&K&TZTjWIc_2xZ=t?i&0|gIbJUICitqr{~-d2IwHJhi`ay z1)=fu-QzPLYr@=xr1PU!gn!$)NjU)!*0Z;hNMj?#fB#t>82gOd>%vw(zjTt_!>IyU zFFJ863IqvOsgx-uQ~x6mDOY#b!WWYfx8jI zC=r1x1>>0POmBHVI{(~%gz4pg#n{AmD%rFv3X*Khfwgk7L~X5MArrJ$6n2z#{mx%31LtzdmCBkk?a_MMqZEBJHyA&?9&q!3^p(uMwoq{ z5&^oY=S{0yu6s5`a~MZ5v&shpYn8fBG?)M{)_Blvx5_PqHThD0w?QSikmBh;{GOeG z*|8f77J@QoagW!XFntD$Qj2uyp)KD0ON2FL?DgS2&#R<~UT3I4e3r9FH!Y2SQZHcJbc~ZSP}sFAjq3H}H+SnRMi#K+_7au5mCO zPrgpGZEO2{y*=UdGNlET(2QJwtEs0n0&3w%tRxa{zcQW})S&QJM) z*I3e&SWC|A##y*ecl%OnoBP{wkwbz@8g@q5hH(%TRzHTLkN24tc`Eu z6H6Y7;$lD>$M@3{Tub&+PQtN~WPl~{mXzc4nynU!q4yEc4rnja zIrntQ2(3R?uZVX?THs410H=;U>_1i!3)9AAedWN6Ct@u}gBGZ55kZh%Y2Z}scB~ANj zq-ly~ZNUji#3AkQZj!;ul$0uxzbMGL^P?MNTF^T*xdaYt;#&D`etCf+-k^omVU0p1 z0KxqmP@K-nZn!@@E|~$i+MyjNM{5_;w%GFEi)}l< z*FWJs!-J$-&SPLtUy5z>=sp}`gm#)_NHcfh`DIl4TjiX-^j-lF0lHmK;%Y)IV11cO zqew0}D%gU}163Ni>^2FU!{|y)uNVWG$%tXncAS6snVLFRnIEax@>b;K6=2CMWQgn>~S1ThPGfl2wonRtsuc0Voj2*^@r ztCL;Dv`I9Td-(5uB}UQc8A2mibW;2-m)cU74*&d)x!`}6Sr@B7&QrFZ=g z)*%)a5fxC8R{qamROpZ$;D^tent+FfAix(E5c!3y4G9Sh4go?)pdmk4*uxIhc4Ao2 zkQmUDFPOgr^g=$k9uxx_v9Xr9v6j8$?bY1{#6FC=ODwsV=(4Wa+^{;iR#b^QDK8&v zG~$6w9N+BiBC2&iX>)zn?&^*ggc~-Sb<(D)Iud(k!@f&?FHhPCwM={_Rur1TqYS~- z)66}74w2oXd0Q)dgX#m`zm139q<+}PuqRfm+n+2Di zY-uPn>-VA(teAyvG&|_V|IB1JS!v>w(I8!UADa}}WJ;M6kkAg-di|9^#0s)Pq7=ZP z#N#!z9!X5nNW2W`fQE(mc@4-sOZ|5P@w`8|mbx{1D&VpEpB8>Q#CdD~XVK+>0021t z_l5s=k|t{FVEJzp^uMbKV#fbmb43~3g`XngywwF!J3%ln@325#2nkj_sRjrol{{YY zOt>Eb|FO|N#bhIz?M@6H?F-hFUl2U+3&022Fbh8b9y{V-i_Mms$=1cR$NSp>aEI51 zX%ni6?ML;JETv9RLLlq+BmoQ3fFbZT)j(th;j1UXewY4Me zUljE>*uUm*z0(irC_!y7V7O@SvrG>m`Xjd~-{Gr@zwK5@l$s*#OkE(f@+b3?4XIBb z+X+3QBQN2=t6#p9Er+R+?(O;vA4<%cVzCxwBttmc_kmOG4ps86tt9I-qq~@~ zB^&IMoX&n-Af@Vf=}uN4p(F{qdQr;rM=Ep0ceyKqB}uuOQLQs~5`tO-)d{=$h-kG_ z`qxX&ap$>-A$Uw+%b?yXFVO!+|)30_aEF6 zBoRUz${gZX8D&j{QYXReS3;aq0wIoB4eW?!<@|ODGUij3dJ7spI~#-)`;b|ZCsdGn zKe0ddUk8@2M6_O`JyL({hAz4SU!6qR!f9zvP!%jix0O5~D%1xUEmr$!`mHZ^e&zXR zD!YL!LuICYTNdE=W|S2tb<3!en!x943TO8Q$#@2)_!r^*<0orGR6|$w6EP4!(ZldB zL=LgMfrF)hk(0Tt&3{Bp@PEl|GSY@oJ4EW_yVO5Z z#w9xo>zpFfja`mCeRJplETh1s6l%d*v@Odl3w|KkST&{N^!#!$na#D9LRo(hen0y$ zjA=$D7MDpIvI=-CylYRzlW5w9Q3$y9AsDG!vkPY?-j@nnBox6K7SW~beSK09QFi1y zWI81cP%aqWWt<#3zGxMv}b3`#tBP$+gd-c;Zv@%eMT+|Sps z*UyhZxVw9meC<8$`t{-Q4PX~d&GlpZrJiX+%DJAQH5cmUX zx#B!ISL(i-fW&SAiCufQ%5u9P%DRU8J(af;`sc!VIVkWn9%f&j)HumnfC-R)gNd9t z2$y4$ptGVmhICY+uwOi?a?ZHUuL#5+XW(^<7HI$oKp!?#;K%d-${pc$qjg`aSJ&)Ad zXq=1XI}}_4xOeB1f5!V2T7iJXKZ|+qiuCHEu92*6kRC#Qmk zdE5X=FGqUb9_<_U_vtfN&fV8G{?<=ln*ZB}E_5?_1#>Ys(^wT>vp|4(A-WiM`2R zq<0FGuxM*$He!z)>0s5#?)iw38EsYJP>q$UZ+Ubm#^<0q^})}Ps0ljg>oJ8a33fP2 zrW-@yEl2sg8-9McO7*+hAZ+h_zG}a)xQ#xgF%_d8iR_hY+p0#NJ$htU+EgTGlj09s zgWwqa_0f0F`PRr*$g2zG5pUhXfn(wLX_P2o38)YZ!uU!AI;I&Qu9g|tg>tLj?N&J- zKb?Nh85le8K)h?2!AZrw{N?9ArT>fjn(pi$IRzRO06^kjSnvOD`u~Sr;eUj{Dm4f- ztQC|m-1Rtg)CLT+v(<1kf*}G~?2_m*%U;0!(KNqkpz3o-hGC-+3~tP|*3$&m)9!2M z_2Lc*t@U9WXq(NX>XPG!@5Yi1bC2sx1Lg#PvJxIK)1xU**Y5YN>%Z@RPiDS-K34!> zcVhv8G#X3^jEtRhmhMAbWSSf-W=O0vu6P2yu$Zgb5{pfuOzA3^+;y6skY)ai9UKwD z1JXUoawm5uw56QRF%4V&c)O`AmU$hRyAXVqnd{vr6k$4!5qGr-vU294SW?rMRru_# z)5D3xVy(b;OVsI6PCoTMMBtHEm7+Vq`vJ4DpdP?0@a=ATR9OQ`dz%^ELHbjxqcVzV zZ{iB<{w_QD%AYS=31bEoqWObSQHl+}Q(;y~VO=djPw`ULvgu;9dJ5tztWEcs>?5i8 zsUmnk2L)K0MdyK90}8y?ry#LwAhbtN>ZKAqeghC?DJ_*NsVoBew<39(LK8SqnDHAW zDX*6C3-H9cKrBv|KjkW6d+u#5C5gGBnCSW++%4Q^(7+S0$@X0ptxzqf)xy1V7~4x? zxSFBRNur<-#~q;Ip|0x8$pOcXa#loXWnXu>F^Aw{7se=|EnNtt2U7U!Bm+n8KtLvK zE8Gyn4%wmG61|#CBFoJ2PvEe@sPrM|RtV6UCP8D?9I-4d+d`+^L$zBZ_Hl7+u&|4o zF+GI%;Xm(o3jCEAXm)!Inf%aHEKxKPDvS3Yi!>E`TPOj`k&T96PQ{`^ACyN%$O=OY zL;{p4B1Pb?+F@6myh-9`wv|3NafyMVK8Z8i_8UZQ5Vy&jcjIlz#blxeJM3j9#19B< zp%k7PT~qbRX%PN73jUX9$ymyt--4lnmOacAC29|Y)JHbAKz}}Uq6{^&kb0s}S;mI2 z(5m^KL3&rG@I9c{zv?AfRJalNu!5%R7n^3J8>8sbP-V^x0%r0WjLecL@+7;AEE58% z29t@z)eG`9pt7)6J;khDx^=buLMEtO*+c%7^z?n{k9(6C?8$hdlb13PB?DWwD3+?$ zjv>N+b{PZv5~~3DI6K@D(3yCWQ(=CpW)cH*k4fc9te+OPrj{}{EktV&kwm|YL*r?H zi4&yBT0rGkbWw!M+JuRdEi(X))BB6NODc0h{pc+%W8(ZE-`Tt1hAfmY+;yQ^c&@IA zS>*K+pf zS0oIneN)%^F1Nek{x`v{q;F-DKJOU!O97THBWl~#^-j*bLuSoOCg?=TJ`JT17@;Q6 z2WV^d2Xrj9dsS7OMJJS4#j;R-#ksEZ8QV8)+asjxqpO8hP??k5Th@%Kz8bS5Za`*0 z!tg?Mk#}oM&>K)4BZoIS@co*dnlG0T=#4s{I3h5YDzmBqEz3LYX7SLf>H+R8ASYYt z%Dt-a10C)-w-;I$G;B5^@s*yr$Lp{?Jm5z;3H{Xj46)ofl3t>i#$mCdxq^9h;X@i` z4|upX<+qdYZ8#F$0iYTW;C_cYnNwwZXzdb95vGe?|1BX$&p8RT5Eio#>-MX>%sAI= zK^;R7uxT2BT`^2PmzV*VJ+?c|1#K;GZDDw%CNoq9rwC5G&}v3l9okTQ+>-C#G1vPx z;!W7|kbk?iv6Ian2$oX_mfOPs<=n$o$T3qI1KoOz>YquN74~9epZ*pmXb9*X*P}tW zzo4xL)1|P!8O_VqS(TjPbW@($@q~_5;{n;AaA0$C!#=q0IH)^CHj>152JUKw{p}kg z0Zyzyr)b^w&p;@DXX7A3_$uo1B>2kaMWxZDXxd>8iBNBJqduu5yPn)bX?HYlCQZ<( zaC31#=v^Q6mX^U)9|~p(M=u`7OMONs)z(}T6Jp-L%S&e~yoT?)5)r--s#=v>iz$$G z8hACu$;V#Y*=iN3ZC|5 zOG(uc*&t6(O_@!StuEq7As%tiq6OK~59roEgI9+mr7T#J5QPUj{k}GcIAMr5v5z=` z7kL2I97@#PuNEWeEKRf3(g10;#t?vSXuVh-jJx1=&|T%oLVkWzq~=~@f7ug6(v4t& zb6TW%vdecwaU{viV-JZtxz7vbd9cIqShbYh65_kgUtwzBbvhC3lK?W+EF2xfM`k#1 zRKDgYr~9{eyJSq(MSc5+uF$c}6^bXsE>~;!Q4?g7NaddfEqR(t$CaHnOLE5*c>;mR zRs{-y8LNcc=e;%(&O4H8^(%kE9;`62#zm}oLQIz`DP`FQsczkObf8*1zA;@_3 zk)WBS2B%MYT|2n=1e|9hlH;&e`K6fiEFwC^O1xC#{!z3|hj2y*2arFsRU_sWEtPav zrD(3me{NYD`!z#bo;0q7KY*i8j22b$?Fg&)B8yB%tW?PIcdl2W*H0^o9-PY`US-x# zEnnV3E{R_@Rz&1HPZ}+~3z9CEWa57Ap#o&|=CQ>em?(niQZ0ghAnE$G`7F7cQ-K)6 zTcp!!?(t{kX=|ivOobZZELQbq;por85JkUUrG8PFCrFQW-+9qj&hzVr+L7&_b2`86 z3GQ27!~&j<0h&~RFZ}DKf0l5}Hp`cAesEXpPaQ=5FVsN94F6GWCaYL0VW}Yd(Ar`N z#<4alqTB|UAwg>NMl9hk73&-%|L*&O`s7zI&AP4d z?Z?}T+Fz5&NMcU2cX65V<~ZIw=048q@cp_!zXqT!n54I%XrScYWuJ%8_SEy}W{!NK zm2l)hMrr^>A4q>U)Ju!8lRB!}dY_*MZO5p;tn|KwXxYikjL&Ml)FdQNew;^3skG2) zV>4Td&+*p{adQ$Sb(-$wGGKt`0=XSyf*Hi1&2pg^+77r}_lVEqO8zUW4?iNQ=X=Rhl4;y{D-9y9&atQHJfShx#3MLem=-l%?xL1SAFD*JRhA=fEO0NV}cfVdLb zG5GcjgdShsE$G1S4rR@9MTKe%vkeBUvu=O3@PS^Q8{uRF$|c?bYc(V{DBBTx5M{-l zN@RESKyB8$1M;Th`%*tihX}PIHB^;9c;!2L-3KCv$w|xehnfnK5;UdaYL0!bIL4O< zOr38|HCmJln7@lv&oMGqhX#^Ev=9jL6RGp67zvU$ zA+g6Ja3gnd%KgZHMjrMqX;S{RmQcOC3QKTCSrLrh-1qwPd#D$203T(s7;-mg{mxprgStH6^AcxJ?O3D`164o!V56V-CXRw(lcQ?=y-gwFfwf_8wJV$0w-(n& zbxGF@d*w3E1+h6KJG#XK>T-6t?jVp*Znfz~V?W1|tpu`;JHAn^4MnM3uC)s*#8^YU zYXvm}d;B=m2R>6?x_lgtvXZR3c$aVkP1BZA;B}U?RH{IJC3A)-MY|C*2;F|N!e1rB zH%aSpswcoCK$J*Kw8r%cZt4@!?0UL^M&kn!*&Ut%I87*#0Eu&aHtd_1iQD^yLH7|7 z-w}JGcy~zD>2L~;CPBRLz|p^ma8&%&dl-sN!tf10pfsW$h6-v~@QTO4Pn!@a<`zqg zpTxAxZ$a`pD)W3hWYt52$AVt*2DDjUQ@THs3PMN`4Bs5PgQBW;TBzg>sKerM~IOS1_?WZW5 zaZC4)yubhjQnBZUutoo*7`FdC#mE>tIhY$c{(D>XU)}A0Wt?mkYe%G?jAN7Nk~ozT z1Rx-vI}~OPX{DqYNt%+nE=WLOVPRQj`!fN&zIAQt!Up|a=>x=P2m>UaDdx{hFW7ge zZy$!<^cFRdM5>{=={3)DmgAp$pZkaFKfWOJp*SH4yC*nT4iku~E3DU0D@boEN3fS3 zGB4HUZd83`N2|rxt*;4y!}#UTFW%K!n>J^zE>{o(bZRgqRamltEv+OEF5lHYfv%bP z^D9?x#YHP_C3l;LoMmfMce>MrpG?0)rpUvXvb^djK#ZWt^0|A+^UxoFKr8Xb=v5g` zYe(&x;CJ77!+t4mnT3n4AlkTF2nj)A4#fKh9i&=sZ>iXY8F=yM;t$YcijP6`7<;^8 zl&|@q78vFVPouzkNpN}sizT89#1N>SaD8V)C}in}N!g;);vlo?5!xck?AqE@P@%)3 z6@zvJ4nhrdey=eEJ#9B-Zd!s+WXUs7@TmSwi;rbw7vkvrf_z24QyRz|t&qP(1tI<& zeb$c$8>U|P!U{=&J-o>8Em5eSah1_+$DyG=3bUyL^9pSD3r&d~E}Ni3tvYuwx3RT_ zwEHnoa%;&B!jWRqFo)6@?4n?%_79#W3{)%i#k>zaQ%^ z*C!%076eqmE@gFDYrr|C>|-6DK6@8W?pU<#vW`udu~Z{Px-ke9v}wVGXZOTq2-%#u znq8tnb1mO0R91VDAxaB{Q8q2fi37o&!p6YQz$F;V`fgWr^ri9f8J-ckcOk_2}Ld*hE*T3Xz2jf*qP!Ul1dav37T0oY6b9RYrwCn8ntb zj@~q#F>HQ*31-Ry7(8Rd%XmRJCEo97(l$K(YZkX+*uGJYa&}aO9^{M<{B)9+UU1j+ zUSQc_&*(lv9@cMyaMx0I<3D!CF6vXPScS{o0d&@$@8joFFkrk9(?;C>QN2;BG-djC zvQJK_&=~@Wb7(|z0jM78XiM4;RI}+8dOToefoa+uXkI}c@i7;4&4B6i)!bfQxPdE6 zZ)r{MH8SFNLh72sDa{50kORGpVM4ByK{bEn zAvZH}P~3}f-+ywB;J&C`?EffjU_Zxv{*_bh|HD?2mCqFyay-x7oZalO${ zVZ`7_>fdy@5TM{uE+Kz+mLR0dZI||%HAgRcYLMq&wNmv#V$K< z_7HRi{KetnH*42o6}cCAzLWuq^hL>A2LY(0-a6w)z!9+oPMtO4fLai<--KROLB?s# zFwjigDx=?Yy?2AkowJbKDU46rbJ}A$E}E!H9XRy<)Zp-Hn!lagL%N20e)hYm3!J2w zff9HuZ{U(Wz#&EoS-1-e*yP}E3T$hmB^tHFn4{KWx$EyZegO?R7>ng0#LMt`fmc7I zV4eyYmaWhTCXqBp&09y%y4yzcbC_=55cozQY#A4L*nGC}nBslrNQVa=i+R$s z0X8Q=Hkc%a@Gj!P33_6EA9%qV2lNnqc0jcq6#w`r>o(~IbrmKaY8(oT(3Ig=f~Tdq zm7zXH7aVUR_knzn8VXs>sRIp?4M(R87HLdSYRrDE0PuB<>A(_GR8f6oIAusTFX&r0 zwC)JEd?-1>oU{G~dVjtsjMk8x#v5%hZYW25AL8yVi2HupUYf~KeO>fR9QCIj4dgl$ zK30HV2#KF3I#Hs#Ii63#R@QcgkRZT8+RjQGi$<2}#&7u(!^s055E$gb5=kx1Bv@YA1)kVjD zpN|fwhM~*!9w#QV->-O%`Hnx1H}7d@zK-+604j5B`WbCY4b-75{rzymKt4Mj?1XJs zcekgvVXXHmXpnI)z+VHm{{GrRgE}z)5mwK#&a`&t-EDoyPq|ohF%L?N#SnUQ00|CC zG1^X}-zpFRML|X=E|Hegc~Q+-9^e5o(7C4|kAZ5DlN&oQhAghz51zmT)U564i&N?1 zI9W3M1e$a1=z6nT#DlC}3E(|_VHDK3a{xIE;z)mjPx%d`x`l~_x!yp53T?3g)E=M_ zRBq*oJV}~FY(-p|GHa;do7;y4>0z{6iyOr{c*#qK45>WgyXw7C>|T&}gA3va0o4#8 zPcK(X*MKd!8qVOGOw>Up-rTTm$_y>jWuVz1`dv+e5AbAtT#d1NX zq12wVk}3{|U2R=D2pa2a;FlQ_f$9c_MEI52D&B>y)%9e$askqQ zeclB6(*?#d);p>#L1W1r7mEg^h5UH{P$UG4InlBq%!*~aKdrU8e`uVZf5PE=G8iqn`5)jHFoxyWUu#iw^CWNkKB-9Kut!zYA#3B1yu(5jHp0F}ke5ew{ zq3)Bhatl(J-NH8?K%^snbHsvOd!&7hqCIzc5Qq%0S`RJ@3Bs8Ln(3j@p!U>VhJ8q! zD{0>9jZkbnyV~Lr0GX@E8Ux8;UPO}_!le8@S$mEVK0qXP2DA<7sF*3Rz~iaH6yB`&WgiZ~@RHbx_{1c| z@d@GX{!~zelc9*j>RSvv0Y!WLYNagP`h0C5%f)XP`_*>3+sP9W^2rYv#&RZ0v=YqR zJumGYLA<}o1M%tL3Vk9F$MpmXzt{vTn0~95_guPNQXHeCd10)#7lg?4!B)rP=p2`! z_qNGC49pDvt+gNELb<$9JR|ZCKFyL*K&!IP0<*K+^0krJ@ ztRr}86EOmre3s<;qVz4KM2}E47qmgh*UngwAI4vzMI&qtQsU3*-|}!v-JYP2AOxx_ zrE`*yA1cc4wjD6u^J|T&?RJ5L$d-8Z)FvN738G?3y(w#EiXM>%nEYs040@w{GCrd) zO1|ir&u&+eZnrX>{1)7N-u=wi)E*#{9dy}aE{WB(Q4CJ49s>J$8Ii(GEIda+13FT} zXtQUQchKF6b>&x;K82Jg8mnOKl#}(TxUN+=|9!orf_o9}>s$!-&Nt5N;6M+DfeY|ycv)LZAHb}|VgXg;5B}9h7fK%Bdn-1}Q$%k?cGjyhW zugK#=K7&)Qmiiev%usuQYrp%;pd~ReUuD6aD6g(r?qFRRm~oSCKR50bAJ5V}rudC8 z%KjOPA%++BC~kPEm8puU1bP=97ZINHMa7~c?SQtMRVXw=olwOKBsQ!n7Lj&FcO~)s zbuJiBXn^8=pTU0ID;e(F=$E0Dx1iN0=JA(&=X9In%Cv2?7cg13Vph_auS8Krnrrn& zm8X&7X}@&Go)qR#pM@cra6Q&~S_UT$hSyBv#w4h*7fZN^k-=QCZl;c9V#jN?>gMmn z7Lo(Bnpw(jf7^&&o|(?%v$bmugZ*mqj^i|#4IqN+TzIV<3z+kV(_hwbY-c!LDRt1( z6Z;Kmqg$9gBbes2wvC();0CZYO4O0Y*H-KGlqhB)NL+nnJX))N+D;jkj~0gyeHuS! zLCAcB)su*nua-IN2ZvD9c!{te7Le-2HX-1rC37q#PtWRCFv{%S3enFY_m8#Hk!)~~ zKLXm|Z}rM6UQlevhv{xDb4sn*CO@j6YAlQ~5y2GwclS318985%&>b4A0%ednr+Wrt4>R(Thbzb)FER9X;NF=CX`%=cI*rGGxY-HyCpiYj zJKTBp&LpIny~&qP3<@qH@<>>G#Gc8xPQeNHG%PIy^WNM)@@VcB!uD!dDHyf;zxov2GJ47q&TliUb4M0wmf1d$7cA zlM;=9m8Mf!y0v>a+~&m@il*`6BQZ&HSt|z%hK+H;g@QQ?3zX%weu?OSCoAZNcfLL4 zgV-Q{QPW<=pacy!3M8h|`JlB3Zqu!2&C%moy!hjMkrYIgQd^UFcjA%j)E4!`+T}Hz zMSjI@z!sscwItW4fVYs9IBxyjw&6IJkS_pH!F`cNu2 z7}Q&7vY87Zf^WF2y(eW(6O^dF%N%~4Yjn(E`|&{aQy0&pB{k0B*%y4(j;*V}4~_^H zBlTzeX%GIeLfWrN{$IYVckbJMYi5SQ6?K49jsB*YFd*u&F8nc^cC3N$KX9CZoMGKB z8d^7My02|MNvRfMi(0Msl3~|^TFQ*lL|vIL(%w29csC(`{k21gQrCdG2^mc*B9dlk zNKvO78^Rg3%%-BOI&mr7lr}HsZo%x_B+;|5nbN4JXv)~!F@60`{mwn)H)){|CdbJW zcSv^^`7O5SHaYDk92f=@QxwLh1UsDmu@5qC@9am?i<_wZEe&}(e_;zZYsgk?I}f3} zvz%br2Heu6M3gG8^}!54l1#9e+YY%hz22@8xI6c{PtblEW3p-HIGPgVTP%G8Hssyj z{mCAln0yj_I@!oZVS~l0jD8EPJ{a}xwee&k(`+bf*37R~fJ}P=mp#H$@s?fmiTJ>T zSz<9yb^9FbaQ(yki=tAtz$NH}3T4t$je^!qo)A~L5X%hc85i;!hf;6FM2ku`mcwx0 zbOvLck!JAaW$E0S6((E2qAr^m^uJm&Q5_yC0>=I&^||v5F>q0PLdhUR5}I#ou|ioc z0$AUc)Sed9hKuUVm31$AMt41c#xL}|{m8V4EiRAltt52pxww>Vu$t0T;B(yzy&x9+G8*W}&IA@6ui z;`zC`c6kdD^VJ|9NZ<`m;95u`G%f}lE{?@|<4-y^QhvJTI0xjoExP83;(kqL3^Z*^ z(p9Q~msDi6RjFm407jfU=tE}Mf7u6^90SFm9gyK-_w2UNaq;3khi?wTKr%4q%&5fG z7%wX5k<^S(eA)Iev1V1N@T*W~Rk?#pFi3YXxGe##>CIK}Ew+CbLLDABv*Mzp0Yd1F zIJQ}J^G=zBt?22JuL+ z4pYrPAa+JvGx$!|Ek-`lX@(BAyxhOLO$8A|P)>_T&?lzw z*iw7|`uc$Iv7l_P^Kgn zc{7VPAB3jPBXn4cqBqG(LEtme*__No(6bTk6lb?cD&9&(e(10kpo`#&kX*RpvrH-` zyY69|`bt-hiEH!CE~JxZm2ChC>d0e%kss@s8IK1%kl{+SA{4VCoRE%}o%>Z1_rg#* z<_5biG5I%>%KrAfplxkN4w&MoeWa{>;htDK>jtGI>}<$f$3Cu{tYIa*vqipm7UDP5 z5<3v@U@Pw4HWF*fR8eAKJ>X8}6@Z{y4S7!TwHY4u-?SADEgjQRcb-V{_1(Sm%Yt?UX=n&b0g#{|w>U;4j+8F~2 zX;p1aGo=q^Dj(1yZEngNZP6h*5(=Aqkm9|m;(`foMSBEamFTF2bci8^rfkKThJG}5 z2fFnvO%n>LM9Ltg6?HPJOo^a9dy6foAs)%*;(($Kt9d;WpU(cOnF>~)K`n;RlM$J8 zT=WuD8Qdk%I^70qVk09BgsPG!<|1cf2lFZ^edAF3?uE9Gwuma#LVu9}+bFx3Qbg^v zyVciW-vy2lQ`9?8Qof3Vo_%^eD9G$ai6hUxf2J`({fNw~Kc=IDA0qSraI^iVqf5li z*vR>R+-%7STQ-OS$RpoC$mID|%2uDa1!(e@38WrN7KYR^VZt&5#9djiKwRyURv~LX zo&K>#`yxm(KU`+q&4r-xG@H$l_vpj-IBV?V_5GgBPt9%eMsQt~IxCD2g+vsOicO^| zo|karjATJQ-QZn$Y|zqDQXkqe-iA z2~J4SfNV#{i2UY~=z9zFtEgk|&1GVA$*=7e8!MjY6icQ@+H~^$wjHEHAui|_0mGMc z8SL0>94>}kvh`3eOS$!(H}F2IAU{AJ&U0!r!t8w*5Xwu!;8cR=ygkNn!;;ShMw{8` z=&M7=ht36)&IN4cx~NPPWV^`b=e!)o$`~hPXXzQJ*QVv3`|P7oE!(Op>-k))Kd4lNP)BkP}O12~B0MHSI+Y0_q~J`wt*&Q-{B{OsAo_rHt8 z=m?j(W00F6(~_kBijI?HmT)_R40uQADuEDbtyikh)jH%z;LfJYf^ZYvI&u@& za8eCefEu($uVddPSb3ywe00mBHAwGAFtVwo)HR!RXFmd*^~7;?@_&LYqy6o=ubet0 zmm75$=N{LW`u-=s6d3H@79TnQ0P@da8|MEV z`rNXhC0oUoT1Ubm$i44zH(R1a8oU{n&|mN89M5LY<#(^gKp;BAxl>IHB@N5M zPA+(uAVCvy1Xx`QQ6rPrlpPc9iHgHH#0$oa3@xt7X5rDOvq5PiI#_K?ayXFw%R`t< zI##WHPGbg{aR6P?IBC;t?D#c{YSK=Ud?nUIuE+7lJz@qDG3%vpH$sbMV8d+Aw)8tj zYWgWYiHnlc-&r5kAe@H*Kt+SX+~|}wP);+|kRt^fPM8x(Ouq$6Zlz_L#+9?e6{&Gy zf;J2fr_MM!IB>%y3Y!h5Tm=~19@Ev3X3p$qL~a2*boGh~(Q4=gh)SeIBU zwUxG%RhrCIRICp}WR9zH^%Pq<@*Xqm6t-TLDs8v=fOD!xw>)XfgjU5)s zn`Dw{SYRFj^rn7SmTLcu4K6EhbGKGfL4`&xW~KGEQPMHK$Lw9!XRri-hSRX(Kr3*> zvQRTvs>50gtz=b&tZ~le<;6Z>%tX>^2Bx!QrISe8RUOsU#N z$%vq)hL2tiq9pw_NewkpcSnk3gs!fwFG5mBtd@T^cOV{)Kj|XU-{(e52`sjOR?(ne zH)S-XC150d37^`32O?yeg6$baR&k*mrARrQi4!(7X~vBXF`^1$r+ewhGsxz@j(- zi4`HcIVui|G2KrVhp6y#*Q&(Pxh|A5xFS4oyn2&*tdPY<$O%JP_FjYrAuwg05kto? zIL2%kSl#N&31REa;cArua%$$ zq3d?>TAARP(?G|^aQRXOMB%e&Rh?xE7b)?jQxCW;{0u#V*IgOu;DX!gg%JhA#?0HJ zK4s>b)&t_{iCnD{RXFRC4f$7S2le!*2bH-}x0OrWb|LN|tXR;E4H_zw3>EJ33{@;A zvMrJ$J2s^e39zB>Q4?yc&J`itH)JK7Ot;Q8Rp<1zj5sgf=`n}+x)XRpF0&GG7d*g# z^(m)uh;*#}_mhA;X?X-^HwhCdhzv>kAlwTv;0%Uwh1_A_#q#gqi(HDs9r(-?K}Jj& zrxB3_icBBEH6-%1#^4zY=ER7>=|3hJF51)19;hK|dK%la+D-L&J}M0Y9j6Lxm-#$I zhhqdR#j-R0yI11mgHCTJsGgPOFzaGnx*j=iy<+sujDBaFg%NnjI9M=eB|wIjU@#uS z!gsUB2F`YpB3;YSEhAjWX3+wnCnRu93lj-~g2ihv!x70{KJmKo(sv{PAw*gz?ApiR z-Wt#sR0(>$xdM7k;9fAFA<~U3p!wNNrG^7l8_;i{U3*3N-bft<;k5$W^^^C`fsj>k zcb5b_a`(;Zn-}w^9bpT*{Ot4jKCwQ2tj1FGn<2kr2=gZ-5v3C&)oNtnys7e3 zO2Ne-X+Fbzs2_X+m|`=w;?2BPP4w)KvI)OuPGr{J9o4<*RhO1*oz}gG~%o3<`^$R;0BJd-tXQ7>bUBNwwu?fE;d65Jbq4oQ81Q?Jv zSQkfrWifB&sXM(vv0tQw%cCdFQ_=GG9waee&u()+sCoR%qikv7m|b5BXa|J+3SESj zoBM{jh5r3Ks!Flq^p=XK#_1X|8r(GE$^d~>6(3ZI?-vSK=ca_O?S0WFQ#!R1vNu|{ zb6D(O{6JC~#}bH}3ua(&L4QKvp)R^sdW&Qb6g6#dxBeE|d11)r0PcyeL{FMbhCByv zeI-7)L8vugcz`7}%jIeKBn`x=E3bi^G@W(e6pzWcK=bFO1zJGX92P56#i;!&ZV{Jhe;yQ3YX!&_VIrNKQuHgVSaq(D zjEuVyh6Vh_NhxYwZbjga43(4Vm-m)f_EIw2R7P9May2mo>)G5Q z+1HqDQkB56#Q{b9nXIyXT-4~16Ih4Ahcy-F;>_2=6nr1vvChx%tNJ2Dfi^4x>sqG` z;KT-7hWo)Ua^s=Z_9GTf$38>)Qtgo25`$-C9km)T@L)qI{77CD=?-s~2~uO!=YM>z zF&Ve~^x70nyRP{FQ$@q0?+di}y3phdPNWNp1Y$4E9t?`cOIu4o7 z0rP-m?-Q2~vX=|Jk2zd}VDT4~Ul%%s9_khh5y{MM zJ{5Vaj={2kD89KLfiZiQRHvrlJjIX2dc|AP4mB{)IRut*4@ZKUMz(s7<0MPPMdbh; zrDg9ZBIg&Cf2O|BXe}QluO9a$%kP#NKqjD{A$m7dbX62Q%s)T zkoM%RFnH)6+YcTu#dA__ErxbdU!f5Dx_=HYjvK@wH*4^N`iB1%ew+@?Ta|`B`!J^S z73r7IY?c1|0W@NgB$h*Qhunfg!aUNb*3tiDj^ zPj3aBeSkP>r-T!j)$#$O+g`Y&m0}u1j;}SSm(p)z#I?KuqQSoTC6li2;XBV{n1J#w3rtBl7iyUgm4AtXU*&p zfc}dZ4jC3X#~XzaGBe~GmvXGWRXjA!C&B6sb*sk9uUuVPC5XYx__6VU#wN4d6R`X? zPWq~^pimkG61=&cCJ7l>*B>M9$rVH!<(d{ug|&hznjbt?vDD9>=d7iEwAWK$g>qxG z3>m2tbtOpk3qB*9DKggA3nP;RV&r?r*V7CUFFBXO+<4u7PTcvAiGBvKf5dmO3HfMN zwgB1ey2Y?z!|}`?Tlv`X5oR)ST~=Q^>%NcIWtr6<5ud3v=_p@nd3LBZr7`4U2Ga;M zSd}%;l4GZ8Dhi~)MC&X=dYTP^sga?yx>4J@^36|^ReO|CYP4Q>GU*ydkq=H+tfTRk z$jc>}lLxUG3S+6UpzR#BpLA8gHp!@E5@s%?)GK#lHN}Rx5zsncEHhU|QSFe(VZ~%P z@0?(6tVl++OVgWJ7Y2_R#=1P%A|tW#58Z&9+)tapx=?^3ahgiB4Y%iCt1{Eel^J+R z&WWTDg+oPT?RG$}B{XUq|E13BMKHZc)=mti$+Vw0$^U;?d&eMKqa|CkZ0xda+qP}n zwySp8wr$(iF59+k?5bDiba&i)PTw2xI^J6`WBpp+{PX3SGc!kyG0b)FF_YZ98uU*O zTdofS$d=wE^0koQG8J6vN}39%;)R=*s^CJTGbAuM@HKd4QsjuxWm3G5pRy!G&N-lO zs-{wI!kMvdN@uecKg!+dWWDJ6JNd43b`2cRV^d@4&oyKp>U|?L^?pAs%wX~Efi(WFp{Ur*b( zHfYN7gt{D2KP89zSq)mLa#&)CtpyuY9;kVqpMuI6yS&%?a0axVmrU+Xf~+H{l?^F{EerMd70NJ*P!sj-ac zMnYV%Cee9&x-B)sUb89X^R!ts6@-Tj!2<2>&B zzDH>0-#Du*eA5$q|M>+(ru%+c>hl;0WM%??q{bf9iF5eGwJXMR*pt(4mZw0913zc^ z`kqzlV+R;xE6LaZHwtPV5dApq}l7N`p)iUe#c?%e>05*h&2dpK8+PJtCE4EJ{Bs&)k#mt^^nT@fL?$;Ym<%uCD!OelaKfzOJ2%(G@qX?0#5S(u(p2!@ON9?dbLy(W z2B5$uQS3DZ$nlsa^E&v1XP^eWI-fA84+|h1@nLN;ZWY7dAt<2<;!1ne7z9vBtg|40 zM&UCX8Bs_Oe0Pm!U=q)ZnaivdtFIbA%G#7Mtw(NJP9Ns!2jO<2H!dpm4cQ zun#G);O?7XeYkmIp}jm<&2uwGNznZ>mApK3@lx?HE5x2l=h71km_kp8Bn#V8YTH<| zFNom}(r@L!NtaG08V^>=dvicx7i4x}fWj$( z<#^Xn+JmIbpdXHicqgHrcX`jcxsx2-Qeuv(Feg`9(B=w3x04=mDOnJjMkGDYh}XM^ zOc}zVJhW>QJw<3MZA@3q#;I($iy5yg&|M0(!wPfQYS@ zfuxg?@eOMHh{^&;FM9eLlXBmxJgBzt&6@}Af>Sf=SQAwgVP;?GX&RS zkNYl5B&OTjz#n*dVZvWzZNN~#)xQvbqpLktCh{OA8-pUnEa(~}Bf!4-gTpv}iJ%x>?saqnnT&D_#D%aGWOmvMxgb%W3bRJM^j_OK2tJ3-X zmVuf>g2~#Iy0wEx2BDooBuEjW(%J}}t00L#oeShh$AWyc1aU3)bTq}b(Kg7?5m3!D zjtC-P3q@9C-Jy$cBh?24(1&e1^W%+?RIkPJ@j(UR&%I-H*Jht`{>P*5_B6yjp@JG&3a=s<)BR9v}umj^S?PI9Q#H~Fr)FCM-FCmxf_;a$EpeQjLnoi%F$ zO5WRh5T0-8xEYnE$cr#*&s7``*E6d~YWa8%96mX|+rZ^sV5DzZ<@>mICIcUb3mgSX z+X@-Q4>EY-$!xNv7MwPGl1eV~sYGG2M71Cap`eg;$S?rE@Jtd)+D{EW4d)4tb{yPJ z2i@4aI40V2NQ|)E3P*lp_<5rnWBv_lvtRuUS8BzPHT>2G@9h|5K-=e@Y~>%=;j1?9?j8$kx<`xC;~#eVxJ$CopwL)(}R7-{rN7E+_c(MdOl9%i9^1* z?kUlBus1H~MCi9#HKv;Y7hfS8&CcR9h;4X&BTcF|X!9&Wv}A0Y5qwB3Gi9yH639_2+oixvd^a{ud$X|r>dlNbPY}LK#gPZrw?$1J9 zUnlURlRLV@tCe%u>bV1t&mTLeoutuTyoS$Ff(e|J&e`fM3LaxxOop!qUEdRjj6hl{ z6`SrzOskxVmM?zQ>+SQ55CCQf7x5n z;y`)jO81&(2Ht3nQq6gKr5V0}F7G4DT6OciUZt8s;TIm%x%z77D?e2{BYVdZUs-4m z?eb;3d6u4QOQBzZ+2b;03;C!voM$y2xr$=h`nI$^)ilbBKGrC>va0Jt?oH~h)R;6I zu1-=%I^eh-$Dv!!7L7}!QJxJai-gO-< zL@Qdt$~+^@$K0#cuu>=GX6B|mG`D@HUa)PZ7fLQyO8CTf1b}8%Oz~5RqWQ#Tuooj( zA1xYl7P?T~1`&<@$0r`*2FL$^TSUZ_qy0&aBW(>pxuvv-)Hzh>4nzBP{PT`s#5byd zdt}z>Z-Mxh@-d4lomk2$XCi#^{cn1>KNtkS4`lVv58BVSksY1Afun)7wTZQht%a+J zqmv1pg&iI6zu_4F`2Bx9M)H4u%;^VTGqASsH24S3_1`vmZ#VJG`@zhDf0zxD|8F;u z6qfqmWLy5Jd!=Y%Z*5>?^1msPWh;*TgUIg#OCC^&f6YaTiyWO;#ukm%S|*Id;BGqt z*V4jMWY)*8-7^-*aJLKosxXp)a4^tMl6_s_x%8y#Qk<)Yn+rf~6k#yva0SJtO_5)r zxg2yCM5D8TBPw2CBvU~W<=!UKu#IKWvKK~f(vdWo_mMcin3zzk>^Z` z%@;#1bS!lyXdh~DcT?cd#Prxi&eX&a!G@VM;dfd~b?rd~RU-0+7t|;cvV~fbx8s)}VrCkN+A)_X41)m;NsJVhHxm$C(}$%W z_EtJ?wECVs1fTocG=cn2!?Ewutg_L?HDtQ7u4TPmH#wx!kW@sR^tMYIrK4d%8zh`J z=tc5NP;+53{T@9?a|U&r%0U=SIY`4}82aA>i244DK6*saY~%&3F|($QctY!>0;dLr zhVo5dPba6B)0wFqquZD~@{!ZYAZImaU8FtO>#{h7%s)ZVo8oDp-PiO7VaX?

>JwKWpFReR(Sp#xFg&7!HiZm>lOO3%@^;KFexJ;92)mkAL zEVh*~2FFH`%M}<{%oIgq&DqP@2osahl~NXZvaQGv{!A28lf%v%&TX}8$61X9njNKj z$T;F#%Bf50{R|D8E{G>cKwC2fW%&GKHb5Mt(a?*B#~_^CavCgrc#V%7<)~N;kJcf$ zR@P99u;?Cx9CY$nXW4K>K_kBcAfSV6wlJoSpkg>Pmq3ByFYdb_rwA7Fg0ZFp44|Wm zbFhBHP_!9hz;)|HTKgZMeL#26@f_6l>M=@r2uw*VWZ?%NlS-dB} z$n@vXBQLViPjv%b$@R+#kTGPg;P?~8BgT=L+9{IGb$U=m{dK1Z@?pof*T1+#c80uk zu1BA&Xvv;5<0b;StBxz5?4l-Az4@gJxrg5Y`iwf0$SOs`gx5ly2*X8K*QqH=%SA}= zn5V{HCmyP}VK5c}W~?9N)9INIUV_!pw`VpkPx+e0thSi0OQSOdG5J>xGy0gS&bjig zsEy=HRX40~%IK$w!WZvWFM~2P7P+q~K}UuD%N!jy zH!Gu^At#Hv7F@;RiJO*r*QHCK(!-~C^^;h%NU)7@n4Z>dRSx z#Ru}{iae@4_<=B338UMCV>a8od4#+d19MAPbMYGW@>ILGX+*s54=&C-WY7KQH&+18 zr0*l($l*q9+OORPBKKNm-<8P67E_(kWG(FF21&4uB`;) z>6OodtUM8mM&9+6nS{yB4RYJ;O0T-2vZoxq^P2$|z3-;l9r7oL+n`6$Z(6%S^4_|@M)@IEH7FV=vBgC&$Ox|SB zTU6X+OKsIG1G$NlU(CIvG-Wypc_ql^-+2Q4C$@EB;s&<|b&v=ln^Mt6i_r--K8sgo z*)jW2Z$yb_gc&l6uL0s--j{=F-dH8kyxvo-WT#$=dL2I+2`l?zFB&iZraMJiY@wk3 zL)T`30RWKxAKeim?#?ETw*Sm$emoqcelDQ@enD8N{ZP8M*g#072MTF~fs#YSh`TVA z6s!;xisZ=wm4PJjT7=Ly{Lf&l7%8g#{@B$tXjiItraU!%kJi%8bUQr|{^CDNot)}E zxpaEw{gAz1;p_bd)Q5^iKYnmElyhQUSez;_;ab9>d>K=tDXD_<^<=cBz0cS(&G(xD+-(XN~iq`SAgY)E2NT%JAy3@U`CRSy>z@%#I5$UL@wjz6l`#QI0+w? zeLY#RczCJ(#KR|+N!31hZ{!WE{Tj0*z&6CSx~`+S@IODP%g7!fqg=1 z8L-$&3&!QQMVUyYp$5TlRXGZj3UDRW=WVv~W6e{UJ`Rs#i(%)3>)d+H{VQz9*N6^z znjwVd`&WUAJqwlGVAQbOzJ^V}uODhJCGUs~qwX1RSP8~;wj&O4tJ5|Z9aRwW{3oL^ zx}bL(O79EyQ)g=b6=yVE+g7jfX&HSy?RP~2MT?UhwwGQ{MBAKQdD<}c;I4wkImii} z#9*TSD>c+@>sKUC0=M^P76$E7bU?p*scucS0}x)ttwHeKwMfrXY>y15LwM?22tnLf z<~`c|vEScxl{lsP7<~vRpz5Fx97rB3yTn->jlp@f2)R~gvQe=~dk(7;7AK)X<9AxN z!RAb6B9GaYW6QTK6B{{SI~^umtqd(^^Xi&45whADtdbbOigjtfXmn2xq$bOJmcQ!@ zC!{`^`DBG=532-%;7#TOIv60r_OZM|x&Z650y7^hTvtFD%7u*B*J$G>NAIL$BYr|R zR>D577&sK)STMGtpP^yhJio>SB(DPS;LWT1S_|^1$SFfbzF45pi{i));N>rb3KA(e zFN&7=`XGuGQPz<3h8ZY-f6#|GoQKqs+GoGF)5-fV_UMer=_gG4<~75oQY6BVa(vQo z5Ra1S?KSc_upGs{a%9iw8I6R7eVef0^yHviytNjH!;dYKV&zr^vtOffS9JUH~*CGVt zAhh^^+F^Cnse@xC$AZ_dw%#WYNS)r+_j&k*)w}-zY%mFBP8aMBlX)x8t@Q=^ule%5 z&7j}*4--75i+>Fq2MAu10a>_ zIY4xtsEi@*ppaMy#{F@$bf(F#LJc6sovZp*9*^f zmp_7--?-Lv_Y2pF_X*d_*4wS$jV|D<|2Mg{gmD2m?ChKy-}#)2ef z`Jqur_GD3^(XugVM`WtNinXQyd2q2&YNfEplct~spBv)92B7+yHP2)yGlmwq? z@W63`>L?hP%>?tv-^f5HmS6$fxL0IOJJvB4tUw3~>W0a|u_~K04~<|GhitcL*^+{n zlgX@h6@v?FEj8|VjM$8|D48~4Rwhe4HtL1DhKIOGRKyI z4384V=CsNEN6_@F>XRMT?@vIUXy}y3oA6P1*y~lCiS*PLXJiXk;M#_Xn6 zilpYM9V}5ETgZHDD&*@nhOd{nL3~V<0E>rLTA9i7l&6(K(x_Mi69}1CcXX+`CMZB= zszuIXrs}KoyE|!5ukk;oGAXyqDHnSZP_2g1_}i$W=#d}}IFK1CC-}=$%A}A+s~A%G zG5N=(L)u(}Rw5gM!m~BCKcfP6C@#X=Y^#=Gn2)K=n{mT5~9bXfNdCH8dU+jdiiaV?Jh|rXTU>{evY} z=7t`TvCul|I65@vMT%Uj2~K25q(^CqN3vu0iHOgKL1!-A@X_}f7XgtdKw}6ENJcem zSb=#-@xRW9z~;k0-fW|yChyNI`4<_(2)z+e{xV&QGpKT3XYG`Xf$nb}ng!(VfAU z;AlMYfWktJg_LY2n$FRmO>`8A%egv3I8WAFQ8ac~n1>3NJ#|g6^lgjTf7x!M+WHC_ zMiNmvYcqv#@O}zVqcCQV0jv7aCM$*|3!7N@e*>8XZ8W}L)Qs}jaEhH2Kpnl2ay3SB zEg-9E8+Dr>;AsB}m#w!?uNs}ZOozI7K<5m{2s31%sd*=hGmif2+&S1fjX6a6PK>UiOZSQ`IWf=wY4Z#^)P}fA#QTOOm>$jvbKJke;`DZ#GAF%qQ5V+j=d-^E6&xytFm$9+-;N=qhc(>B)4I&;2{@LGvZn3CWpchNG_z!VUE|`6 z3%t_SS@G0ofuZ*onmF{ibn3dZXh5~69d$%czQnB-ekf)G+W^l~qSqzJp3L`@X3?Qt zn<6P>knDYan;6=7Ug~@pR{EfD+QUDV=-h37Yl_kbv!K|&4lv}vi#k<1wX1Y4^=G@4 zw`K8$VQsU49OZB4%Vv$Dg{8DbSaj0%3F)JW(Bghs`NwkR#_BvGn7#V&;zj4ce>FJz zbk%l(i8sp=6tGc7+C3wazGRE`GU>7zC3`5n!iaHpk$m+pBIoStcl-4i z`FiU02%zS9FGZAK;=EX%gKqMr?h&Ny8?gQ%I6pzCSjNACj4~J^`<;{-$t_VR&gG4( zXWuA^*elJ%7H#&iUShAfZ*BxYcXx#R-IHOL!8s3uy~iSff@8uLGnaC2Q84GU{AX&9 zg|mD&YNb39Aa`e%b+>IR1I=L#c{^(>H5L`Z9gH!|ZeNSj7dKWrFNl87t2?Zsx%^3T z;SY1q2AxEc++L`MzS!WX`r`p6_dfMQ--V97ho_X4JA)JV6!t{ZAl@7y9NsSHSW`lL zVseD}Oi?V=7+A0Kyp_>ox?yVh-Nb8D-Pog9)1yB)cBZKL+Rmclg5 z%+X&i4Bmd&PYE2M(8l@5{QjRcC7(l_!C0eF=0x){yp(r9>2&bko-*Z;`dn&`_LU*R z`Zwypn2Us|+JcOVZqBhnXD(q!Shc~oj|t{n1Mvg<%qK@Vw@6+WUx)5JV*WCCGb*yJ zD9~cr5z5{rF(pwc$xSEYxpOBUGsLM_Zt+8l<^Twk{pyR6KsLTo&D*8o4tsgm?X_F= z%5X{y;JZlWxmM3I)XyGAw&QIja`5XJBeWsC83lRR;hhptuW(_dmVjrpoSS36EYC{J z^UZM)?ayIs(v->#SC;}BQdxHt{IlG_;6p5Vr#U(Xy5CwzS0Q2M`i7_?{9Ep;1N26sjK7uDLX>{VKoLpM51PxUNo zY}3yhx^2thUx8ZN)A!E!M{F>n45*Xq9Vy+YQ=ge)ns;j~Szm0%D+bHS)_?vA8SQ23 z-uag{Z6~7lq6zsA_`1g8Fk)_Fhh4YRv-9N~Ngd+r7z;Qy+q)WItfTT=Rhb=LfdomX zIpMp%kU%B;3$#OST~>DCxT_FK9((5mf*> zrDBayI6$)~+ui~)H=$>YUtoluuSS0HAQ{NoChzFYORtP4FB&&7U{hUilc&|>92{6- znW= zwWx|<_|J2IGCVS3??kK$)zGN*x%h?(W^gLy+xzGH#OC}tC392~*oeHlD=-9ekmv`X zRobaY6dF+pSpdfmiy{!*K4=C%FH3(aWPnjI+O2_szDIV4J?IFq%Ckrg;*B@v<6QWy zl_M6y1P&!#8Zc#FfH{a^=mm!{?4-THh(;ljY32OqaL4|TbkfHC75lm-Vc^A%Y|jZndann)7v7?6e1C2V z$#=7brUPLfs$^II7sY2)cChkeb~aRh#UZ_4NMIlq19ON$h%Lejjj=|g2}7KFy;cE? zLxC#l&mJ~zxFs&rP-VsVYf}F9VI@*iF+u~3juw!u?6|FlADm3TSsIydqK=his%u!M zI9s>I88-yyKAiSUBHI(6VsO3(xU{oqwKaPu_$jyAjAYzKzE9DeFv1S(YnH>#jQ=nXnfcs6Y}5&{s+_$u5>s#~2xOyN9jZ zM=!-(Qu16BM)bARI~*%~V&>#M4-KAotBpzlE?9t&1%r=me@Syf3zTcf&N6Ha+&Hsz zZ%EZyS(=*WQb4#LiD%8+3jG*HcXAF|*rT`zGf!zPX4OI?Ac1F2P>_=GoIl+e)xK3B zhQgl8Z-MoJ1wCXjVa%HkoGUp5ukh7NDeT-Us{L%j4&C2`kKKg%9sXdRI-9?#{{m1r zYjCb?Q1S!|W~iA5qBtlK6YNoW>WZ;%a}VeiA{bz>FxHHTkE82nUaS-~$sn$4k9Kpe zcXe@=e=#*Vq5eG`Ku6T&3G|&7;4x$gP^cYo=nBOtQTk4TRmwfFA3W|$P zyu^ZUR$V7TeGjXny{B3DT8~ZJ?29+&LV4Y#IM*}udr`zwMGOK2K8 zkfUKm{Ry4{!b{T0dxn;k11oZHM#!7*;?0T-y}TnvwPm%E0aWH8cr6+ZEc4n*Efe6(o9tUp{(SsAjS z8PV8Ey{`a#4Kw97NWY!mmDgBJT&xJO20+gIjO3Qr9Fycqn}5=`A^ucn#i=HBwmv7> zT;pPGgz8lNa7!PDVYBhzSOCt}T!MqzW z0Bj*C<3P)6734VNp4N@d$G=IsD5=U@+d%^W9Ag6ji2M&v@_(X*{}5D{Gqn7{eEv}s z|0lkq4yld3gz*he4Rc8gNk9OZZ^@k22L#v!N^B^I44MFtVA$lKPD($DFT zYg1jVW`m`5pdaNMg03o~-3m)%YQ2Cx$Y{m7rd6FG>%(3m z;!;glQhUBpCYBx9Rw1Zh@4bjwF?WzMC^XmB+`m?}*5UQ1cucjf@8xwqdOfTp@57~l z{6dn

(+lCOqPtslD=2Xv}dO+P!r8?@2JZQ-C(I}+|3>bM26+R`wCL%xx_WcYwb zlRBS`oZg2Gb7LFk3M+%WPT$7N%5CC(I%UabYC;Pcxft-&(RvUYU%|c6hP`8y13p;$ z%-TY?UrDEO-P4`Y_(Ic<>@OD9-j{U+jhr}a#*{BwSit*z%h|#2^D0U9LC8`9*Z{AF z`~tCliSi}Ybp!rLmJQ%=f-Psw7U3s@{Im#L|++3l8Xy$6!-JRIQ9qM9}u= z5DRu0sECQubB#G9QHpKEA#dzb{Iq%g{-auqHttne_(1Fm z0(-_!D%6T`Ygc0K3aJ5Ng2 z%{D_$2+Ls9YQp2-a*lI^ol!SXEU8s--$^?Q#P zT>Q3J^ITC~z*xi~q8!^daE>gy92kZgXA~$htiC;4HegNEP-;@ln_Y=yT9E0NP7gk7 zv2(Xc$xt4r9oz`2URK&WOV9xI@?^!CwnUM0if0@Gos%9(9O9HrXv*jbW*bDliyWoG z?7S$gVZ%X+8M(G~U4!Y5V~v7E|JA~jz@u`EpRJK&@U{_@MsC++6k{;XOfvz}J|n#! zpf>AiO?%67J#5$JFgj%l-KelJp!`_Xz_t1|i0;6~7D;BYRZT4tV14iHs4B|tJ>?6! zO~Br(B=F=+Df^Tg#Jh17a%0G=UI0Uz#d#uJ=*gYKdZb;L=V)o#oI#|je5~JU%2d1n zr#;GOW$%^9TMp$ZzHEZ7ebet1cslq&YV7Ki_2b^uwAwN}{`e(vpyC*UMDh+Suo(vL z$S;27Mk+muKEJTLeHqQANhsU*KpMrh71+|O@kl(-j?xP1z{4xde$+!;GYJpw0L?qF zFK9~Ej5F00HE)?qV61t8C}g*q~?d#PGP)Mi$m-PuIwf zxzJ)`lzj4r!l`H7y;GeJwJd$a>QbfSCLEq6p-Nriyp z>^A@Tl@~ygKwcw;gq_^7T-6ed5U4xsfw<3B%& zbXB@m%|b!rR45*Q5L#DxmiB64a16zgDPxrJbli(91rJ#87a+u|#R> ziNyoCZ?%Yd2a_J|)~m^5>PIO2lHdQkbB0>zDa zAzmdJle+bN*fF(Ggss_XaBBe4Svk|-949=Vck5X-VqLw%hzFBr+|AD6dPpSTku^Xf zIgE4DGNXfLi#++X0nt@iZ;ZJ=Tp1q#9}8L-{C5D~0}V8&cdmcB_jjas?(j=N(+h{T z!LAW4-}iVj+89ab4w@>iXg}}d4UHy!$ia&mjwrcedDLD}b7$pqo;O`Zju`29w|`S8fU-kvj)mUd*X1L4lIOz{D+3 zR=?Lr`X*mw>SCjZNtGDYbU)wWjUOV~^q?a@Z#6%(LS$(&dv1sFEEL);MJAcq-DdD% zZCGYn=|=e{>LW)yL(@&lIW+yiOCO^HOLD*fZ9Ew{zO5E6aAeZjXAW99_F%?yMd0ar zjQkRy^BfqDN}+_zL*6IaFy%HSD6+&%6+i7A>Mtt>1}Fh&9aD+u<9xJKd1czU+QDMv zk}y3(?!8QLnp3Gtxct0vmp%g!qGT235A&eh)L2=1ml|YVAzaG?H!e`OB1l>pC|a3} z24%rt0UlZ}AbbM0?VmML96h}9i0nc03BSXBy9(ZB`MWZa)}}0IUGPWbGjcw1;KoIw?jI{>XIP1E#zg4zwy03^ zJv$Zc8WNN>F7TG0eIICp9c+OmnLm-rLx;9lRJ)_7ie878`eiW3U?`F5B6mz4!OQL> zC(|KNvE^z^S1e{=H9yQ*c|fcw=QvSb(V3ir^DtZ%5=Q11`pNnqXQ0+sh|Sj!h^*<3 z4R2yiyD}G#?mNN8@T7fZdP2r~NjXQ=3}Z3LoC`$~5^$a|js1}GGlE0t3PaT%Y?CHO z?$z2Fc3KpaXTe&}KlD@l0|dc&OO|pNU_ru5BDs&pR)ta3og&D zDR9bIdND0P2zy!*t7;L+^S(9}KGGiVDGdfIyo1cFV87Pt7jLSrAc z&L^HrnU?~XmwK%oIR*GSvm5c>K9UPL$?BlHw0{ zekJmwrkmWgrI8Q!a(7YpAAbHDE?-8hTJ8EX4Y=_$SE%{_J)Q|uq>bkH%>zkD7W+ea_nx{UtG9tf?J*x*@s)aBPp_J0+V_Z!fR`lq zg$)GP+k~cK72v3TiPD^+zhT*faY_ zW4-Z;J^vIW$GHXb21(Pk>TU>JWU?wuoGA1njfo+~BSb>(cFJY1(^5RhmY$n37tg(i zqyvXdCata5Zd5lHj3aSTE98tx8zUvb)VC^}Q)&ph&Cn3$1;S`*i#W<#i*x@;zm=>D zmIpXQaHF~X+z}Jt@96k`uVeul7AdFW#9n_N5y#7bLl9a4bjRkKSxmKd?m8BVgwt6Y z@C@oqF=81~G|Xdafe0Nij7b7DCq{PqIl+Cd1iDc0vvnDDJ|<@vmnD0@Jf$izN&guvCt+j1cA%62znEp5u_tfcdgnB`f3hiZ;M9<~#qnSbtI z8Ui=M(uE)$vV>j2^vSdxWo(EF{#4+NH!ryPB0tE!5)ERV)!#He+3sdK$>K6I z`?2VK$LS+l#K?JFhi=~!zzSD+xiU(Z$ow-04k+s9N7$jc0@~)C!wP&hlzC zq~0cqQ~iRG8c$iTk{nLRrs}j+$ROc(nfIO0Spv{WBq?!de;Z+E!MTFlE}6Sj5`9e*&!kKJ92q-kXx0HPQ_5)1QEmS`dM)AGDkSh zz;+0vKz+%lAk9XCM0aR|$3XW^7k`s(ju*ZoD~8;sbU1iWL{W~+l+!VjtKo4z8E6n^oI4`Qu%{<>9!-+5 zP3k~d(YjF3$f6Cv%pryla?cyeSEAAhxwJR`(-rT5FJ_kw(c7nubhz^ph?ewWLA{Se zu*=EQ8uiEc+MiUr$gYA#mZhpE)+qpE)+s|8W@pN9#+@#o6A)S;^Va#K7i1?#ANe z4OJvH?2%rX00IN~!$shLGLfGdXQ+awNeaxU0>?T*Et~;$4Y57h~yLB{kynHsD z?n_M;ok}#H-@Jl&1$MLRb~h3w2Kq_Op7H5C-+y;{Z$566zFtp$rktbp+WV%u61V3f zJ4`TC*I3=64adYYWjlbiLl{980vxh|a?phCn*tmpSObA%wFUsAh8~xHc>b58Im3={PQ~ znYzv0S$)vdKnfTMXJ0~`%SR@O)MPfCYoRw~aU=e+?(ZUYlY*x*V~bb;DPXU}@jGF| zny=*Li?nc_&C;Zi_>XOV>wqu+@>6BjF{;Qz8}5IBZcyzxwhBpsX~qJjUAu{gRfD7rgIph_L$ z;BU$@QfS#0v;e6+G}_$8R_?#wok!X8GG&E3$RgWjHa9O-4Cz%Z+U5O6#^rZa2v%Eq zpNsULsosuuw}1+Z1{jXuN}!C^s2C%R4jM9F4My9&mdQQF$fI@9c&TavGPGU7>t$27 zo?T=eSSQ_AUz?mp)^j?I zDNxuHZYQ+*De-Mb$)6z{LN4VD$7ONO6egPkT@SA%a@x~zL9OYri{*rA;L#gP`aL`> z+x8R>wHdpuB{$3L2if*CHpUW@yx30m@f`%_yERgHuA5a>kHsaeTSE2`vv|GObcDSR z^>ekR;t<_@B9>1m9X_VT-wBej_iGa;5<}s<5lZ$LXAdEJI9jdWzHmI=mb>eh<26IFcRE(cUGm*aZg$zVHZsx@+PH zSVCmHRm*D!Sm*r^yanN+zOfbMHLa}2S|8yD#C}E7>%SdYab;XYwK97+V^-)a67ZH% z8a_^ZJ0(n7Q$zUAM8_?WEk{AeV;9cJZFCh|MprjvD;=ke0Rw#jiT=3;!IACz>GTUa zjPDldPW2H|10!9N!D^RiG&fVj)JV?z)zWVK&CJD*TYqh;1j~W#Ayqjvz-{jjYS9Ou z0eoil&ONtX*XWZ%OwT@5|5{1+(TYx0fpBMq=n=A~1@TVk_Udn!AAJef{Q%*E00qaH z2@b#m>5l}V!@>NdNACIsLWk7lXijfug@>(>sp2oEdMokkGibLoe|Hp+p6)FWJGXqd z5#K#pp9uNW17NZ@BoG^3xegN~6T~hg;Nj!>PV>*17JPtBK=!OpS3SITSpXgbY3-yb zC~rTs1ydO9-PQXFzLJl#6AF!%*74X?EhXDDUHT635oKMV=8T6_x4dRX zs@hUTDThH5T|iT74O3Htbk^UFD@Ly$! z$$xr`{)0n;EQZ_-%ETNaB)=#;wVx4WlZDP4Ne-rf49Z+p5WfD1JUh?Cm6T|GMNYAm^BnMmNUsCdMIVWUJCjPal{8JMFB}>-n_pbF-zk)cgJRR}P?MPs_hM zC38A6=Zt-x;_#BTM&raF&LF+K;7 zz`kt5vC-nJ6YwbICI-|@MlM9_uZ9ZWmW31&ZVi-ps;G?%F@H_p0^jTOr3S8NnU z7-W?e_9^O-RGwvNc4Hu`zs-{ypifxrGNCh0%-KgZtdInHnJj(`2~fTMz%wXk=XWGC z<7Jv6g1R4_`uIb87qYr8kUYNcawBjJPN$o5WG&L?o?sjuNOdu&B|4(GC7A(z111#+ zE_$lKV@`5A|8D#He6>izS6`#kK{af#q(s#~Af)ncK5R8MS776qyu6Nk}> zvn|^WJdjGj2cUpw0+Ltp(X42R7#I>QQ%`3Pv6h=zFxeO-kY{+3)Ryt_+^jraq`1zU=2!Xp}>&T$+dX|t4S zymqzHK3|L`tslRow{V6Zf6A+XHW=>iyjWDJ)jJW{3L8l4VORgrV=+X+Q+p@>6GS?> zX|uHj3^!WA5u`U7q%Z4T1Kjb2E00U@T@Lz{O0>#Ae;uGDVu?_VTMuI%Wj$*P{Q;+$ zio}Ew*Mne?d2=*%1<`)~(7yHd;2uHRy2gew-jA??o~kQ)xeuh%Awa->QgxLdb35VH zI&)c6c|n|^s++e5AgYNu75w-mj#HLK$1e<&sRNNeh)`w{4K z7}|NPU*?q6f@-$o&5uXS`kvH6b6`1l6R4<5eRE~nlu#*#y6uANBn~j6Na+1iFxqtY zn{7^G!Offk3oSd>AJ6Wx5}Z72Qb&pzY}iu!$de$N^cljym`lZR>EP6p$^H!fm_QeF zhs9>)QxekT*fez#)-{hm66%wta!|_4Y|bPMkYxU>#wQ47DgO^??;IrAgZzumwB0>z z+qP}noVIPd+O}=mcK5V7ZQGva>%F^s@9(?s?u*@cQBe`~$2nDTPUXq`WacM7nH|^s zU3X-$*|(7XbOqHm>V+2c6s8m8Hn*xs`_4^M)BhK-0>pt*r&m3LctldiGH;)Wh$ zc7e$qJ^~?FnvsooV3PT2vY|at+&*%0LJ69*DwD)I>m`RKhah<@D54lf$1W#o*%YK6 zM#pjw3o^xf(BbgmjcoV=ZiQoRRsQyaX8HP;^LeL#a|O>N)5?6t3uu{Csq6;-=@H%x zhhU@*pXC5mZ9KRSjGn!2MEG~$6D{?r5`kUHbIUN?&JERFb07NVYj3P~Te2ln(>;xe zq(F9I?h5uR(U@q$C|%L-JX`^2CpNUWO)jZ>%Wp&E5+ZRd42(X>fW?Xx$q29mTehj~ zgY5Q%$f5Q~w>_2FwRI<)`X?y3^H2p34>2#9H?_v4EN!Hof(FuR6-A~|9WgtMrk(Rj z-Ohkj92LnFuA21^mWK?)V$8ITIE>Ez%J+aBsZ#T9{~e4@1|bkNmI~DbFbb1x2r%~Y z#DiJ9sK8nFU!_`&+}Pj+_Uczkw|3Z(!}^D!7VhkDqI>ki+yD5`kU!pp7nY%Jl6%0K zfXYN~;3wu3(bpi2F1``aM+Q29`+HyvPaU%-ilh_*C22f3qkIm{Da{cEW<`XuDK5GL zvMp=Jg?^j2`kdtGsqQHC5$rk0ED+U1m+!vD_Yw~waOI{5&5Cq zxqT58S@4UT32xJb_}3^*=Tp+v8u;Xd90MPPKcprwH0F_MN-9bcrB%bqM5!x`lr@y5M~f}Kw5@){|G+9T@OmkXXAXIA znvzb%5YdGQV5sDy$cb-T^v3Z4j0=wBT8-y3g))w~fzyWK4-Ciiy4>!-@T!=aVM<{_ zGmu!O64R%#nEaY|5Um421#w~kaRU|_02XyrW3KPiT2yIB+2NlU$nEm0LW227UV?7s^?guS0QNSf`9tR;dD zs~^_aUJH*}x*(gG(^987BuCNV#x-H@-}%$^xj(^zhvL{p3W}VqIO-GGRB}js3G`_< zi3b@5FW~4acP9Ex4U%{KP}duv zrxTtL7K{)}@1kGv*nn50p8G)3?UHOd_iv)Q6qnKw2n;h{Nv)ayJ35Fu!HUb%4|JGZ zG{T-0gDlS3)yEv-tjnFFlc!=%Lbj5ky25*W<(i&>;Lb3u?eHr>ZJ z%M9kJZ<9%MnePHnS+NM{gT)q&>XYo26mL1VIMU;alG*<3%tbjiQlaku>TFUmQ|gJj z>wL%2N-d@>C9b3{FhWV6?MPnqgsEs#$*;{P)~+Xf`R)%Ei9Dt=p|+j3-rpF{K^Ot4 z9EBmFkr8{Mo$uPgpsdZ@sG~+E&B5U-FN?;BBubm0yjxzJ>)&kU_i>;uVvNFvbOKer zOGe1U7BK7HmOLJFY#lH9eA341fuu6qj8z(a+3KIJS2`9F6RhDkKB>%_3lwG44w_O{ z)W)VE}r1 zEp(Mzq)*4J1er0K&aJjD-HEv%OOy?nbzvoYf{iC>XLEvwcRp%CdQR!Zpd}gIl)FPS z%QW=hZkXBq)3fK9MU8<_Yn7yPd7g0#=_DP*7*uU$}Mc z4e2;!8|{8z(c*UP?9O5}$LN+&y=>E*K1E>39&criR9xO`>k53*LOs2|OjkF$ZQ4bA z_7ip9Qz&waUlMg4bj$S!BdFnuTkhl0KdSNPFljWoYuoL0llHPvNk8YeQeplM-AmZ@uC!meiAIW>=fxbppS5MI;7H50-}itIBo>m!#12S9MnBYv&jHe_a|=aWc31 zdoip^O~Vyg1o?v!8fUc@mN*xm5Fr&tsv(Fa_M5*IOhg*LIH84UvB_Lv3769oF~}3B zy@0m8-i%%@9Ra799?ys?p4T<}Gx+`3Ue#%u6TH8~F2Rw}@ z3Ik4#L0JSGWL=S3sSe}^psP7y)~epxe%X$ttd9vvGk?2M-?*K6d7fr^hG$Iz`4Od3 zUV-fGNAo2~Uo8=45Bzi2enzk!Rh{JX1HDid(fg5XM%Lu2YDQTHtt$qruql;auSOIL zCFM0aC^sQ=i_K%b3JlyA1kNiqOUvWHgvS(apV6-V*hyqtHUMS~|A`55&j6YMDno9D z+$htBD$s#!h196yPioc@kQIQdR-2*!=jSLdNqG2d5uF+>MGv959)XKZgm zSvYd`-de(yP^={gxj$UUNA*8Tlw3pPP70vllyI(8Q#Y+>W>1{iQ_c%W&XH7UseW5C zQ4_Oc@G<}k*K?WpiMyzc&E6BK*~NcnVOLp)&UZn{%p)r3AQT4x%-C%|`ttjOft`3L zXONB+>`@`h-2tBU-HEeidy)CeyVz3|rGj`uO5v6UzQS@PZFhPSW{# z)IUmyEs4dqg~ZEe1R4r0FCA%(*yzz@;}njDAHW4~>Q1(JS9xoO2<7J-OxUe2L1b=WkEHklf7ctXUuJ{bSnlBs)R zK+u29+!XWcDi~?0_-5>(GN1*66E8O9R^&E19II8rahY~MS6_CU**SddtiEt_n_5?* z&b?%6IepoYdNfm+mh_Sc5DmVU|7&f8(q;zm@W4~j2SGhWqst4P~OIRsU-9M$)mG}RR;m9 z9xS6CIN2(*t|?ef5H?a9tqMIvG}Y2?6Q5jrD@R@LgP8GR5LYT^!2A0&;ez+~PJ#}v z`R$l)&iT`rKo0h;M1)-Q^M0ya^lRY;Sr+d}a^b1z;42kT36j!r)?hF<82L({1oxtt zf6L5MNlEX4C;)Z9fyjXZK0#H(*Z9ajO*L9uMnChTW|(mL;0DNEFLSV>ZnTNJdVUvH z^Nl`>FT?@JD(}s9$iS+A?Ac}4%<15YKeD_LsiPLJV^*>!Vzmyuq!z7fB}8BsZs!{e z_8@sWL$nZc1pFSR#OCmiw@|pZaM`zL+Bbijx3Gj~X!|!Ah@n;aj}pr}(oISye&`1g zPz9D4toJ$^*J~U33sA+gO5xI{qHEmE6z@VwYEgB(?SVA)7UHE6SHp+opWc9Jsandx zt3se9n|#3nP8V=J5J=@B3p|W=1gJ_(y2NQ=% z5ij_HY==l#BV(JDEp@_eF_ik(ro6N%AacR3L7Vh2q7_$nku!9?w{&G$i-(H3z9-}` z5LGfB3*x9t5?S*8A&M$7@*(h@4L4JynxF)Y&^5V_K!45Ta_U%-+P-eY-j@^;=YJY8 z|9KDo7sKM8-OMb7t*>q-!bfvYH8xpQ5@moNsW&;Vn*pXEIf269p!|Jap#)ybvP?(o zWqKD^{EJ#2v<&_Z{B;cDlnw*_?Y9jhQx_A{?ljxO<*PfXZ?K&PW+20#eKcy4rRo!g z`a(NWdm;Uj)%$X|5(V#%G8N62ehk=+(a#J|D9gZ~c4nYgEz-SUavm6l@v_@4#3L0YEK`mI)d8-!(mn)0E4B3P+Eobovx2WFQh^p zLDt(kU4hE?f^jO@ zI9*E!pmlFyna05b5!<(4++|;LXVwYbP~b*@oX@|@j9VioTd{@(W_rRKt2j>z3;402}z-pypQ<7(Y=n&*l4 zrslKT9==zglfn+)lI*U%;K)I=+AQsBt@ux387>WaY^CWmHmBvrs{NC_m>5rJK!E^+ zReQePVgkoTxewpE(<5OQIw~BJu z2P3=p>z~}nh8%HKKo?}0clmOff}lPR}~lExLbIf-e$B)-6{kmSFM35 z+k^KVHVUELe5Fy^RTzI|&6PJmW(8XicopPDRma4dO3Mj*z#(De{Ov8ZC>QyG5fk=; zz$up|0JV{o$S(v0b6ukW-|qT5kg@zlG=tF3fwWSCLnB)c71|L8k34!O-)EoHyzV=o z63bXX0TDbHDaRQkLF5Hc)uQoSZ9?bi{5%kjPY6j7{DV&DPc)a18~R5GW)sJkD6C&y zx<<&dGN(ZPmeIYvz``=Nl8!)G54-Jd%0DACNu@2$2OlcqQt&Ymf!4;ck{+?B+W+Kh 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

Ry3Hm1J>5_~Q&G9HJsx-`((%S*#Kfwb_rj#9& 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

+s5BMX$*tG4D$E3)sXH30 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%0DmxI`|n$;=dNjQz*&A_;d>dqmJh0A&Du z!c2wt`Vvnrs4nJ+DcZ@42wNBGn3={veSNT`n5pv5Vbmp(i$K7m7eq3`LT!rLSoj3^ z3x`ZhaSj>xLHq`}=&Ft`whAsTSXjYeY-bfp)ZclxwyC&)_26WxgTT{EB^U3X^5!N9 z9y6@_AzXT(;^OJ2o&NcqrIGNSon!Chp8lOFUvk2hezK`%?hs1~DU7SG4!Z9e_60p0E%FCRye4 zAzqt8Hmr)^jwzj7;s#!5545|9q4o?%+F0D)k~+r8l~{l?H`+Y%9k4EdRFMHr{4u$h zcpfr?N@A8x34Lp?3Jz%l@zemp;+ZGp!p(#-d`w6yj2t~N9{5TmEa4R|HfF^Gv_E7Q z+Y#{61?0ldp!_{aI)8eqe3uyB3btVcJR-%(S3xcSAr~aFu1woKlA;1Gw;N_dGXy{O zFUW<%ljPrwg$M7ue;Os`6BF0&cO3!q3BaX{h(T<6N$wzKBbzeX6K#$5z~JNmJ7Y0l z)~=)CbzcCt0fGVKrTDK`-F%Sa5SC@i-9oFsfb(un&XqkyDr98*;HB4rpTz z|9y7SeC|GDU4q^LX9AGCsp0d6YH1)BVP45-CZZ#DkmiG)pqV_M3H~9;IzN$PZy^`- zpZU(14l%2l`WpRl8A$IHOwAey{`kL=3q^uIVzAmbf81<`b&Vp=d=6uJ$%Vx;8d9P4 z_5SQN1`r3DVERYKg8Hfd0cy@Zi7d?XwuU>~!vZE^H^c*kK!Z?A$XvA{-H>n$9SD6H z904IN3RpufMHSYQi?hH+h^g!3m64qNAU`2cnL2_gVRmxCh);<;I|#FACKf8o4Yqj? zY!jJg404f+O4ckzjO~n-hOL_*yhOtw^bl;H#TJYJ(+F>55Gqw88wTs*`OkO>D{i<=KVAr& z;~J#P3JArCXpoCTNQ&DD!@W#+lJXM#@hhY_iU_!;I^@F5$PS7%e|IKk?S?R(IX1mA z6=JzKs0u2cc&~5z9_kqXuD{J)?Q<8lkL=eC<53JeJzL;DN?cHsvu@A)9LN}jFiL>OQ66BId zDq{X^-)ny0l5Ro0^LrS%czEp1O&3J`LLiBguLTs?3VwlH%lTdSAK+%wV;*fR_K=q= zsepGs-)ZA@u+VeBstLkEOD_KJxXKtCC$u}%uVEj}8SA0|UHz~PO{9o@HuGS7D%dYB z`#{FrhIn#^@GsiVR?oEHs%_l9!~AvDmXjhpeZZ~^SQ4@o;9UZ_Sh$6;f(zQo2R6D0 zqx{+u7d60$LVbXJI%NHy?K-(A|2UfQ48lMUE@FhLA-eg4DPyVPjX=i209{< zr%xg*MG){pA@U%@`Rd>Q7rZ|qh{&YHl5myo0O)lCBtJ+luTxNvnHJ+aA`k8#s_tlK zA{!EwnJSx)Y(}9HEaxwt(JRQsBj727EhllbO_mYF839WTRfKzp3y}*p-#vI{m?PeO zQvXUmi5DW*uc=WnepvycG^1gC#86 zO(T3`C_wuNi)7@4Wv4(c8X-RMzFRC6X)vLn&x4@g0bYX?^?OZnhcGMFxw@;0H^d1W zPa`kbFCY=`Kc!5TtALXGz;lrKNqFsz-w_m9~}F36nx2t%azGL;sA z^kTs{kX>M_>`6k*w9w3Ufe{fAZ&}TbL`Z*aIcCp1|MT*-jwDIXbk7hTu8#vlV+#yd z0){L0k05-SgslY<&Wmg(A{HC&^;;CRfgO!Pm`67J@cEKKey*X2@ZbY-Q{N=Oz!ku5 zlo5kBgT}(e20trnVqJ@b5u-0$JOG&$Y7(3fx&2^zVsS(sWiMxEA44yfnGJiw2?r-)DrY;}+eloZ{hDSJsj!~754B8WNZQ zMt|Y(mlQ;L+ra(*yl}m3n&~7#9RKv0<8=~t3=T2DT!|6@Q6amA&Mr!8#%X56Wdwb` z&^P3OYcX>o=KmoF6~D*;R4Z5<(%N`37aR5riYge}UkoDiRhB_hz*%6$2SOvVp(`hQ zF^m~@{?J!*$9lOEHY}qw#`DjB4LwjZQeqL%XF?*-fb@w;j7X4IW6NKP>zs*zX$rZt zx}U!o)T~}G?r`iCp&i0ZQMM=!1p`w~AUmXPeixI4gs&SSRg|jLmpaP>d*oU7=23?2EGLlvFE)(x6xF<$4THr;cwiU z`TOO&#W3(hMy^q>eY>$2caULdfrfUr{s%ZjY$IaZKPVe3q5*1n0Tfrq{|dE}u%fVz z9N%LB%6WmJRzo8O>G-q2wu$(Ua$v+dHo)Kl3?+o3CciF*K~hn~=7y68mlfOtz@6Z* z$a(W{4_Sctahn)Oy8fFEJ_x5`knSK8&?Ehef#B08te0AE5vf=(?$=Q4LsIV>T8#RP zUS&F z8y*EkDqMWt1M)&9wrO;f|-wVXgOL(m_GjQ0Fsy(AobcKI@Ma?EIv)snxz&KT}I zp9Qdh#P|l+YLLK~8UDPpP(&ukpuH?xOhC!2A&6)og2;&2;uxyX@2ZJ)beYSAd1jz- zZLk>kSxyOS##Xy`GZsXb$2Wf!`(Ou2*!c?%!sP9&xwlS0loDl{-+bx)6{=BKVok?6Y9f#K z!F(P9yX1n1AqyHVPI|_V3CrS4YGjr%m`??m53*UJ)_!rq3(bd!m8z&(@AUu}LkO;5 zF?cWEgNwru^I=ts?Kd$XY#*SE6!>=M#i@Gem@8t?L{7*aUZzU397sQaH?q5xxL7z!d zpPfJ4p-COTu7Xz!$u{KWUjgGrEeHi}UlylKtdCJ$Jd+|7h^_=HhdLhKtKjS6ROcoP z;x#7}brd!3W&+$^Ha{MD6e{N%NdVmRiaQ%fSkBk?ZOWF0?B*CubjTFJx`!m$89C3+ z){9wncFKx8*pWghfW(nua`-z*jDI^~#E#(iJ?wvjyD9+}fsx+t_JbtpIf>6CQamKO z2$K);g<*ia2zWnILR3)gT+DxFJNOop(E08rQ8B={yTB$(CKtyb*0$rHc3XP`2&WC+ z$TkbfUnHsGdFQ{8^C+vRDwt>+6Skw4(_SAt07oD48st2lIZYC4#*P-w<3wsCHr(kV zX<+?S0EOJV=mmdXOc^}-X!k=3B>Hi4^La$+fpj^%kx}RC5|ZTc_7Vxp=kC4}6g)r` zS0ookxUT{wNy;&sJWxe0f!8vSq)KQ$C$@6r7&q&MiVbQ7%w5O~d@Bn{kQvbs z7fH&nS`Hl)r5kzY58iCu;&c~ig0Lx_EMeI34$P<%7(n{K6JC-a8z{j39EGI1!IO3+9;}r3EMd zCm&;cuYq7sf%$39k;HxICzqtAvKC&%oC*Gh7*{8_#9;mmLxh!o)`*kG4`rhAK(cbk z-DlyhPw;o|JC9&pRlaKe=>mB;$wtUQLCyr|Dq-Pr&6%j_VTAEib;mmEpk4p5iKk?| zG;2$Wqk%a@2XWf@Efcm{{{i2a)N>S@t#~KY*rylI0PgDcJu4iZmu-^OB9y3groK8eec4RRr2r_y3AEt5!WP zPggHbBTu-f$(gWNr?Lto3qYi2VF8VddcjBj3+8N``)5#p6-#U#W;-NTDh{~E0apdV zYkI(c!No~-mRAjzMOeKlrKwa4inM?!8d>a6JM~{7i7$*d8guAo07JM$5*dG-Ldir? z^zpp(hS%0iZjd|)4etPuYfV#Wc987AkAyj$mC0Yn4kcBJG& z!pOuyOpSz1aqQU7&}fj{OL!x5Kj&C7QShf+!XS$3k%!&_0GuY8x3g~&M<$4d-mF9Y z6&*1X-*cd@HUr!_4gxk(YNsxe3H29Zeh&OS;q>CKVEeV|PwIktZ!LDY_}7+Y1A zQ%1Nn`q}>plxPsil&8AA9+IN9kQ5>J=J!_pH^e!oCM-{*8_mBQz%npk8CnR|YF_;} zAUrE1Cnz8FJZ}nBUsM$J{GgoJ@ZW%uio|1+$lSPbj!oR$IPifa8r*T?xnMS3 zu3AnB;YWB*8n_Rf(6UV;w$4c7@A&Z~jkn&nrQ)vZ0!msyAGrbYpq)J0bEC%tAWM~B zJ4ga}fy@`EGX9BB{0A^Y32<-ZaQQkHrv_~{?nW3lFWke6h^$uqR-F#RoeDS(Ajq8w zqp$xFfSAEh8tJ4j1yu$E1afD>u6uC^BOkcR)mh&i3kOg;*SN^59OtX%O9y3loY7d;m_B0|?07A$PdqjD%6n=oOb8y4VvL{M)Nr zzt)0HrhpzqaArRDavoirw5pfOOwo%-GvMpm6sb)h8A%A&$TDp3kHv|D+`)7_h_52` zmNbEk+k;2L76Y9I zoJdILt3*b7IGRNSt0A16TBOv4z{7ahJf(+yTp@i1u)_S@^w*69#wb zIAP<>3(BXn1SVgCX(3m~&Xmg*mEl4#Vve{kE3c>&SkqfzXWT0m$N6h#M1r<6M?l#M zV8RdV3~2~ERxVEbkG2TQr&U;;|=hSk}>!ZS`ur)d@^t&eteCrtMTQ>eE**zQUg z@&O#VxfeIIi7jTk3+1kk^)zzDdh-1Fd*Kg|w1TlH=Lp<_zixqc+Tc3O#YXd2J4AYD zlb7up$F;jZgKS=Dj!oPLN~Xn$FXV(r4dY3&3aW=XAGiqsi@+q1<{-ejIN)qt*iKk& zzt$g4g1dN7aMpPq>DYDTk|wgMW4*g+@+k;K7)Yxk#Kg+BIO&CAnmI9v>kGnodlK`? z1FY9$_kddbpqQ$Rz}(6Hf5jv$z#D1?mJ@)u5rQ6az8TuIIHHl4wa3hCMws~6ZJ*{! zV8eHT8Smm-ocLceCZc-v6t>s6JBvoZvXGT3U0A0s<|9UM2zxg7|9iHHFbm=;0mdQ_ zZ^|HlhAt<(m!aU|C=0FNZ(LCGf~iyyrV8AVT8yeA#>sZ}^grQY3<=qIp9W^{feRVn z4FZm@iTp3aKo}$bKM2ED4ObZAW(!I(c{kh zASYp(?TaHVls7(aJW8;o;EBQiiZHv8TQ4Q!@}y{(43LW^Pw~YEF`GdW&mNz>jA#)A z7JtASSu*aBSe!cUDj(PZgN=9G;sz2)!YqWWPv(X#CX^PeS&=N*>|PvX?rLTtS!y;r zHRci61MZ?*WJKQ}w>b6Lb`~PJgSE8ddNq*!R!D%5rp771IO&B3K>U`@$Xa)2+)_su zTw4L*Q$0Z=B+{- z_bpC;=F(4$E$(3g#5)KLUAj9}A%Voeb~|$FSZ}d7#6o!yW@LKv?-pusK{~K6i!l219(c<*B<*gMJ+fI)yKirf3BX-MN9bNU^8Apmn;Pmxk;#rs; z{Lu$tuUKPRsVTa9naD8~~5U;cl%D#1V zae#$7`5UVsb^lXNA+jhoJNlE(3qTVVOodC576<<4?S$Fta{IO<9N2jRZ{)JR6uK7{ zQ<5^q31bhtj4t*nZeD07{JDxq5sr6Gbhvuz%~X;YHn_XFoM!GwUr>viemZeK0f-gB z8@VIWPd?F^L}d07_66rEh%m4P!9dqdk_^D5X1M2#fqWK>-{TbB5bl0u+;z^#oj9Re znd5DT3#seZ1){*GrbWSxqR8Rp&{0(sGbHm4dI1i7JA)#Zu;s0%v{B!h5AB%PR zd-(#9G~(&$^%>8>W-mcp(LkU|q?3!P>f~te;Q4pMmaP5T%UxJ^+*Y6`Fe{IMwSP!1 z>>tiRZGU2p2eGP{tlROB96+&z$bb}WT`sw3NG5e)RRWKaB6+at&fTTVpMl5k5LuC; z?RiWtDhY*(i4u+#TaEG=v;Y<=Nb{K8mH+<%mdK&zq3Q1~(;!~H1q%IINH*%6AI>S1 zNP#v=Yn2%8*bdYi=kcgqLN?l;LqY_6g$Qc&S_&39K(vKmfHc~^QnFDO@N4N9w=S1+ya(E+! ziLE0Sb)HX>ICD2}N?S@Acnk*hA=i?JUXhFTC*BZ$l!7s8=eY^cSTn38Re`O4uc_B$ zgAlb!!pghyIcY5$VB$9Ck)dtRyZ<{Tv9(A1F7r$(5GQWsEdP&9`1GNPY}AFGKxCye z`M%X_5PWWgX@2{S!Y6XkaA$)TdOBg5u3Llg!bMOh@XwQJWEZ)3BpiZBx!L_hoZ}9d ziG!i0iA*&VyU9hJ>kKe*LA!c5V2NbO1J@n+qd-|N;EfC|uLjA5nw{=p@>Rxq5j}!t z-<=?^1ZD-eD0N=Gp9gn(keAay-fai>1`$@3-N?gTx}Yl9)0n3!I+%9J10bQQU4&6} z`pw7TC^{+_)TM|p|B_L1QPnW;Fh55dvcyQIYkdw^Qp6p`S4QBOLQR@XE*H4lf?OJJ zZk^M3sVrBUgoQC&@)5+uxLKzb(UEkXph#zmygDi(D`zS1%J>My7`LvWG@O zB8K1nGsEFKu+R2o1<))A;#$wgJd!f|?H?Y)#=&+&AD$GrsrWOdbI9k~GW zMM#*HpI&z?T!0nW;hVQYPGln&X}<9jJ2iTEosj$)h+D5AZXs9W4jlh87LiGNRg_Y6 zGDy!B(pNQvt7&nOi$%ir2@7+pZPJT6VD8AO`TlLpOD^aTlq!<{H=g5ZTT z!xO^f!u>IR!u&9r8h2a-UQTh&?{e6)ja&dEKZJ2EF;Q!&ff3>ks35&qL!Mk55?(Ap z7mWHL`K{_T*w&0W=u95ekM+0)ds!kyu)A|oKP-CuG%I{$iGC<7)l_BM56hb^Z9)$a_^*Xsg3j|5RK)wo!iT42Y00<092)wu( zvIVMf>onkY&~W}9T7^K5tK|9p=1kBrnPgz9$64Q4`g7% zak~e&Oc2?M9#1YwQ?xseu{QjP$39+`v`Ttn4=0=r_T-v>KBDIdY4+y^4NpiXOq+7e z@N^Uyi}2R@v`t@-O`FKL2QwKjx4U}JS%+C<@dda5l&I4vM zbHxc^0by7ymcXN4P)#sJ@goFOF-95$cYq4(MWh%dxMY=8J0$LDQ1(J@>PtfYfr zg3Cn8ASFbGNz0{+ke^{;=7ugq-~~(M27}MCgP$)!LHsn^eCt_5O&UM}*KrH?Ud~?l zPgt>UO>$f6agqV+M>^Ly3u)?jmqPu0R0Cm%zU{eJB*AX>L284P(1x|7At0}?b;Lbi z)(F}+h-Vkqo8S1t(`->vV4%qC;wIZ701gcq3 zK|K~8Ne<1JUY=g=L@N3Fi)->P01u*IO2~FOJaNC^%AH{aqX{dlHo_E?^S zA4ZRBniWJi@pdlK^yd^oSi>fr;^*K=3#bP017!r^6mHUlXWbb8G3|eC>F?i!5$|7^ zE{X%Bad-Gbdce`9zj9g3Vl`--1k$qQFM{H^kC+jPoBMxI~U@ zqJ|jgA%rOudX~_(GuMj1M0AK}?h|3H8>prKS^=4iKfo)^@q_ztNbSEv&mgM8IV+s1 z2m|I|F6wA;fj?@@w}IDMWCQ9uprP@MFo51zN$FFtVK?B)3c(T9g=_#K!o;z~GwVb% zv%EUi$(H1ni`YTwdQ}ju9ay|JVlX#d|F46==@}<*!eUs~dGYZj5Q7v1A|$ez_x}}{ zw1&S4a8yCV1d0t-CMpP?_xh5J48>vRnOjuAg#R*X>WDc)m|3Mx!!g`(AZ=h)4*_`* z`ilwGT5x$3ZXJl{l|*!|i*>OhY0;MN-E!XnFf6&|6)3WT$ObpUIHO%1u} z>We6B!JyVc?kI`ivhy_AK(nsHV?g}Tsa`QZZE@FliB5}{y$*9cT1WO(zi}@wrAD79-!Q+pxNp=V1tg!}V zoq(M#WZ8!Mogs(V|vtrO?xw(dRZwpwSc z;`n~|4GHf_-f^Cj`L*>5gun6X}-3YHS zuep(^fZ0_bj7{rY6wvl^+XFi>W00&OZTsYFC>!Sf3b$5wd* z4{kgKtCi;R%b8wRak5C06|s$~&9{>4s6vS)pCjhvykd$*!Eq4%7k^@`>D65aBaSJ} z-KI`uo^FQza6gl#B>~)1j~kH2IfWFs_uSfm)`>}PS z+sK(R_7Z+t$0H1dUE!dW-Tv^QrpFpqxzB zm>9VR-tf28eRqK)wtz*`a@=k%D9V(BGs$f;`Ec9k>`qD4DSO;NwOSHOmR4nog4oTjOr`A+3M^* zS`p0mDCC&jDqnC3N(m(5z-hlY1f*N=(Lf^QUvF@MP}K^KW0$YAVh?+*HFkqy3qpk5 z31s4(&xZ73;KC>bdsbHF==ovQHX%T51M{`XNCNM3jl|D1P{D0}np3^=L68lCqtXs@ z@DncbRJI~RwHl9NlsGkD@ceAx>vznU=(^dEr(EEfLX=ix(MC^OF#lW(6OAXrJNcFi zfQvgTry|xl)EZi+QyKQ&+f?*3+L4jne*bwRQ>}Gs7-Qp@P|SdjAG1)^Ft1hg;#4CXPwpF0g22v&V$-YS7zQ?4XE0IO3 z++4WSKm>afOCi%hmAyP}_VGVn=Fw>Gqk~7Qp2GRu9fyL~PhS72I4e3+Sm-8f>r1); zXVtYmqzduP`ga_jaH{a=O9>bY?mN(QCS&PY#_h2Pau<1mm-V-N*;yvb-h!8a&I%Tq zWBkf-jY6Veu?s}n_TDu6CzOR=e?Yb;j1Mo*1xamB5H+64&+sQbJHTvrUl_s06-dw0s95Yxeuk4q+hI5b2f1($_cL1D*n4DF( zz-=rGGpdXbBm7nlF-MM%D7Or_?2sp&TAr@v60UfLkzvou$RmTzt*-$&0LZkdeA~w* zWJENj`fOY8MBRQEQA-#y&1GNL;lhzptKh+Q50abRl)PKIt^uX)ja-iX?4Y{;BWP*o z-L-P@=>d=}PTXlChyxY>#%*9%0@k7`>HBv8nBC;siVQ&0*o^`3iF;?ecSG?yfMzDn zEl|*MP22#rhOEqHg|w*4z8<#Y14R7}$*L3Kw0ldpL9G#kJ4uZ>`{(nK7{fA*p*I=B zaeueRAg++bH6%yUY;nt9$I=zs2@^!egF65@`c)F?N=*-gvqy{$2i{i5fhs>&Fc+qj zn_KlDAGLS=dahbB0K*W&D5HBr+y=F$3~7T~|9sC6s+w0zyk2~$Y~4L#BN*|QYLJx_odkys{7J10CU{oSlRU>o;tag z+mHgQ(s%=(mCFAUMA!&y(rBrVcNISm#~d z75f4lP6UTktd`MSNP_hIwTW6CUd&)S-}QLi!oCAu8Z>?L1oLr##sZX<(MM9aK<&1o z+yhu-`P&;0Q)q?YpWdoE-fi$VZW;&IZQpvSCk=mMLhmhxx{7m^f@nGsOhQY@Q0 z{KnR<5+GQr!Tr;?Kr{2TvZ?I%yBNu?RK%_44mamNp3ViN%$>D!CLiCAZCiZx8Q`&i zn@ThqS+(9q{AEqp+g^Fk@O~9$MlCv^7hrm{6Jb9X@;y%xSRQzR+~$=ilXSV2aqy zbYAhIyD%t0{K_s}@97_=Tu{R(9I2y0ImhbH8<$$t8ddbulG6}_8BRrm^3Zi$$Ra%} z^B5A)py?gR9tUC^;^VUQTu8R?&oZh~^zT37{M17LvcRwcGc}A~cQ$eXQ8%UzEN#m^ zU0%;->+JO)MH~SVee!K}1JY_&B9oG-y(-oJJ?lw;^#LqBJ5gsF7pyI1D{F3TzCmU0 zA~+$+YFdhTL(6_U7mQ7FR;{sp;ljQkX<3eG^1NCNFtmu-3PGE*iwlja&niovSh#IQ z^AbY=nvCGkodDO~<2JBO!kMdL9O0nPvY{SqK`g~7G*;TjH4>Y^fu13A{8AQs9lt6! zVt{!Tm^1{P#hG!ANsA5B8W1vIt$$EPPK!yaau&#J8IEo{T6~xbnJPRp=6YrRR5tKz zeIiqLK;!HU8=8t99d&mEcE!v1?z6CJkEKIKuH)`viej-`p;5kHkxPaAK+QBa~LAa0hAvBWHk9tyE_`2;H9HW+0}kmJ44NED>_~L+5!iwnj{wL`w5RTj8 zWgZW>bvZGGU0$#h$(=^{@z^n{R90l{c^91{@XvSxOZm=?UYqB((PAgfS=9cOGwlPv z&STHKI-<1Um2N-h(JI90E1*o3F{}&^RkD#^8e)s4(yc8RW(;&*lMH-F6&?u1Cr+Au zQ*`4K5WWLrqM9CFRfV{trey+s@#3pOUm%aqD&@EvbZ2#C1UO&D{W+r3qtY@;@oMpC zJIpB({_jI%^hG0O5SuySepZ>YE}>i6WO)giiy$T{qMS`tXlIH@@g=~Ll_qWa8VWcB zXP`Idv~Q-25MVNLoWe@*dfulQq=Sbt9L~~gZmCS)DjkQjG%|`_x#-Buo(LMA2pV*U zaj?G%itxUY3+kTIc6igQ5Y|)kZLf@?i`5#9 z`grFPH2J!Pf7ps}ksr=Lmks9glC;hfF>SMANc+Q_A>02ugz73B)s>%p=5oY}z@+Iln$!%SwGo6zc~Y@XK;7PcBnp<(b`Z%(`nek@)xuWRfHF8TLq z%uZh82oSAKd+9kLB5@6d1xGP5pcD?^7}J_HtqOkFx&*c5$qAfbBAK-^o01dda&3n` z$4jgMOem@~%2(k;PADs1xRn>Gioiu$@_HIrXIj7(M4CIWLCRO@WKJ+AzQo=mI_+`5 zb7)#In8OU{t2;44pFx~Zkp^6EX496f6!Ri~Yps@S|4rH%dRA+H?=X}(*2y?`jo=)K z-8g056InguRnsSsX*~Rz%JtG{P6)xTGuTmb?Q5rwjaXa~Atc3dxue`04jD2!b;L4#|tm7D+^Y&4xcc6&kvjb;wE_tF!P*-=j^m18F+*5Q~3uo)eAOw#}-k#bf4a`w!B? zcv0!y=)TY$+a5=|zm{v9!vKaVw}rfaJ6#_T?4mXr^6O#aC6@wib}0AS#dqs>9Gi?3 zUjTN3s$KNiaf=gz&al)irt$Tk;$MTSzhN~r8n1cA32A-wnC<2QhKhAAoyOncsPLWnHEn#g1lbl`<~Ak@f#Qk&gQr(6_SOMc*fPS85)vH|0ALf z)vgGGTMH%8I2eIbP+V?gyTgzKY|bv(`!cqPh==N&Zr5N&DvsgREo%kDffPh$G}%$Z*h25=m~ zfJV~OMK~d-5lBld`_vP=**)y-urG}IXrtnsFe1%ZeYB1=RQ;nrr2me|xKC-vBSmSY zIH7{#wMo3wg6VlTJtzbnjf9T6k&tkuGAEpLNFaYJbVQYNVuiQOYO;CPV-!ww6HZJ` z&VdNIgLy%Q)kq6?*zJ6CSrf7%LOR`y(!Vh$ka7j@`!@UHgAnB!xHIh)gtg=ZP-I}# z$}`(jLH`53)U9HCIq9jjO{0c_ET;HKW!XttheTkz0$GLm&L%l&FL`|T?sy;M2tD$4%S>kQ>suoy0Eqy~ z&c8jk_UT=O@ZyCXCo~U7aH2&d#vAZvz0%ZrM(yT}zk#XY86XF9^BT{Il3@cDFPX=w z&dqzP^{C&x-CxE$av^CG>!cQu$o0MMEf8D3uHHkwR0So!+oh24;10{n+!Y1g@pCAK?J*QmsAyE|Wo zuLhNH#2UKAZ7i8$sLGsEXMck1vLkxa(tE=$PAqHbp_J@$d9;4Z9?;#6FE#dgyE#GF zEX6lD$>bkzvgp~pYtlir_qC(m&4I#pI5!|B@pca<5;C`PkZY2zZFm~=*FZ(IRGpZ{ ziNRYto+_B_%6qW$Gb}GN0x`a}Vp|B;#HgKNptPv|c)8|sO|WqRl^QJ#J{{tuZI9J5 z#W6=WtvslBtBjv?yA zAnMSiumM*%p`a67DlWU&_NY_P8diF;ha_qfBc6GK6Co_QcSLe;X~n11K2*{dM#WC) z(%$B+o1EkYg(!PT%C#fQ*}Zjfn9xyA9C(`(MWhjFPI}DClArAb+>4Y$i|8*NaAF9m zFvXh!aXpnzW?^S-wc=4508s;zM(Xg3A92FiX7o0h$)&UkS*oNw#@b<`(;V%$e>kDU zosf`(M48u~=DbV$_#C#yLNev)^;?&K7)Nal7Q7}ccfHr8uX2YS0Z++aJEk+=bZ#hX z)oEYkk;C#jc&=Z8%2?&a{xv!o>61MVU%+%&Ad|MdCDY&CW?hj2$N@BXo~*%%V-KD( z@5-j$yIqdm!16mRkItg&*5-s@#@$Z#{>rJ>s+MEN@HUioEY>d8R zVF)croK6YlL~<}TuSipz-YAWhd|o@bAnJe^%o@59wBQcBXzFW;LUTuP4|?+(d;76D zcn4I%zGNJyqd3PQ3hb&L{bb?VS#c0DySI(nP2FCca6+~cVM@z)!HkO)9(W+9hdKhi z(Q*QbFoX`IG*IO~rWdUXZg2y+!&IW-FixP%MuyEoWu`6dCk8g^0i4`L9mD*gk#1v2 zvvuRgClSd|RCcgN{chw0UMS%puSkm0NZ0es>{k#T{uD?w?T1d}L=rE8$>ri{h2LFG zMGLcHdBI2c5|UpWCI1-?YE(yU$A|K|M#SPKGc zG#ejV%86vHn&c;zDGfIl)Bxf!zAcILDlB&iB7+&_lIhQHeOIpmGYe2a`w_HJD>%{Y z?; zij1Uf92K<#h@?w}x zSiaVqRnkM6Pi*8#=qLqsI-RP`zQ&1^(MFT-R+%n-S-UN;F8-dI(l|PYhx=rRc@bit z)QKhIT~DWl0FCcAy=$Y8=?!P6XAAvrsSZz|XnVZC--Ow|UU;H3aG+j?W~n(sZ|-l< z$@EH#j9VW11gM7)VrUq$xA!_uO}mpmj`!v(bM5JxU$drFeJFL;FPSL zRXkvCQn?0o}i7LyILlCWf}X;98_w^W`Fd%JgpMx4Ee) z0rz}?uuT^{{wlzSBvO*i9%Wv&t)I~U8y2cRVxL<_yH~LvoeT3}(JgLxjiz@z9!f;2 z2+IgYm@#Pw2~~u&V5{${vU@sMWrD>7Q1lt%!8ZhFEut}EGUGZO0TzdxqxCYa_}u;% zAHBhJIxo_1D-pCtGsKe*JVF#a6xle#p=>HXuftw5CT2losipcm@> z4?yK+RUO9valbSm%VAc~oxt92zy~X3S6+4ji2kceXlSg@RXP|J&ejcdWF*5I^Nz&P zajDALShQy0NoNc6lJQ5`Hzq2Mx<0KM284HI9nOQbYQ{Stdt9_BUoi#fy3BiD0A*ig zR2A)s2+p?PMN}36J6tYpe_7UCzug9g@D`3wLs?h=AGT8{lWBtF81eMyPT=xua7h0AE2GU zYhC%UoD4?VGHYF0vAApnjpkaoqbwJ9=L1s71=6rpl9Eb4LHa4@?O3W*9mxlqX{OQ$ zxt?E1s0U5?BEV3WGyKSh;H)iaq_}@m_f>+PUO3sso9XT%$=Tq&lGmmmKoDt(3`&F8 zV6^X~7ReoJA{BKhoRLainz)@kG#>_D=~C^gxqR4|!^qc`9*u51stwY|;MKk)ri9Gr zBTp3;uE$;^J={%NYs(%KXp|jr{s?0c1n$y8J~%Z=2BS@>PzW8~Ie$@Q4B&6@OH~=V zm~Q~;lOehA61#9{@KP|{0P3XIef_nR4_j1v*vk>FG@NJMfsc6cSn~;6w7LXHZgy+ zK@}{<%_)TcOTIeyTwr-x}oQT{HQR|5NtzA3ILa+CF;r zyhm;3=4dtw9uMAlzl`b>0jEiU8qCGCjJ>FZsVMl|aaOi6ktKOg_a+^r>HCPfjd zeC>qT50f&XaDiS%6BO-%frB!vLUE>)FJ|F}s~~;@lA>Yd;Z?e7E0Yv>c()1uSH>wCr6| z5J5#B-9IjZNCOPL+PTyfkZlF4sU#wgtv0We0g{c9lXEue5`8SC*wK_9F>=~AE%mPw zQFPw?GCFU;T1za7Td8u3A-tRvv}Y+0JP_t7!h`=@M3CrUGK4>0)-eu&<@|-piXz-h zcM(D2ct1lpUTV?(TF`5BImZtAnolkw_y@O>R__mG%1mUJ(nUcNbk0^ACdFJf!vP(P zR6-N&{es&mxI^r@(4lOnO$yLxo8)N$7m?VOFo`(^li8!__s`f>r`wjciR%zRfS=8j>{q3PGnPF~46AZm12^ZQ>ezxlRtVfh|YYPd)<__J|}!$=2Kr z6dC{Tvr@ZxJp8{XG6zMe?&}7MObiIG(Qn2Q%-gzS3PTHnT!C(&$i{*DyY;Dgz|9bF z<4?l;SDoF!v0eJf)Ff>R0|w`wz)WEVOu?4`CUtWIkOiN%3eo3ugIw|b5PhZtl6q^q z9&W%XM4 z^k|qOUDK^&a2-wBFm`;~D#---rNet^4B9xt1q{hGP8JQ(T}g zEQHHdAA6q;xyY)Is>lr#!*{I97&7ta!mTg5jDeh9LQWK;$pn`%WTH>h8{e`uQGqN# z1wvz7;YluI$i|*MMcSN?hP3q12<`IEn&UExN)^&JDxzUL;=n}U(RkWRg2#pbrVS@E`;YvjhukEPyzXe^7P{gH<4tb!|8;o+nOOdM3#571X(a6EJ>DE=+w8iql(@l$F`UBvBZpWQc)pZcr z&fbUP^Y!E4pL4%(gt=GGb(n~-C~1*3taxVWTudI4U|HB7Ci?9E#$_03h~!HCOE!RT zE@*{Tk(&ZshLF6jfz|a_&ipp(|F8vTJk&yceMg`hD6)EaQV9cP{LsAmK}{2ZHz=0u9oG4 zu+jvw!lyW)+en*Va?ga;l0bU%j#RLTSoGH(w#dlJu(qZ|K*{}hp@dfjki_)nM6+Hp1u&#Vua@5o2SZ() z=Eh$Qb_uDIHa=Ox{uoA~Wz%QmKrGO~Y-osHGt4y%z1a|BkkikDJ#XHRK_++%3!vL` z@{Hq!2{LP?dfAMN3d>TnuqPgVfuv|QyD^oQysA3PUOzu?0>zgr;uy|OPv-@R5H1(* zq$sO41A>ag!vzN;)KG7U{)HPS0nZRfHg=e}3NAP2&I?fu$9fy}mh$tt5lqQuonBRF zjXSTXaTl8D20Nyj`F@FOPzpv|c-r72b0M)8kQlA`_AlcGN$!n{v6CckZ(=fQQQ~fZUmS5Rs==0UU;>Uod{!f3+9 zE(Vhk>;AYmUM?^N-gPAM#Xnwau_yzSeD$mZJ5;<6>YxtbahwAH zmqtVw2I~X+BZO-uH>@*#e>n8}w4?B{jR2xt96zlN5@gWFM=Nu2{o|Hd`M=g^o}loh z9`^8zIzTISCkY$9qQYmNOe&s=*qR-^Y^w0KkJT|kjFu#GvW{t75heND(@3@leG`&i zAaS&YngX%~m(9^yM}&tM*%fa#R&bicf=d&V41%pW*N#j(bqwtoz=({^|zEw&>Hs)Jw_r;YEVH5=I6&Juw;zT_Nc`O4H&?3QgD z4bW64@~i2gj?+$`6l;o>=HOG|e2Mu$`6xJ`dG9ArUdqZ=JY#IN4%ZP*nqureWbALe z)e)3w_dnG9XAgv{1ki6uXqT(OOB+X1e2uoarD(;KPX3UuHmo=ZoHQY5vunD5Cd4mA zpdkH!p4Sms&I61BRjB6zkPu=Nfs%&S+fx_X?}hV$w5;yhzy%5+*udLPZ|)-(Fty7cj{5~J zdH`P;$_*# zLlIxu`V6hFa{kPNKyrqh)qAe?v`mDe!~7j*JeTLX3?gkySw}soej1Wmjb0`#fVch5 z1HqS(JlFS0n}@Jn3Dsy-VzD=NtDs<2PI{**^Msvsi|73g^yc78i>m`iROm+P2PP?c z*{iv6W5PW(nnw|iMQ19iMY#%Ehy@w6<=m`KpZ^+{M=)d8td%f1l zU|}aYq*c2#^0B@ZR2zbNjXJ@qr#vu%n<*l^nbB+oyMnU-QW-@Ec*%nxxS1lttn0

iJJkrA}u_M3qiO9er zb|o~s;GCxObYEAnjpcgqDW=dy`Q`>M8mkw(_^V&C9 zi!rl1AZXP&tGgQM|NLf7Jbx-M995CzgrRjWb4;2_3lto@d7g*ka zQdX>P%3dej>D)X)6Hl>>5_L6{IT%y2hHnE1sVj_>xVCyFx!9{XPhphQvqFAwo7#XF5X-`5EJjO;^_A) z)9yZV?5F#no$fU7o(b}_GsBx=6@Eh(wE7%O2R$Q)floK_5Epiz$xOGh2I-9Np;QCt zS0Yv0rv3`FlakDa-pNu=ud}LtR{4RE*CB}(D7`BaqOS+Z6ZIREr0;|m4diBq0spov z@)TqA?5xpLAyi8Ym!~RJeryk(+`RT|^#+5P1UkZT=q$heQWdg_TJxGx>D0htSkFhh zln!^=3KhCa3iBA8a>56e#-6#ZNTkqVj{;c}+7SaKZs=p`di~#z?>sU{QVFE(RUm1- z|4(7BNxrlruhw9Q@9-^8hU&FXo^D`tADIqf>tC;UvmICL<|aDMg8Sv^sZZC_M+OgP zdx*_x%?kSH~#pRcJ=lnE$*o9fi9336U=C#v-CZ^)CBU#J=5e?6)P z7_OPiQHpPG%hMA|Yw3#Ob(P*QJ#+q-9`QC^RJk&WjSpTVe5i%q zsUc6;Vq1xG+CubJ`^)Bw80dIdJZ-9b`|!}Sx8jtt)be>H8&}7u*~Tq8Q;w+P5{P2X pTH1C-&Qf3{#)*}4^;|-s_M9EX1#ZX6);?RC?pZWJ4YB5``G4ZrlEnZ3 diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/SchemaMigration.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/SchemaMigration.kt index e7b36efa94..b126c95928 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/SchemaMigration.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/SchemaMigration.kt @@ -4,9 +4,13 @@ import com.fasterxml.jackson.databind.ObjectMapper import liquibase.Contexts import liquibase.LabelExpression import liquibase.Liquibase +import liquibase.Scope +import liquibase.ThreadLocalScopeManager import liquibase.database.jvm.JdbcConnection import liquibase.exception.LiquibaseException import liquibase.resource.ClassLoaderResourceAccessor +import liquibase.resource.Resource +import liquibase.resource.URIResource import net.corda.core.identity.CordaX500Name import net.corda.core.schemas.MappedSchema import net.corda.core.utilities.contextLogger @@ -14,8 +18,10 @@ import net.corda.nodeapi.internal.MigrationHelpers.getMigrationResource import net.corda.nodeapi.internal.cordapp.CordappLoader import java.io.ByteArrayInputStream import java.io.InputStream +import java.net.URI import java.nio.file.Path import java.sql.Connection +import java.util.Collections import java.util.concurrent.locks.ReentrantLock import javax.sql.DataSource import kotlin.concurrent.withLock @@ -36,6 +42,10 @@ open class SchemaMigration( const val NODE_BASE_DIR_KEY = "liquibase.nodeDaseDir" const val NODE_X500_NAME = "liquibase.nodeName" val loader = ThreadLocal() + init { + Scope.setScopeManager(ThreadLocalScopeManager()) + } + @JvmStatic protected val mutex = ReentrantLock() } @@ -46,31 +56,31 @@ open class SchemaMigration( private val classLoader = cordappLoader?.appClassLoader ?: Thread.currentThread().contextClassLoader - /** + /** * Will run the Liquibase migration on the actual database. - * @param existingCheckpoints Whether checkpoints exist that would prohibit running a migration - * @param schemas The set of MappedSchemas to check - * @param forceThrowOnMissingMigration throws an exception if a mapped schema is missing the migration resource. Can be set to false - * when allowing hibernate to create missing schemas in dev or tests. + * @param existingCheckpoints Whether checkpoints exist that would prohibit running a migration + * @param schemas The set of MappedSchemas to check + * @param forceThrowOnMissingMigration throws an exception if a mapped schema is missing the migration resource. Can be set to false + * when allowing hibernate to create missing schemas in dev or tests. */ - fun runMigration(existingCheckpoints: Boolean, schemas: Set, forceThrowOnMissingMigration: Boolean) { - val resourcesAndSourceInfo = prepareResources(schemas, forceThrowOnMissingMigration) - - // current version of Liquibase appears to be non-threadsafe - // this is apparent when multiple in-process nodes are all running migrations simultaneously - mutex.withLock { - dataSource.connection.use { connection -> - val (runner, _, shouldBlockOnCheckpoints) = prepareRunner(connection, resourcesAndSourceInfo) - if (shouldBlockOnCheckpoints && existingCheckpoints) - throw CheckpointsException() - try { - runner.update(Contexts().toString()) - } catch (exp: LiquibaseException) { - throw DatabaseMigrationException(exp.message, exp) - } - } - } - } + fun runMigration(existingCheckpoints: Boolean, schemas: Set, forceThrowOnMissingMigration: Boolean) { + val resourcesAndSourceInfo = prepareResources(schemas, forceThrowOnMissingMigration) + Scope.enter(mapOf(Scope.Attr.classLoader.name to classLoader)) + // current version of Liquibase appears to be non-threadsafe + // this is apparent when multiple in-process nodes are all running migrations simultaneously + mutex.withLock { + dataSource.connection.use { connection -> + val (runner, _, shouldBlockOnCheckpoints) = prepareRunner(connection, resourcesAndSourceInfo) + if (shouldBlockOnCheckpoints && existingCheckpoints) + throw CheckpointsException() + try { + runner.update(Contexts().toString()) + } catch (exp: LiquibaseException) { + throw DatabaseMigrationException(exp.message, exp) + } + } + } + } /** * Ensures that the database is up to date with the latest migration changes. @@ -98,7 +108,7 @@ open class SchemaMigration( * @param forceThrowOnMissingMigration throws an exception if a mapped schema is missing the migration resource. Can be set to false * when allowing hibernate to create missing schemas in dev or tests. */ - fun getPendingChangesCount(schemas: Set, forceThrowOnMissingMigration: Boolean) : Int { + fun getPendingChangesCount(schemas: Set, forceThrowOnMissingMigration: Boolean): Int { val resourcesAndSourceInfo = prepareResources(schemas, forceThrowOnMissingMigration) // current version of Liquibase appears to be non-threadsafe @@ -140,19 +150,42 @@ open class SchemaMigration( /** Create a resource accessor that aggregates the changelogs included in the schemas into one dynamic stream. */ protected class CustomResourceAccessor(val dynamicInclude: String, val changelogList: List, classLoader: ClassLoader) : ClassLoaderResourceAccessor(classLoader) { - override fun getResourcesAsStream(path: String): Set { + override fun getAll(path: String?): List { + if (path == dynamicInclude) { - // Create a map in Liquibase format including all migration files. - val includeAllFiles = mapOf("databaseChangeLog" - to changelogList.filterNotNull().map { file -> mapOf("include" to mapOf("file" to file)) }) - - // Transform it to json. - val includeAllFilesJson = ObjectMapper().writeValueAsBytes(includeAllFiles) - // Return the json as a stream. - return setOf(ByteArrayInputStream(includeAllFilesJson)) + val inputStream = getPathAsStream() + val resource = object : URIResource(path, URI(path)) { + override fun openInputStream(): InputStream { + return inputStream + } + } + return Collections.singletonList(resource) } - return super.getResourcesAsStream(path)?.take(1)?.toSet() ?: emptySet() + // Take 1 resource due to LiquidBase find duplicate files which throws an error + return super.getAll(path).take(1) + } + + override fun get(path: String?): Resource { + if (path == dynamicInclude) { + // Return the json as a stream. + val inputStream = getPathAsStream() + return object : URIResource(path, URI(path)) { + override fun openInputStream(): InputStream { + return inputStream + } + } + } + return super.get(path) + } + + private fun getPathAsStream(): InputStream { + // Create a map in Liquibase format including all migration files. + val includeAllFiles = mapOf("databaseChangeLog" + to changelogList.filterNotNull().map { file -> mapOf("include" to mapOf("file" to file)) }) + val includeAllFilesJson = ObjectMapper().writeValueAsBytes(includeAllFiles) + + return ByteArrayInputStream(includeAllFilesJson) } } @@ -184,6 +217,7 @@ open class SchemaMigration( if (ourName != null) { System.setProperty(NODE_X500_NAME, ourName.toString()) } + Scope.enter(mapOf(Scope.Attr.classLoader.name to classLoader)) val customResourceAccessor = CustomResourceAccessor(dynamicInclude, changelogList, classLoader) checkResourcesInClassPath(changelogList) return listOf(Pair(customResourceAccessor, "")) diff --git a/node/src/test/kotlin/net/corda/node/migration/IdenityServiceKeyRotationMigrationTest.kt b/node/src/test/kotlin/net/corda/node/migration/IdenityServiceKeyRotationMigrationTest.kt index 327959f466..f3364980a4 100644 --- a/node/src/test/kotlin/net/corda/node/migration/IdenityServiceKeyRotationMigrationTest.kt +++ b/node/src/test/kotlin/net/corda/node/migration/IdenityServiceKeyRotationMigrationTest.kt @@ -8,6 +8,7 @@ import liquibase.database.Database import liquibase.database.core.H2Database import liquibase.database.jvm.JdbcConnection import liquibase.resource.ClassLoaderResourceAccessor +import liquibase.resource.Resource import net.corda.core.crypto.Crypto import net.corda.core.crypto.toStringShort import net.corda.core.identity.CordaX500Name @@ -84,7 +85,9 @@ class IdenityServiceKeyRotationMigrationTest { persist(charlie2.party.dbParty()) Liquibase("migration/node-core.changelog-v20.xml", object : ClassLoaderResourceAccessor() { - override fun getResourcesAsStream(path: String) = super.getResourcesAsStream(path)?.firstOrNull()?.let { setOf(it) } + override fun getAll(path: String?): List { + return super.getAll(path).take(1).toList() + } }, liquibaseDB).update(Contexts().toString()) val dummyKey = Crypto.generateKeyPair().public diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt index 05c898a0ba..85b3aaceb4 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt @@ -114,7 +114,7 @@ open class MockServices private constructor( } val props = Properties() props.setProperty("dataSourceClassName", "org.h2.jdbcx.JdbcDataSource") - props.setProperty("dataSource.url", "jdbc:h2:file:$dbPath;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE") + props.setProperty("dataSource.url", "jdbc:h2:file:$dbPath;NON_KEYWORDS=KEY,VALUE;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE") props.setProperty("dataSource.user", "sa") props.setProperty("dataSource.password", "") return props diff --git a/testing/node-driver/src/main/resources/databasesnapshots/4.5.1/persistence.mv.db b/testing/node-driver/src/main/resources/databasesnapshots/4.5.1/persistence.mv.db index d5f6cdd09f81c8cc02786367e207024317dff57b..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644 GIT binary patch literal 0 HcmV?d00001 literal 110592 zcmeIb36NyRbsbt=m?Fp_3dE402yS4ZL11Qp?#A1f20@~xyJn_1y`j5$7J_8)t(f-o zbPanMVkAYFwk(DgOQIpkq6kuyC`z(Knv}KtkW8EZTZ_ZC!jkOI2!|}25i%u(tZ$p) zFUyu?h2o#gd`sr5w^ac&6+aq*n5ycRFZ14;_ujl)W?o!W=G@(pcO(AX+WF>SQI;fW z&bxlJcVki3!Z~j@aP}7EId4C34g-HtHZ?_;%usUuxp1_9(>cV$+rj?9&Irx$Lbw|o zde?*fMOBspNjL42jpP50DR4}IV+tHo;Fto(6gZ~9F$In(a7=+?3LI15m;%QX_=Zv7 zV(jZ)tS2cep5ZUiSY;O~Gj96OGnPqp|gWH5%{!O?*DxIKA-#{XFxkkI~Pw z-#eh6k3F}+K7VtidH-XNKXA`0Deeo3@d_Zjpaw1SVZD0Kxfv|%j@->4Y}4@!slr2&!sf~(FCX<0!pPY$q7t6Kmt69 zRW#|=_@pag1XRQU;}8ZMgaHTP+)2U~v&-EiySmX?TOV}R zwpUi7X6u{Fz0I^;e+^H?7UbdJ+D31-bGf^@bg{d6MwZTI8?7$uTidH?6IDMO!_nJX z+U#!(`s-`yV?|G%FRibx_SOblxeo{33%P$^=|8jGKiA#jfKsG*#HIDMLGNlok1O4C zy_NjImU|m3>(>Y{!#+sHVSDoIsS}TMI_;mFS=#J@uAQas(#76#XV5*j((BA_Y@b`{ zFU>9xns*6Qpwilf-pcxgmGz}dvuD_ZkUn$BOPY`wI=8;Q((A5e2}>Sd*z5u$eikFz zvUks2OMxYXCD1s;zx<6w>cI>cXx=z4vPdscwQEKoq-r#p|d zKRVN08T2-5QlQgaUhZI8wg#JBLV}cH8<&RpgLOEl&(MKEyyf0i{#}Mwgu$s3XF3nJ zpNw$c>OHgFTU+8M=VJd{Z*vW^H;f<7b|4a)gU*%y;9^HrPn|f6IsW!$`5a&FZm$f6 z=eO4`^fCEE^f2gc#dEyAJ=oX=`w&QTXBz}q+v+Y6X`oED7IT@QjmcDhYiql=Ib7?m zg3XhLoajUiHhbqO1DxxxVVckAvJy+e`jxfP4xZTtWAz8%pL6{SJZ};fdJM$)Yz4#T z5@WalqJDiyLVE5m^)~=EWLTgH0r_!&{A;y9Ug@v(x|?6m*@Ffm?6x9KGdFwN+q{+` z4-(lYxXb~UpPn3-ZyfRG0q`$W08FKYf_SY^WFp;GCMi)9rgy3QU+ivO%rHbk&8YQN zU6S&Ii4c1Muz$J&cE;;GSXd}i(T7qQuC3<^J!w|~9N#Tri-fg#y+9y+0*d{EwG}&t z6%>1}yw0AP74+6zXEp;4#yxQ#wEpJ{w0?fK#achN{crH|m7O zZvYkEQ$S;G!H5kAfyRsm0wX|jxwnXIcH#ky;MJzHaV|q>*kg2x2kS%hjF}#;b~i9g zR@I(uG)|p-@Y8oa&}hA}^`_RFTW@K-we?`@ZLLo0q1J5c;nv$*kF*|boo=0JoozkV zdb~B)nr|(%o@hx80Qp9!+kbdh1m*elP1tFd`@JiiZFiZ{nZeS#S`5dyuA1w_USpV4 z;pf)T+m5R{iXOh)PV~um{H%k>xu%o4f++&Lr}uJ0-sv#^|C7xB|1kXj-=_Y34gdeH zY5o7dnEBzxU8(=?B>umbeDgn%INeSh3Gk@bjeny^K%v(im_`x>Frz37$R9-k{3OaE z>zu=#8xrt1huh&C?ldIeB^<{hK9#^FaUeAt2ci)HF%G1dlR)Tk5ROMu6zdxvNr}v| zWB>oy|IZ1Uh;|?eJe01k%UE5H{r_YC|JeWkh6P`a{r_YCUxrH=jo@ytX)$RUsR$Nv9cN&i3fzj^mL*+*~d@c-qnqyHcKs#@f#a{m7WPH74| z?{mKVu0QRO`2Xt6uiteS^8cIg|DXJK^z*J?en0)Z`+u?Dum0Aryy1Zd9z^IzZuJ|D z_J6-;>#jF6WJT3^{Up%)~j3hwA!tETd!%|*E-dDZR>Td*SEf@^@i312mn3Q zdGzX5Z_r^8py9dg{>pMkw(V0VW;?U(zZD1Kx3`dC?4PHB{NC06)?lkMyVXV34!Ot9 zX-%;V!?1MKS4@1dPj^VjJVP@j!&NoKQ4QU3CCzdrHMDipbzIe#14(r(TT)d+ zk?p{CJ=gF;*EOxc_bvG})~(h4+Nl!{cOC@eJ(d)VlZQg`DC+4r|1VN8BFl09KdMzY&i^Ah zhP=gb{{J}tA64iG-Uh-=W2I1hHmER58z~w-F_WfqkMsXnN=b;~IR7t{;nm`-oy|7R7fljZ+^;;&@>f2=--Uy9Fmx?3l=dMl`DW@Wj2>B_=b zokO&ki;Xf`D7TGZOGGX5iMt@waGX59xxUI3LhvJ>?60Aov-9rtK3}Yx-z!B9^&|3A z^2sX~QQ{5PnUtzVi|;t5ou@ErZuQ4_Ta<9T33VoZK_!bwpORu0zF|~MmsfiBW{kB` zFcztzEEyqNE<=khNuA7F(C0_uKts0N>HMW3t?)&CD66MTf6}HyEmJnbf`wxNZB_?6 z#*aMOB(n){-iq3#Pc`#vs>?brjy_i3QMV$m!Z!(^5d#{*$4RzEptN3Km=ubO>j+4K4D^#!d6Uj( zf#|=kgJ_E1aH+RBh~tCRI7Dkne`^(6DGIlZpz^=Jb|EdnOg&Ip9pAmfMkR0z*(L(? zyRv>xq}#(l?Td9$OUnOKdKJuC+8|TI%~w9h3+PIB0Nu4ZyxiN|q8o~G*Z@J}es?8z zO9PGF{y<$EhS3Z&JAwSjW3Cvqbxb1R%IJ5BR)lx-_k>#n4yKGrb{KufiD;cG5E7rKiFIV2{#bhcM}%fn>H7`J&W>W^%&+JtYxwoeR~R=QhTd8A4B zCwh-;S0aLv0>Fhs=);mhVIN?3OPyghdrM?jTRsX|n3z#S{@uook{ov0gsphFyS8+V zVq4HC-P<%|TjfYc%f0htuTbZ-bUHgK*Pd*D;FK#nwRITf&%PQyh9Br0?Nlgmbg4S+nU%_bXj0J zf_~qMOe#4;pH#g>N@_O!OSBX{j-lRLdj{0P0n)_*iygN<5u(q5=pQQ}dI}KNLoxC5 zn&(Xd4?!uxRYLE15MfZ82wCeefr+g1$&YxgUjXtSsY1St-;y4;@(@j)((y`B?GjYU z&Y+_LktVE_V_GqOi+L=&7k~ng^Hv@$B+%kd%V?3;Pcbu;!<(^$D+pEIS0rD+J=gM0 z`X1*1=yd9pG#dAwxbMeEBt&j0Ew!b!^p?>wTUN_%Ew-L)y`%M1>z%E4wVrNuTjyF! zt>sp)b-s0>b&>3ZkU_WaE2CSg5K0tR0wpt9C^r$V5sPRO)1y2Xc%Cqm0$b#-N6b`U ziqJtfHrF>`HulqfaM53`5!*Af(q~t}r0M1E%64{#95snlkmujNWb))H*2$SDoeEKI zpqr=V9Z*^WrT<1PlXMw7)BA+faF9f&frV{2YUdgGnSI2vVu>40Pw0T;Z`Of?vi%04 zQ0Xq;^iP!UaD$9--p73n1E74g7AUbPy1jv2^2;e3GJ^#3!coOGQjB9@ z;`ghKSjvzl0Q=?|a?wvFEAi}VO;YsUwPMm(fS{Heh65kAx)*wwkHk|*|HM%x#|h}K z!HJ@K)QLgPIRIG+-s-{PLAeS)-w|a|6i0X957)L=vGG3k$0!wT;P5RqsMsA0aL@uh z9%8tKfEB#Hn4EYda#~&u410$IPnHyo+185yp^kzhQiA~F*mRajQ$&F%n#vE)``(li zPXdgpjmK2cu5WKrZ+@fOPl=G-m6y&#o&tcueVIgnsbUsnE(VyMW0CWmg8B}C`ZIN8 zatq2}d3yz9i!AQ1M|wRSqzk`0@f#Bcr64~AkUv~;o&%-sT5oV=ee)6tIAOlAaX;a+ zOi&_^uDuNy0=d3p+THNnHL_P+46DSF0?vl}x!EggM1w@RuVWB$WC@MlY;*^JM3`Gj zhZdt+*@C74H}SxO$_3A_sJj9nTz4apvUG)ua}w04d>ZD!CrZtM z@g9neOlCEWy^&{&$D&+rIRPtBq6;o< z6^uX#(E}msAHieh2bvS^B))fKK@b9*2Lb+e83DKzs{-@1Wgg1Opye}9orVj*xW1O@ zb#r45sf|K-KmA4L{UXqQp$zS`eI?^_P4f^>A6INWBzRyq{Nqo(aMy#4R=@S`)}_`; zYqho3T5oN%o@s5iwpxSMcI$HMO6zLtTI)TnXIt-Wy|4BD*0A+~meX=uUdwLmaF{*ogQ_>aSzr33=1>3@7w41f z$(dPs##phjBDALK)g_SOYVFmeYv*Z^am#I%8f^sY74XpymhsWp%N0VDZJ)YB*uBP%%!;*1#_+1lyeiNs~)a!{$?OLkA>xfM}2+c+r@(FWf0 z<+{nuPEx(L1%|#9F?3eFGo=hGE1Sz1rv|UU2k5fCnPFs4)+z6N8=c3eCuQMHG2?Cbp7K(eR8$)LG3{mJ#NTtNF zWXQ)A;Qr$UxaTj##fU@Y!rA9f^oft8g9vA$!;`6J92^8#uR@kzm{697v8A23*pW4o z1}_AAAd8DrwiixiB zF5dZ*I)1X#vmo||B8Kv$OK!z##IdW zl*AE|FfWr0;tP0XebbXI8Dbeni&aeU^+jPf6bTK&hq=e^(%CT`5XVkkI>Ht@1DK?{`()op7FD-ry7md zetPbg@4mlshxz@DQ}=Gmx-4tDtlPR~==1V|yt1h-ERFVkr~UBFo#*zQ!=2II6W-|N z&C%Y1ciq{0F4!GCH@~yDbGY#Q&D~f3@C)yH@zekJYa0)(#K)qyXRSrs549lBG(&SV zN0$813l&o~JyUgb#ZaX7g0~+yhrw3x!J}Z$3pxkUmo=Py|J3yzH`w39<6->gv#$UDfoY zAaE7OlNDVuLfLdZD+r|a6SbkAXa7Euh~+l$v8?v z@-@kA&z9jZ5&N6dg8g{{_GtlZ)3GE&3v|nuCCQPLz}JmHS1lP*VH<(go-YIYaNpTG za6F>>hI^yh0<2Aq!7mUD&IlN2IA6opEI*Jn*RWmRS8zIq=1QKXTA^f0?W>OOZ|v{f zboTFD3hs2g(cS^z-q|}W)JQQcQ$-^0ZQgVVze*6E6A*S~#Z)knzUM2dqT|#YO9?H-_5xXT+A<%uupt2zoPGY3f7of z3w?!B>$L*4Y)AJcSu#9L4-Hku(PNUM1_}-*1FI{Zg0%D`)DmW&Q6iUCXh)?!CRY5r zj9Nn91+-fCb^~W`NL|Eq;XPgHs@_&(U;C?+NUsx!1Y1z{RnrY^7fzq!J6McVFK`Us zwN+d7+FzUTgWce;(o1FNKG~_)y0D!ig`@CsU2V0#ceK0fxI4Q$hj%6- zzZR4Ke^Bz@FOVOOP>56V6!_St7KTt$INDE9VDR9`J;@E*e@=LGZFIPX(K&JUM!QEh z_o#e+@W|Oa1W@BkW+smCWT&btNIuBn9WtkP=tmUWpN47XAIvt^aL`8O)C1zAo+^ zWup2$7tLbsoJ70C%FejE~z)P~6P(kEPBA&d3KqA5^fr~GWHtC!Q%4Q1n zUvEw_qvSf=LYrFiA-pl1Xig?_dH6Z`3pyun5auM1;1L9tt;wEZIht;nl5a?%PJBPl z*Cp)Fs>8TU_Z0YcoMNhC(;EHcs-RQ%4x#pj`&bumJA2-p*vMx}d7{NZkJZX|{y4E= z9}pG?EWf(%IiUpK(DXIm&{RJNHC5LgU2}D#{hefSSfWe8SXd^e!_h4tLFB^6Rr))N zWYJ1cK&bX3gi<>P!_=sR*BXxYZ_?ZV{!G@fgNbNn#5DUjrP&(=nmJyGbh~=9|5Sa+&X)s}Z1e&`yNB&NDXBlG;?%WFCT_%<*rxOThK+QVfJ2T^~ldY6UPPB+v9@S&s9M zSxz8h4YAbTK-Aw4$e@h#3dAUrf0ViK#{>xcJVoF^0Rh;cNPIyzgdyxu$8l6olTFEn zia_GavD=U2OAAGS!hL;oP&Z@8ThbH2{fiXtw+V3Tfdkr`ULg7Ki%fE4cqrrWa97tu zJ&>*TV^wfd=S5ngRV^@8U2-f1DFoQax+eLyt%Q((_6k9&{PFRdO(Y2;-qr5Ytz~rKW6{qn3+;cUkp4 zqy{VEqItp2gvaX=i1Vi=nDu`~2_)7BgpQ6X0Lk^Xn)HD z^hvdTE?LsYFO3>k_Jf0;wVukivuX4dnSk z4dpjVUlD%OJlN91u~xB0eZ8}LmG6eq#P$8|f%e4$P$^3-!} zPUrB@@vh$t_6jZi4D6i2G9m0=rLcGgjrH(g|>x4B}GG-r{vk8QVu)H zF})kNMkrS*T6Z(Ba|Hnt!u|&ocCmKDa8ZW`N=rc~Sf#(7X69WIo6nL@p!gWkIxo%*nl7;93SRL6SGjOmDS_YPx_L&maiGkk_4)^cW zr2X_7=RZ;KCBe&eLj$>!0Qyu$`6gK<6%K$*6IWVme=)iqNV;g@C|t&OLM;A-Vxb9G zAfONcbj$@XK&a5tWy#UORtOM!p>MTMm&h@PMNQ~c&a#~l`ah-6>jL!9^r(T+5N|_y zC9LsKGkr*}?ntKMDyrFDs}7BD_CtQ$fNM}sSE*d|U_vDSj3Q|WNFq<7d32SKEF?(+ zOOZ&)l;WDm(HnYL2T9J*$VZZEP!q}N7sEfNNSXqY5GBvSEsLguB^^OtLl0CJ^=^Tr z8>%fS?e|pAt+}RFkEzStfbf|!PH$J8D%7d zAcA(Spk=ZI}j z06QliN3KCl#+`m4`|xK-a(hugQbIW`wsd*6@1mp;-w@1_O&R;Uu#7pL(cY|P+!zOH z50#@ifytyzuWP@JBKo9&Xn?FUa$+)S4>b=92I?IUlhUy(#}Q_+ zfA4;%QFJJ9{R5;(tRjBiBW-i!DSitalFbQP>vaQ?|IumA~3M|C~lkR{tSeOpHip#6br+2?F1 zl5wY&U_Q!uEo-99C8H)ZNWP1r{VoA*Uq=+$SAEy95CsiQ(=~!145bje6ir97+Zz+1 zU7*-GbZZ)y#`LD~_fj;U7SNPoSR&bmJ#;#f&#;ZLlhL;l(}6t;=QWK{PRJ1-rKIW#r1Ct}UBjSJ6(84Y`><1`j=LF2K9nI2p z6BU;TYaq6vB2b`W|Dhh376NB(`}Yec2FM6w4-B?O2B^;=N|xAH zYTzMY)_yz2kD}j)WD7bCa8t_1CRK5KY@4Gki=(HElTEsK*owl%vu`K0@SmbkFAGp3 z3L_(ZZRjS_^w^&rDz4=t*kq%I&y{t*{VW@fMY9pLr(aUgIkKl*L!dF| z20`X-JSma5H`NIyq6ZL<8|#B3X1z|RGRfaD5Q}u2Zb)ZG?7Aqobn4zVlJ7Kee6)9T zfbf;Ts?@Zv&x1+H?PuxaoEIhsk$lyKsVaq-LL<&q_)=+zZfT8QRA0@dm#({_V zl2hhsTpr(ukOH1fe`kb;FRuO^1^t3hTpfCX*g1ZI8P4JA4s8ru98ibPTg+*R2@PJv4ebEg(h`KD7;Xk&)US}T+j)K z_YWxXRs`ZHx~D@b_rCG4s|Sd?HuzKOwVVtcWrV{<){(h^)=vX$1bamZ+mMTA4UhG#z^A#3zlKsW~$ zqK{<%h@3g`5={SOgU1&s1=j=$;ux_IrK>V(R}IrZ-842%qBH^d3{#V_`J=@2r{|?{ zM;fL_FlJ2uxM6(DO>(ek0pcjw&c{4{LpCs1_`*K3IzMG4^V?J z+D9o)ivNU>kAF$A-xRQS5orEC>$^9iVfi|C&iGsTd_LZ; zh*nGy2k3@v3r#k6PED)dqqMpr(8`A(BNyZ%%A~kD4jIJ$c$^lBQ$SEk6ofX}uz+Y& zq^wz>n1(xdd_G<`1|(6)Om%1T23*tn`%l3*#X+81M=mLdqnl?Lm6FrRZr!K>Wqy96 zBob)Ha}&)bcHgtY=^=(kaBv8Sc6Yo>II-h!v~?YOOtud9Q6_fg?9_Aj2XyYP3UjB+ zj!ZWAg^FiFO<|`wYEiH?z(>Sd)eXOnNYX3-sl>wpVQhA%2Jw$5h}Q%VWkW?q$#9TD zMo>#bU3GxKpywm>fZ{u7Q?fU|+7c*`5po0q(Eurf9rJK*L5X z*1|EvC>9Sj8%rVfxjW<^jhyBcO6@x(m3A>-f=BCU60UhH`#cZRRh4T~K$`Yh&}MkJ zgQ(P@b2Coy5dNecaHbPyP*1o_{1-~8X9Y^R*xrCTXrvUdeZ|GuCz6UIh(kCMC=u1P zmpI{9;zf_0h)j#pqwz0Ze5P@7h91rR=pX(j{XG5j575su58lr{|LkAT&$Ex7 zWS?8_W}hE^fqp(_e~f-U{-pu^oco~-_W749%~oq>X6F9K9)IATS9#Zu_HHc5vL1g^ zyjQZnDMs|gR--STbT@kxz5=Vuf*Q2QhxKX_I$YQtxtr8F!*{9x7IeGOn0fBS|D(}t z#GOl8^3D128QR$*)Y6=m82NLzXai$ze>6H=)No!e{zFt%^5=*EE}{r@jwOv2HOr9o=tuYPGBJZj zdyBGW&qa0|1qX#BOQWN`!$m0t(usj|915gf0;H)>AV~>C1f&y2Kw1%y;BfqhZ9859 zq&*3c&Rhy)qjA^mPrb9zO2{m`F_~pQ`4+_A6g&BrPJ-;l_=Ot95C0ZUg6S~)lmx%f z%HU^sY!YDlu}MH~a{~MTPKsYL39_F6=~Yhx15%e_AT>$OaEghXtAul*uoyBRJz1Ru zNIw_IAOez%N7%UrB~@%lXPknhcLbyvaf(Sf1@?5zDW0rN0%X7e`I%Ve6gvh|@vAB6 z(&^TqmARRi{ZOh8K|BXu`EeT|bkO^W48lap_WUe&DRZ+3zpSd*zf%{FQyh|-ye zS8Tw;t3I7zM^s;+Mfl+e2Xg`h0Z%#&KNd5KCPz@RNx;9wn!%&2f}Q#?j1hzm;}ar{ zC#rl3PyjUrI!>k6Pl0SP3E2bq<8F zQoz~R7UA! z?NwD%1R8aKl+uV4#RiZRa2#A=r2!fq=;rVL#OE5=cTVh4nHs0E#6A@EVXzOAeOT-#H$facNdHPt zY+sPgXoGNKgc|%~Ma@3;gvZ&p6-{_-W!_eV&S5(omov4ppc%wyGLkC|N$8%cay z*ZH?)JNwwUhvLoNmiVr~6`8JFzAH70hAt369UIB#-^OB=={yj|Rx*zbRd}pr9}7fK z_3UF?7&}WNLj*00wUwZV6b)vIQboRE+Aki5lY&3&)VlcQ2_O`*^ zHrU$+lNW=@i@`+DU|kxlFV@~&s+)nF5Pw*-18qS=6&EV*1pZ zU4%Eg>fGYzM9}J`;iY7dIoMjg)6gtl`x*dsB^mdlfZ2=1-i-j%lqLCfj ziI|NkA|uO-P9z7>9Y0u8V- zi@Lyd1o~$M&J##v%9*!4MR+@lSfDVC2QDIK9tt$dvQZ#t9V0tZAh5HMW#>RpX;$`e z7-?HJl~#7_P{7j4vT!J3Io7!_yIAr?3WpKojD^jy&V#pctD%y8EKB-ZxK(59G5@li z<$oOb&z`ff?JQAoJe2T$7Bg9vI^5t0C}nWN{c)^8w3fru7m=4`RTv@5ohU07o@dz; z^$=`;qdkALGJ0-dYyeF{*vJ3eziB+q%pQv_1vdTHq5rz{UuM=Y9UtCHuv$ofMlLn| zsSCeM;g`+-!eMgUdpMUOalMQraFhj zVTsO3g0fh~ctOf)QH6h)B0eg?Ls*dLqRWF=`fO zS!1+mwx_Y&K=9sme`NX{u^d4VtRmRGp@wHx0L`c}>l4YC%&E znugTW@xR>E@w(MC?53_ab*pJgO;c~0cGGlbj2R6P>`(mMcQ+nrB=p3ZM~(Z1tZ~oC z&uA6RT1+@5cJVLqVZ>h1!)St|UlG$pzapk#zf_?m4qc(F5Iu_+D*6?PWc17A;nesa z!dp0qo$FB-M#Je@{4-%}+^WMr6UNpf0b!8n){H(Q&!{s_(==z?8Lz3hGng=~sTxhy zY^r`!m1fKt%xBm%oTer>HKnPkP2F$mVN-)i-1zrDx7WC*A&eM_4;#Y}u)xM+4mMMO z+Y&IeES@kzm$uAiGf~X1AX$e6m3^4(!)j_~Qv+LRO~Y>*W>Yt3z*68_?;h;Npsp{X zH+uXd#4)Au4+SSnNl@;`{~c4{m;%QXIHtfc1&%3jOo3wx98=)0A_bx<9xch$bBR!A zF2uco*#En~NKRYC9WG(#W&ZDEJbqjo0zZ{xFG)4Pt;HzWi@ndtY|~7itVBsv93zhu zs)5}CQK}CqO2y@X=c_wjW}u_)P`TAmXMW|byHM)2^>m~0F3@5i~WA} zw|?af4?OT7F1eFi{YInx-|rdR^@av63DmGts`bj&t6C>puWsGbYParfy{2_v>s0Ht zt=Gj@MJI`*rz)eyIU7Kr%eMz-8gg3RW-%Ik$R3R zX(-#rDUR4H?_j%$jFagc3s-ok_u9 zD{lo5dU3h_Y-iA4?QIRZs~gz?*Eg4Yn`yiL+92D4JRDrx;D(c>F}PQk^{wsI6e3kW z8)3BE+gjS}Zw&hDYw2Txdh~p0eRZ|BHrUF2IOtx;{rgJ)ne9FyF!xkSdt4jzt`_vT z(mmH($scUFx3RK*jQ}&`rwQ1eJbUWIBbfG|j5)LJw6Cl$UE-%chrFao4_t9>eSM|Z zUCUCRJidS%9dS+vH;WPN*t_ShaZ~_d3AB%P9%^q#Ji4*j$Gwo(I+uFaI2MimI>cXx z=z4vPdp5?t3=hJ+==*f%k@iPtAcK(3O2(&eLZX&C*af&X*z6J#q=K?>X_!A)hlBbI z9k|n9Tkc)u-(`437@RtBrt@(7$q47I-ZR_1wIyCAFZR#%HrI&0i626f47!^T@c!Up zM^#UqIEy*{_GY6*}+1=y!RuTgD!8M*YzD2qDOdM&8+Go{oLJFTLX(E4SkltC!m+U0)l z3TNA0rgUbo^sW}eF|Modm#o$hXw>O$o!r9VcS{52aq+CXFotKe(uj?Crhu}}Z6m_K zhRP@If-r|C&u^};a)jVVKG|P`J=b~ndY?De^LwQuf`8(j`pWgH_Xp38z+l zJa5TwHgSj^RVh{WW(}|a&f6-dO<=&N>+(rUAf|5Aa2Wr7Ii@Y{tC%_^6DOcjgFf7} zA@tS$wpiKZTK9tLV(tczOW{-^XRn6 zx&!F0)#2sd<`xYo=CA>R#{KR}Zb*a1ZhxRI4#Q}M+2T!pFlUndlLTS!*_Hm9gX4A zATEOEBu4_s#Na0nH)xu5$V5JgOuqpG-fDb_1UM0kv>|baB99$E{C<=yM?Y z#|ns^0>t%DO#Hm&d6U3HP)cx>INb9f!k{)0vesb&6IthzAMskh0OUVXg?t&mB|UEC zA(}j;JeG|GPylk?%A4C zje|#jxkhZy%u1hK1(T+iyDQt-2st&0RFLQ2zGU*`D%Q!FD4hyXZlIf|P@B$yWt7~4oOj%>xmlwDfs zZh^40tRhLi2u3WWkqN-QxrSWylgWw!iehUVIC+y4y?3pcG!`JJ<%Xf)W2<|ihxtf| zlKzPUBWH^GYjC1yj+z+coCA=R;H@6aQk1JC6hmhbW#c6KaBX`PiTBtaqg1qk!?)C+ zB7cJN76MlA`eJh8k;rMnT*WZZcR285Nzs^Xy$BHMC^*6q2r!OKXNFS*X^KX#m~l#p zCjmxP;*l!a_3cgS&2M!3DG{1gt=bE+|ny(`n`rB3(_vhqLJ~qVWi^&w&o# zUq*-Yl}i7EYnq37`j`%0ioZ$FUz&^&a?u}O)T00cSO(5TXY{)Gxs^^8?KZcM{(_GV=%l&VvAdyNm$bidBJm+AHtJvB z+IoL$*!n=rX}K-0<+p-XNc_P*NEnCB^5R306*L_3rV&eRWovz?dgGfV7SwEHJP}5E zH}U z^ItAi=LjLzL5Tg@gpjkx{a&(rBG0Xey4l7~0Yn>k&zI{aH#fs|)stCx)3|DXxDjP8Hkcd27NlIolotmcODkLk7yn6_^yT$Z%*Mr# zW`cmFxrO3h>c)^+Jwp__6H+O0EE)1~1-Sos0q*$=aWUdhxp4ORioUoc9U`2G4o{|@ zQ8)-#uR@kzm{69}tFR`PY8vRP8K}Dk&b?V6X=Ems2AfghI>(gCwI_H}D)ttLiOLyM z@8M)ID=twv<+ym~PwIGO=d&R8ha!y1Lh?it6H|?Zd7@osu@PSpA1V$1ycf8|<-yes z{`p*MQ~*uzXWj>{|2qX-PgNBEYB9rSn}W?L_h)%M_M}SJE8qYge?K7mo-)WdzNkvk za82{p(3q1-p@IenR6{`ZA8SIzNnA1Iie)0BBMX5n?jh2TkgDQ52}c@ErZ~O$0bu;q zx){f=Di=`3if{qxH_Fjx5&;@T41q?YPb$Ir=4&ifpIQ7t6f-Rf~{ZU zDu#PX;s}YHGua@%fLGQx-Pytr%Q#xBVuG(PipzN8nt1f>Lvj7xI#F1zpCtY|AjRKY z1V1NR!M&E7Hi^TmwETG$8+NhxYq+_c*?^}SN#-Id21+bM`D|Ltb9tsROw^Edv7AEBQQ-aUK&{r5kFJ>zFvPc<5^ z{q)?I?!Lcrhxz@DQ}=GmIxgmpT+YhxM&@|i^hC9zBKP1d+Pd*OKzY#3~(8gN!hl>M-=XJ~QQ+xl!|-VDxg9*f4T->` z=vu`r{M;SilfnOE1pbEw_*Dh}>AJ7F3T_K_F(ZzzDY)Jn*AdE+8nopJ;3s!J9@fQz zyL5XpEIvc9c)Nf_=qjF$s}mJjRa9G3aFc}O%c|*nxM5AlU36#5u-M(%3!MGQ=778D zdNSyLia`H}0KMk{dIsLj*^gkO>*0_WjIX4{@iuWlY#wt0`_SEY!i2d7+RoP zK8}BNBn4OV8G)`^GNi&b0^|Y;KrAx(R)AdUP*|P$&s$uJ%ruly0 z1|crI4R9B?A9#w61;aLMr#+~FtRU_Ms0wXrB5F^I=obm1j|qtCz9Rd$@m{heT@D=F zQexs_SP2(K%eYMnC&Zqwfhd!z0t_b+IbE6-wJ#CW9v4tk+)&r_K=(~trsmMoZ6r%m zaG9N98G-9o+;>|bc?DQbDWUq*OYE-_gy#f=U0E>|Or-DmimK>1HOIo;@rvyQvg*iT z*gnrxV}H*Np6}4paH)xad(V;^HtApa3kn$-reITj>XqxC64d4e)Fjt5Ll=r4clRm* zbiQi^AucZV{m{@Y*=^hTsF91{hC4p~e|{XPi8ym=?efnFHVXnaCe~orRV3U?ZcC=A zSbivLYJiK0Wf!+JYwdUDV?zO^<^!(xeCa4ntw(-?p!9@*5`-->=UT(-d5!M`^DZbSN>z^^TI{7Vv;febM6g6DU>M9n*NmYFt>o&Pb z8sa6Jx}!KasrX&_C>|XZBQ}Lx8B^P{zeABaB_JiceqiG0F9TPqyKbN>fvLGNK~nZ5 z)ArgwG=l};{AfSed2SCk;4Y2!4lo-#dxss@IS8n?QS4_hD{-#lC2!HTB-lhe60kCB zk^G$N><74PllWkMMc?%nOJiy+^c702*9z3aiF3Y$>i|4W4-Hku(PNUM1_}-*1FI{Z zg0%D`)DmXDgjQ%rr9LKB{JV@=Lf-|nTK9GXXKzSVe7f+~xOY{JeeJJOBE3!^5^O=) zS4|f;QN!tTd>IIJByS8epUi)h^et>iQD!o*OF0P%f)w-~qK#7Vzg|>w~vW27Y zab0b-zIU{{>$p3+JBN2BBEJ@s|9?>O-!G6Kj!=kG@^FtkZcoHz(g<{50u&fLIC2kn zsN)K7@aWp;a1En#;>=wJ{=p+>?+`$ZE8wHbor8nn9-=Da zeaBR-f({FSvwvrbPKvGoZ!}*2L%p+zP{oHFR-KB0K-HDK(cZkbdxTr>_X8hB;k?X+ zs;d2uTevbHp@PU8 z?s=6Z3xPz0Qvw%X9BtA$5tPjo>c8HcWJUqsjfA38GTl?aubg74V$&M^6s*NVsJ-Dn zR=V5Ho_8lU@|jYeXmQYEwep=mPHfl*gv9~Nua5h~LkYg2>1)2BseTY@s;)b_=ITcK zJIUg(G}=A7xi=O>j_JT|Z7+OWrN6UCjspxC`hz)rU$)y9?Pu4YMAVB@k7v27#?OxjI=H9fU0nu6JA|yvfuq`Qc)eJc1DadTp3e)O zkGr9U2Rq-)G31%bOxJB$j;H4HbZXurObujBmqWPNB%qHVmTw|KWFnNO!G!e@Xztz| z`8(kqaxc~3&aD96C6RcF1g8@SXyg1y$zPN?+7k13oTL3{iqzi750GyRM+LdNpQR|h zRY1veLlvPB(^3qCA6*|txoQP4BqY!DWLb{$k6BJ2V-2x5-$2yg56Ga5^9sZ$lfRR> z@W%uQ{5(bAK>-2Sph$c{H-sVVP{(mpPm@i_hKfMq%dy*!wyE>n_eLK@QX}xWOyj!@NiexLp_kK_G49WQ{^-Cg8jpqMCVM-34s44 z3VcTZ-V9*;gGEBcu;9A+hH3^l>fAC^J=AnZZAbbimum>oi}*alCn^^snE>Q}L_ro4 z?MQ|?rm6*|s!NWgAcX)MS=S`rwiR3+AGTMp5*8a#8Fm@7$1>0B&4|jSA16TXOB6jZ z3Gd@#E!Tnc!sImUP?HQ~!8F}dNN&&az4qmr=%rj$f@9jKCZb%rZvsUB2}M-Q)_aoa zxrp5OlI9t{2|pBxdP`C~!|+21S2hxxr^Y-U6r)#@IgRP$>DMSyV)9-xRoC@h$dWF> z;FWC;x(wVZIha3$@lYuUtZKN_lnry#a&DkAr`?bmtOxnwyM)K<5{UDsCYbeqMhPU= z2ZWA}Dgep!15H*{&GAtoVB2tTk)lST!f1cX1oTO@elA(k$1jZ1QCl3bSZQU-NOa1z6~SMx9mW2kWcg#+mQmzCmw$`IGN60PJpn8 zbBda#R4Q$eYKp3uKmr{lSc!PD=QI{D<_@BhVk^-@gyD(X&hAmrA%k^p!F|B$xf2{P zk1?H`x0f4Lw-YPz%XDhQf(q3}zRa>s2Rv)pYG7hUH1I5fGn(bta;Zg%H+OI> zc-uC4a!?(Sy|wEc5^s5UJJ>&f!#r(22f_Z1vs?6brPD~{mAzcVN{z;SFWlZHVcD~- z$6AlK=34Wuh1L@-spzO4gk=ZJ7hu|egqiDeTawfG3Y|T%9t6HHk{4J26&bvR`a~FD zq@d8VJynu$gXtD>Rv0c4mA&9Y>>G7TsA?il=Q^X^D7cb5$R!{Xz5?FNcqyFm_E<)s zFh#d_yx^d~Q~5oLs8|mI9FfO`-R9etZNN7%P@DuCAJ^?5^MyL4$Wza?Ii15p$Gd(r z*ekU3Gq7_8%Y?9hmBKC-d{|BpVpTU#6J|-F71|aGl@twSo|0#WxLLXkc9dg!H*SrP z*eqIiGq7_70TaUh2NZU(b_2Jx8X+hx1)*d?^O&LLyS{`(x`W@U+K%ot&ZT>jWI4L^ z+`i)%)^}zw;PU?yV(^C)1F=LyL2}PfVJ|=_n@WIE6qKFVriWQUv8P+Ekav7A%??>4 z3Yb3QvdEm#Ga>AML}3>TGBgDR88(VJ4dP~bC@MjThUFQd0H1RZ`tEA@@wLcm6ZQ zKo&3vY#A9Kaw8pT$_-yLB;5z`L&Me(HP+g@C|t&OLM;A-Vxb9G zAfONcbj$@XK&a5tWy#UORtOM!p>MTMm&h@PMNQ~c&a#~l`ah-6>jL!9^r(T+5N|_y zC9LsKGkr*}?ntKMDyrFDtBx3O_CtQ$fNM}sSE*d|U_vDSj3Q|WNFq<7d32SKEF?(+ zOOZ&)l;WDm(HnYL2T9J*$VZZEP!q}N7sEfNNSXqY5GBvSEsLguB^^OtLl0CJ^=^Tr z8>%fS?e|pAt+}RFkEzStfbf|!PH$J8D%7d zAcA(Spk=ZI}j z06QliN3KCl#+`m4`|xK-a(hugQbIW`wsd*6@1mp;-w@1_O&R;Uu#7pL(cY|P+!zOH z50#@ifytyzuWP@JBKo9&Xn?FUa$+)S4>b=92I?IUlhUy(#}Q_+ z)4LyP6dej&{{SfxtH@*GvLvd`I2 zB;!sk!F-hSTGm9HOGZs-kbDKt2 zyFjsX=+-nYjpf<1pF@-^v9Hv?L%^*4c8niIzYob4bQ<8Ml#flS;`rD$M_U$0PZ=khbn&nig^Oq3 zPHN#lMWJ36phgr%M*7;&O{D3uKRZ-h%SW)uMh%}U>wf!LHXMtpB5F^+q@Z(TPq~Ih z@y8pEF-O5fw_a^y-uhy$=@*$i*%fBNM}dvx+u4F>fSbX8qvh@ z(caMk!dC*TQq#UZ4<;qIpQV#?UYHz2@>Lh6x_sxcYMx^b109b?B*@>`J~4ivk%HNwSdw$B7x(^oHHg zir)TIMR7GHDi_blx2CuTboMIhO1S2g{T0aIIMFzdWdfr8eM+>80@1J?9vhtuTb7Wz zu^kz9nT#3NLZo^T7FKcead|~LC0ZIU$j32#UWa69>#E*r6O4;FO+c{EQ-bvcg1Hhl zo9ixC6IYXQrh_Fp2DXi1Td$8=YwWeJ#bw+olaC|Upbp8n=9T@`CK~7BO+d6SP@=tC zAexObI}+kRQKscNwu$7lhLTKQw>4BWYg+r~>u_1tz0b!q+o}%vvhURExi%p==Wzl; z{wyWrC4rEdWxCu&V-LQGx;aCI+jC?ZuXk&Gkr1OK^F~R$9ZxA)_@G5f1Gdp8bS`tkGiu z;T%|qK9c>TJ!hA=NU`NSsp+3=@c1I7;F>@|93vK@bX7*}s$m+ao5sdTlqMjbVQMlq zf0UU1^t?3gNW=69#*FD7H(a{7iZhG1(sl)iqhLE9^VoGhNKLmYhaf@zS19u90`i_> z_?7`XL{%N6bC5`f<1D$7t)X_@clCBy==a1p;iSC`QU%D5HRP@oEuh0X@l_9Spa3e+ zroi~uDaIRum2Ef@aMe)-2#f=qlBVj|uIXz~Pb&N&z5S0Xf&+;s5y3^JVS?B|_Wj(@ zK-Bke^1wg=C9e@m9zGwd&B*mnX*!pw2+KGChtJPPfzG&e@i!=KpAl*6+o)r(GzZKR z*a$572sWYs&PPrOyK{mzfg<1c=QYrior4eVvij6qZ-;9e<5LuE#9kg;3c!(t%*pL#0?Y%S1b)y}!Tdm|c_ zuVd$ozm?DDKh&Dyangxn!xO2zn<8@;|5{1lEcQ$XpHLbt@6pT|Gu?`sVrR}y zJ$HXV=kBU7ce?DzWP@L*cqY^ocABFW1zQ7rM66Za@au>q%>s~0JRA_lW_M~3|A>Nk zO#o3gRAiJ42PtF(wKUXK2M7#$K0*&DzJoR;d-JO;fdUyJM<6hLa$}y%KG^||gH>Ld zM)_lk#(M-bY{X(M95ak!@ldm|6k?ydL;lgoXn#Z!w^Dtdi zxi$r)X`cmchKD3xQB8Y^6MiLL^w^2W^l0LgUu&F0WyfagcYU6e1HD&(9XkLtL~`H;D3&A} z2=O3Bp(A$%8B;yas3TA9hbU=rBpG4Gmv^oEZ3^@I1ekpfyUlE5YH?&G!cwp@Q3dTe z7S618WZ8A9Va5sFr(XQTD`y%qZX|o?Y5Z(7Mlb$JqcziLY(3q0^tWG2KTmJGKtIpC z>SOft?Dr1n=VQ-pu+QIIx$FMN9)IATS9;fv_HHaF#w)M{ET}8N<=NALhkmhdDzsu&_{%CZ#h?E3QYF7-{>i!&&l10rjWPOh1 zcE}IKjec|wFSC757}}eQ%qOGc5M|+2h(#I3v+z2>Lq0q_I(79d=TmqX1*Z^H9 zrLZXq51W|-o6EuG&ZV&ZXabuT!B!hIoMu0_hhrcX)rfpbL_KU_iXgNT>M1c$wW#ND z5P}1K|406x2KH?cR*@;+$P)Wd*au@K*<`;g_TkbGCHnB_+baE=%HCF4Un=WFjh<<& zNyMM5OVwt7bl8Xgdcv-Y*oU|`*mw30P^5t1)4_xEuk^^t1#lA6`3ZLz{9{GUKK6vi z*|!x8%1a)jApMP7+VyOqh*h&UYLlqut*~bDAR6YCH7RJuf$Phux;%&;p*x3m%jYi`Y zuluRz8;>;@g9Yp(q&&+QQe(`jF=o{mvucb>HO8eHV|>k{Z)-mLU>a7ZKXk_UI_pbk zed*CN)``wK(b?NNgG6VLu+a?GiNWA9*xLqs+hA`SOkNBoF9s7qgLP@JzF2#c^<}cY zOxBkfJ!5^DtS^)GWeO|;H7ZJ(Da@jtWf9Y-3(X=HnpNi(KPP&2My-fdqjf0DE><+* z(c>(;SfS8)meK7nv1<%&~!l&aG_cu0SFyOF>tlftBstbp-ln1m1W^j#B!{2VRntN z&LhYf3!7t|2XEt6LnZrImh`u9tH#)4{$)GM|2Qz0J!fOvS)$^21>yZHX0j}GxWRFO zLT3JDSz!&LwOkq-K~{wkvfPQX9N~GEJ&{jf103!7qm|Kf3u6OlQloK~|C#$5k2AB! zqDz5I|8?lUF8!C8HB85c_Y$lY5}*-_On>UaFH`ts^S^Ky3-=y&IrF~^;g`^hV)4&Z zN%$o`6UNhomYTx9hw2t?bW~k)zYL+JA+$6EYz^@}VRTdG`!t1Ka0w1Ko+YEgxMnB_w2tz>{jCPi{q z2pr*QmO(w?X;#7nwIz>9ccgeCM$N)3YfaOd37fLiRJ^9FG!1-ro3hcA&8BQMRk^7u zO;v5GK~uGxs?#*|rr|aEokaN(~z1v{+F9NUbmWt-PHA_ZZ%D*Y3fbWZko=F zF{3paGoSeBmBs^&gq~RQsByoLHSQVt8LgsOiwVcXF8(DxjMyuB7)@~WD`J}HSHv{z zmnyWxre(?s(X)u5qF<3nM!!rRPL2N|yoG~IxgK?4G@L%cKNH5rsr39aVQf7T5C(~E z&FC}oj5^~qO>@Sb@tTS|g9+1`s?k)U zjeq|$Ki#;eA&eM_4;#Y}u)xM04mMMO+Y&IeES@kzm$uAiGf~X1AOV2|m3^4(!)j_~ zQv+LRO~Y>*W>Yt3z*68_?;aiJKSj>39{&h&%vbyaK?OWsJhop(dyB{R3kus8{(o%0 seD2?T!|j(iEa*qkEv$2Ckj|eAad7|&fcF=1{5DctlG1s3|2z5r0lzu~FaQ7m From 021c70143b81a6d948640a420444aa65e927dba8 Mon Sep 17 00:00:00 2001 From: Mahmoud Almahroum <84918112+mahmoudal993@users.noreply.github.com> Date: Fri, 10 Mar 2023 14:28:40 +0000 Subject: [PATCH 009/126] ENT-8826 Upgrade Liquibase to latest version - more review feedback (#7279) --- .../corda/nodeapi/internal/persistence/SchemaMigration.kt | 8 +++----- .../migration/IdenityServiceKeyRotationMigrationTest.kt | 2 +- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/SchemaMigration.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/SchemaMigration.kt index b126c95928..11f370c0aa 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/SchemaMigration.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/SchemaMigration.kt @@ -154,25 +154,23 @@ open class SchemaMigration( if (path == dynamicInclude) { // Return the json as a stream. - val inputStream = getPathAsStream() val resource = object : URIResource(path, URI(path)) { override fun openInputStream(): InputStream { - return inputStream + return getPathAsStream() } } return Collections.singletonList(resource) } - // Take 1 resource due to LiquidBase find duplicate files which throws an error + // Take 1 resource due to Liquibase find duplicate files which throws an error return super.getAll(path).take(1) } override fun get(path: String?): Resource { if (path == dynamicInclude) { // Return the json as a stream. - val inputStream = getPathAsStream() return object : URIResource(path, URI(path)) { override fun openInputStream(): InputStream { - return inputStream + return getPathAsStream() } } } diff --git a/node/src/test/kotlin/net/corda/node/migration/IdenityServiceKeyRotationMigrationTest.kt b/node/src/test/kotlin/net/corda/node/migration/IdenityServiceKeyRotationMigrationTest.kt index f3364980a4..68a1347db7 100644 --- a/node/src/test/kotlin/net/corda/node/migration/IdenityServiceKeyRotationMigrationTest.kt +++ b/node/src/test/kotlin/net/corda/node/migration/IdenityServiceKeyRotationMigrationTest.kt @@ -86,7 +86,7 @@ class IdenityServiceKeyRotationMigrationTest { Liquibase("migration/node-core.changelog-v20.xml", object : ClassLoaderResourceAccessor() { override fun getAll(path: String?): List { - return super.getAll(path).take(1).toList() + return if(path != null) super.getAll(path).take(1).toList() else emptyList() } }, liquibaseDB).update(Contexts().toString()) From d2900d54ababcc719bd3df0d6a2bd2d1b547c291 Mon Sep 17 00:00:00 2001 From: Jose Coll Date: Wed, 22 Mar 2023 10:47:51 +0000 Subject: [PATCH 010/126] ENT-6875 Two Phase Finality Flow - improve ledger consistency & recoverability (#7290) --- .../coretests/flows/FinalityFlowTests.kt | 326 +++++++++++++++++- .../net/corda/core/flows/FinalityFlow.kt | 216 ++++++++++-- .../net/corda/core/flows/FlowTransaction.kt | 37 ++ .../core/flows/ReceiveTransactionFlow.kt | 18 +- .../core/internal/PlatformVersionSwitches.kt | 1 + .../core/internal/ResolveTransactionsFlow.kt | 12 +- .../core/internal/ServiceHubCoreInternal.kt | 21 ++ .../flows/FlowReloadAfterCheckpointTest.kt | 4 +- .../services/statemachine/FlowHospitalTest.kt | 146 +++++++- .../services/statemachine/OldFinalityFlow.kt | 306 ++++++++++++++++ .../vault/VaultObserverExceptionTest.kt | 52 +-- .../node/services/DbTransactionsResolver.kt | 5 +- .../node/services/api/ServiceHubInternal.kt | 96 +++++- .../persistence/DBTransactionStorage.kt | 213 ++++++++++-- .../statemachine/StaffedFlowHospital.kt | 31 ++ .../node/utilities/AppendOnlyPersistentMap.kt | 10 +- .../migration/node-core.changelog-master.xml | 3 +- .../migration/node-core.changelog-v23.xml | 12 + .../migration/node-core.changelog-v24.xml | 25 ++ .../node/messaging/TwoPartyTradeFlowTests.kt | 67 +++- .../node/migration/VaultStateMigrationTest.kt | 3 +- .../persistence/DBTransactionStorageTests.kt | 198 ++++++++++- .../statemachine/RetryFlowMockTest.kt | 2 +- .../net/corda/testing/node/MockServices.kt | 42 ++- .../node/internal/InternalMockNetwork.kt | 15 +- .../node/internal/MockTransactionStorage.kt | 29 +- .../kotlin/net/corda/testing/dsl/TestDSL.kt | 7 + 27 files changed, 1739 insertions(+), 158 deletions(-) create mode 100644 core/src/main/kotlin/net/corda/core/flows/FlowTransaction.kt create mode 100644 node/src/integration-test/kotlin/net/corda/node/services/statemachine/OldFinalityFlow.kt create mode 100644 node/src/main/resources/migration/node-core.changelog-v23.xml create mode 100644 node/src/main/resources/migration/node-core.changelog-v24.xml diff --git a/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt b/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt index 1d13b53c66..0fac4a11ce 100644 --- a/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt +++ b/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt @@ -1,30 +1,82 @@ package net.corda.coretests.flows +import co.paralleluniverse.fibers.Suspendable import com.natpryce.hamkrest.and import com.natpryce.hamkrest.assertion.assertThat +import net.corda.core.contracts.Amount +import net.corda.core.contracts.PartyAndReference +import net.corda.core.contracts.StateAndContract +import net.corda.core.contracts.StateAndRef +import net.corda.core.contracts.TransactionVerificationException +import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.TransactionSignature import net.corda.core.flows.FinalityFlow +import net.corda.core.flows.FlowException +import net.corda.core.flows.FlowLogic +import net.corda.core.flows.FlowSession +import net.corda.core.flows.FlowTransactionMetadata +import net.corda.core.flows.InitiatedBy +import net.corda.core.flows.InitiatingFlow +import net.corda.core.flows.NotaryError +import net.corda.core.flows.NotaryException +import net.corda.core.flows.NotarySigCheck +import net.corda.core.flows.ReceiveFinalityFlow +import net.corda.core.flows.ReceiveTransactionFlow +import net.corda.core.flows.StartableByRPC +import net.corda.core.flows.TransactionStatus +import net.corda.core.flows.UnexpectedFlowEndException import net.corda.core.identity.Party +import net.corda.core.internal.FetchDataFlow +import net.corda.core.internal.PLATFORM_VERSION +import net.corda.core.internal.PlatformVersionSwitches +import net.corda.core.internal.ServiceHubCoreInternal +import net.corda.core.node.StatesToRecord import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder +import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.getOrThrow -import net.corda.coretests.flows.WithFinality.FinalityInvoker -import net.corda.finance.POUNDS -import net.corda.finance.contracts.asset.Cash -import net.corda.finance.issuedBy -import net.corda.testing.core.* +import net.corda.core.utilities.unwrap import net.corda.coretesting.internal.matchers.flow.willReturn import net.corda.coretesting.internal.matchers.flow.willThrow -import net.corda.testing.node.internal.* +import net.corda.coretests.flows.WithFinality.FinalityInvoker +import net.corda.finance.GBP +import net.corda.finance.POUNDS +import net.corda.finance.contracts.asset.Cash +import net.corda.finance.flows.CashIssueFlow +import net.corda.finance.flows.CashPaymentFlow +import net.corda.finance.issuedBy +import net.corda.testing.contracts.DummyContract +import net.corda.testing.core.ALICE_NAME +import net.corda.testing.core.BOB_NAME +import net.corda.testing.core.CHARLIE_NAME +import net.corda.testing.core.TestIdentity +import net.corda.testing.core.singleIdentity +import net.corda.testing.node.internal.CustomCordapp +import net.corda.testing.node.internal.DUMMY_CONTRACTS_CORDAPP +import net.corda.testing.node.internal.FINANCE_CONTRACTS_CORDAPP +import net.corda.testing.node.internal.FINANCE_WORKFLOWS_CORDAPP +import net.corda.testing.node.internal.InternalMockNetwork +import net.corda.testing.node.internal.InternalMockNodeParameters +import net.corda.testing.node.internal.MOCK_VERSION_INFO +import net.corda.testing.node.internal.TestCordappInternal +import net.corda.testing.node.internal.TestStartedNode +import net.corda.testing.node.internal.cordappWithPackages +import net.corda.testing.node.internal.enclosedCordapp import org.assertj.core.api.Assertions.assertThat import org.junit.After import org.junit.Test +import java.sql.SQLException +import java.util.Random +import kotlin.test.assertEquals +import kotlin.test.assertNull +import kotlin.test.fail class FinalityFlowTests : WithFinality { companion object { private val CHARLIE = TestIdentity(CHARLIE_NAME, 90).party } - override val mockNet = InternalMockNetwork(cordappsForAllNodes = listOf(FINANCE_CONTRACTS_CORDAPP, enclosedCordapp(), + override val mockNet = InternalMockNetwork(cordappsForAllNodes = setOf(FINANCE_CONTRACTS_CORDAPP, FINANCE_WORKFLOWS_CORDAPP, DUMMY_CONTRACTS_CORDAPP, enclosedCordapp(), CustomCordapp(targetPlatformVersion = 3, classes = setOf(FinalityFlow::class.java)))) private val aliceNode = makeNode(ALICE_NAME) @@ -62,7 +114,7 @@ class FinalityFlowTests : WithFinality { val stx = aliceNode.issuesCashTo(oldBob) @Suppress("DEPRECATION") aliceNode.startFlowAndRunNetwork(FinalityFlow(stx)).resultFuture.getOrThrow() - assertThat(oldBob.services.validatedTransactions.getTransaction(stx.id)).isNotNull() + assertThat(oldBob.services.validatedTransactions.getTransaction(stx.id)).isNotNull } @Test(timeout=300_000) @@ -76,12 +128,262 @@ class FinalityFlowTests : WithFinality { oldRecipients = setOf(oldBob.info.singleIdentity()) )).resultFuture resultFuture.getOrThrow() - assertThat(newCharlie.services.validatedTransactions.getTransaction(stx.id)).isNotNull() - assertThat(oldBob.services.validatedTransactions.getTransaction(stx.id)).isNotNull() + assertThat(newCharlie.services.validatedTransactions.getTransaction(stx.id)).isNotNull + assertThat(oldBob.services.validatedTransactions.getTransaction(stx.id)).isNotNull } - private fun createBob(cordapps: List = emptyList()): TestStartedNode { - return mockNet.createNode(InternalMockNodeParameters(legalName = BOB_NAME, additionalCordapps = cordapps)) + @Test(timeout=300_000) + fun `two phase finality flow transaction`() { + val bobNode = createBob(platformVersion = PlatformVersionSwitches.TWO_PHASE_FINALITY) + + val stx = aliceNode.startFlow(CashIssueFlow(Amount(1000L, GBP), OpaqueBytes.of(1), notary)).resultFuture.getOrThrow().stx + aliceNode.startFlowAndRunNetwork(CashPaymentFlow(Amount(100, GBP), bobNode.info.singleIdentity())).resultFuture.getOrThrow() + + assertThat(aliceNode.services.validatedTransactions.getTransaction(stx.id)).isNotNull + assertThat(bobNode.services.validatedTransactions.getTransaction(stx.id)).isNotNull + } + + @Test(timeout=300_000) + fun `two phase finality flow initiator to pre-2PF peer`() { + val bobNode = createBob(platformVersion = PlatformVersionSwitches.TWO_PHASE_FINALITY - 1) + + val stx = aliceNode.startFlow(CashIssueFlow(Amount(1000L, GBP), OpaqueBytes.of(1), notary)).resultFuture.getOrThrow().stx + aliceNode.startFlowAndRunNetwork(CashPaymentFlow(Amount(100, GBP), bobNode.info.singleIdentity())).resultFuture.getOrThrow() + + assertThat(aliceNode.services.validatedTransactions.getTransaction(stx.id)).isNotNull + assertThat(bobNode.services.validatedTransactions.getTransaction(stx.id)).isNotNull + } + + @Test(timeout=300_000) + fun `pre-2PF initiator to two phase finality flow peer`() { + val bobNode = createBob(platformVersion = PlatformVersionSwitches.TWO_PHASE_FINALITY - 1) + + val stx = bobNode.startFlow(CashIssueFlow(Amount(1000L, GBP), OpaqueBytes.of(1), notary)).resultFuture.getOrThrow().stx + bobNode.startFlowAndRunNetwork(CashPaymentFlow(Amount(100, GBP), aliceNode.info.singleIdentity())).resultFuture.getOrThrow() + + assertThat(aliceNode.services.validatedTransactions.getTransaction(stx.id)).isNotNull + assertThat(bobNode.services.validatedTransactions.getTransaction(stx.id)).isNotNull + } + + @Test(timeout=300_000) + fun `two phase finality flow double spend transaction`() { + val bobNode = createBob(platformVersion = PlatformVersionSwitches.TWO_PHASE_FINALITY) + + val ref = aliceNode.startFlowAndRunNetwork(IssueFlow(notary)).resultFuture.getOrThrow() + val stx = aliceNode.startFlowAndRunNetwork(SpendFlow(ref, bobNode.info.singleIdentity())).resultFuture.getOrThrow() + + val (_, txnStatusAlice) = aliceNode.services.validatedTransactions.getTransactionInternal(stx.id) ?: fail() + assertEquals(TransactionStatus.VERIFIED, txnStatusAlice) + val (_, txnStatusBob) = bobNode.services.validatedTransactions.getTransactionInternal(stx.id) ?: fail() + assertEquals(TransactionStatus.VERIFIED, txnStatusBob) + + try { + aliceNode.startFlowAndRunNetwork(SpendFlow(ref, bobNode.info.singleIdentity())).resultFuture.getOrThrow() + } + catch (e: NotaryException) { + val stxId = (e.error as NotaryError.Conflict).txId + val (_, txnDsStatusAlice) = aliceNode.services.validatedTransactions.getTransactionInternal(stxId) ?: fail() + assertEquals(TransactionStatus.MISSING_NOTARY_SIG, txnDsStatusAlice) + val (_, txnDsStatusBob) = bobNode.services.validatedTransactions.getTransactionInternal(stxId) ?: fail() + assertEquals(TransactionStatus.MISSING_NOTARY_SIG, txnDsStatusBob) + } + } + + @Test(timeout=300_000) + fun `two phase finality flow double spend transaction from pre-2PF initiator`() { + val bobNode = createBob(platformVersion = PlatformVersionSwitches.TWO_PHASE_FINALITY - 1) + + val ref = bobNode.startFlowAndRunNetwork(IssueFlow(notary)).resultFuture.getOrThrow() + val stx = bobNode.startFlowAndRunNetwork(SpendFlow(ref, aliceNode.info.singleIdentity())).resultFuture.getOrThrow() + + val (_, txnStatusAlice) = aliceNode.services.validatedTransactions.getTransactionInternal(stx.id) ?: fail() + assertEquals(TransactionStatus.VERIFIED, txnStatusAlice) + val (_, txnStatusBob) = bobNode.services.validatedTransactions.getTransactionInternal(stx.id) ?: fail() + assertEquals(TransactionStatus.VERIFIED, txnStatusBob) + + try { + bobNode.startFlowAndRunNetwork(SpendFlow(ref, aliceNode.info.singleIdentity())).resultFuture.getOrThrow() + } + catch (e: NotaryException) { + val stxId = (e.error as NotaryError.Conflict).txId + assertNull(bobNode.services.validatedTransactions.getTransactionInternal(stxId)) + assertNull(aliceNode.services.validatedTransactions.getTransactionInternal(stxId)) + } + } + + @Test(timeout=300_000) + fun `two phase finality flow double spend transaction to pre-2PF peer`() { + val bobNode = createBob(platformVersion = PlatformVersionSwitches.TWO_PHASE_FINALITY - 1) + + val ref = aliceNode.startFlowAndRunNetwork(IssueFlow(notary)).resultFuture.getOrThrow() + val stx = aliceNode.startFlowAndRunNetwork(SpendFlow(ref, bobNode.info.singleIdentity())).resultFuture.getOrThrow() + + val (_, txnStatusAlice) = aliceNode.services.validatedTransactions.getTransactionInternal(stx.id) ?: fail() + assertEquals(TransactionStatus.VERIFIED, txnStatusAlice) + val (_, txnStatusBob) = bobNode.services.validatedTransactions.getTransactionInternal(stx.id) ?: fail() + assertEquals(TransactionStatus.VERIFIED, txnStatusBob) + + try { + aliceNode.startFlowAndRunNetwork(SpendFlow(ref, bobNode.info.singleIdentity())).resultFuture.getOrThrow() + } + catch (e: NotaryException) { + val stxId = (e.error as NotaryError.Conflict).txId + val (_, txnDsStatusAlice) = aliceNode.services.validatedTransactions.getTransactionInternal(stxId) ?: fail() + assertEquals(TransactionStatus.MISSING_NOTARY_SIG, txnDsStatusAlice) + assertNull(bobNode.services.validatedTransactions.getTransactionInternal(stxId)) + } + } + + @Test(timeout=300_000) + fun `two phase finality flow speedy spender`() { + val bobNode = createBob(platformVersion = PlatformVersionSwitches.TWO_PHASE_FINALITY) + + val ref = aliceNode.startFlowAndRunNetwork(IssueFlow(notary)).resultFuture.getOrThrow() + val notarisedStxn1 = aliceNode.startFlowAndRunNetwork(SpeedySpendFlow(ref, bobNode.info.singleIdentity())).resultFuture.getOrThrow() + + val (_, txnStatusAlice) = aliceNode.services.validatedTransactions.getTransactionInternal(notarisedStxn1.id) ?: fail() + assertEquals(TransactionStatus.VERIFIED, txnStatusAlice) + val (_, txnStatusBob) = bobNode.services.validatedTransactions.getTransactionInternal(notarisedStxn1.id) ?: fail() + assertEquals(TransactionStatus.MISSING_NOTARY_SIG, txnStatusBob) + + // now lets attempt a new spend with the new output of the previous transaction + val newStateRef = notarisedStxn1.coreTransaction.outRef(1) + val notarisedStxn2 = aliceNode.startFlowAndRunNetwork(SpeedySpendFlow(newStateRef, bobNode.info.singleIdentity())).resultFuture.getOrThrow() + + // the original transaction is now finalised at Bob (despite the original flow not completing) because Bob resolved the + // original transaction from Alice in the second transaction (and Alice had already notarised and finalised the original transaction) + val (_, txnStatusBobAgain) = bobNode.services.validatedTransactions.getTransactionInternal(notarisedStxn1.id) ?: fail() + assertEquals(TransactionStatus.VERIFIED, txnStatusBobAgain) + + val (_, txnStatusAlice2) = aliceNode.services.validatedTransactions.getTransactionInternal(notarisedStxn2.id) ?: fail() + assertEquals(TransactionStatus.VERIFIED, txnStatusAlice2) + val (_, txnStatusBob2) = bobNode.services.validatedTransactions.getTransactionInternal(notarisedStxn2.id) ?: fail() + assertEquals(TransactionStatus.MISSING_NOTARY_SIG, txnStatusBob2) + + // Validate attempt at flow finalisation by Bob has no effect on outcome. + val finaliseStxn1 = bobNode.startFlowAndRunNetwork(FinaliseSpeedySpendFlow(notarisedStxn1.id, notarisedStxn1.sigs)).resultFuture.getOrThrow() + val (_, txnStatusBobYetAgain) = bobNode.services.validatedTransactions.getTransactionInternal(finaliseStxn1.id) ?: fail() + assertEquals(TransactionStatus.VERIFIED, txnStatusBobYetAgain) + } + + @StartableByRPC + class IssueFlow(val notary: Party) : FlowLogic>() { + + @Suspendable + override fun call(): StateAndRef { + val partyAndReference = PartyAndReference(ourIdentity, OpaqueBytes.of(1)) + val txBuilder = DummyContract.generateInitial(Random().nextInt(), notary, partyAndReference) + val signedTransaction = serviceHub.signInitialTransaction(txBuilder, ourIdentity.owningKey) + val notarised = subFlow(FinalityFlow(signedTransaction, emptySet())) + return notarised.coreTransaction.outRef(0) + } + } + + @StartableByRPC + @InitiatingFlow + class SpendFlow(private val stateAndRef: StateAndRef, private val newOwner: Party) : FlowLogic() { + + @Suspendable + override fun call(): SignedTransaction { + val txBuilder = DummyContract.move(stateAndRef, newOwner) + val signedTransaction = serviceHub.signInitialTransaction(txBuilder, ourIdentity.owningKey) + val sessionWithCounterParty = initiateFlow(newOwner) + sessionWithCounterParty.sendAndReceive("initial-message") + return subFlow(FinalityFlow(signedTransaction, setOf(sessionWithCounterParty))) + } + } + + @InitiatedBy(SpendFlow::class) + class AcceptSpendFlow(private val otherSide: FlowSession) : FlowLogic() { + + @Suspendable + override fun call() { + otherSide.receive() + otherSide.send("initial-response") + + subFlow(ReceiveFinalityFlow(otherSide)) + } + } + + /** + * This flow allows an Initiator to race ahead of a Receiver when using Two Phase Finality. + * The initiator transaction will be finalised, so output states can be used in a follow-up transaction. + * The receiver transaction will not be finalised, causing ledger inconsistency. + */ + @StartableByRPC + @InitiatingFlow + class SpeedySpendFlow(private val stateAndRef: StateAndRef, private val newOwner: Party) : FlowLogic() { + + @Suspendable + override fun call(): SignedTransaction { + val newState = StateAndContract(DummyContract.SingleOwnerState(99999, ourIdentity), DummyContract.PROGRAM_ID) + val txBuilder = DummyContract.move(stateAndRef, newOwner).withItems(newState) + val signedTransaction = serviceHub.signInitialTransaction(txBuilder, ourIdentity.owningKey) + val sessionWithCounterParty = initiateFlow(newOwner) + try { + subFlow(FinalityFlow(signedTransaction, setOf(sessionWithCounterParty))) + } + catch (e: FinalisationFailedException) { + // expected (transaction has been notarised by Initiator) + return e.notarisedTxn + } + return signedTransaction + } + } + + @InitiatedBy(SpeedySpendFlow::class) + class AcceptSpeedySpendFlow(private val otherSideSession: FlowSession) : FlowLogic() { + + @Suspendable + override fun call(): SignedTransaction { + // Mimic ReceiveFinalityFlow but fail to finalise + try { + val stx = subFlow(ReceiveTransactionFlow(otherSideSession, + checkSufficientSignatures = false, statesToRecord = StatesToRecord.ONLY_RELEVANT, deferredAck = true)) + require(NotarySigCheck.needsNotarySignature(stx)) + logger.info("Peer recording transaction without notary signature.") + (serviceHub as ServiceHubCoreInternal).recordUnnotarisedTransaction(stx, + FlowTransactionMetadata(otherSideSession.counterparty.name, StatesToRecord.ONLY_RELEVANT)) + otherSideSession.send(FetchDataFlow.Request.End) // Finish fetching data (overrideAutoAck) + logger.info("Peer recorded transaction without notary signature.") + + val notarySignatures = otherSideSession.receive>() + .unwrap { it } + logger.info("Peer received notarised signature.") + (serviceHub as ServiceHubCoreInternal).finalizeTransactionWithExtraSignatures(stx + notarySignatures, notarySignatures, StatesToRecord.ONLY_RELEVANT) + throw FinalisationFailedException(stx + notarySignatures) + } + catch (e: SQLException) { + logger.error("Peer failure upon recording or finalising transaction: $e") + otherSideSession.send(FetchDataFlow.Request.End) // Finish fetching data (overrideAutoAck) + throw UnexpectedFlowEndException("Peer failure upon recording or finalising transaction.", e.cause) + } + catch (uae: TransactionVerificationException.UntrustedAttachmentsException) { + logger.error("Peer failure upon receiving transaction: $uae") + otherSideSession.send(FetchDataFlow.Request.End) // Finish fetching data (overrideAutoAck) + throw uae + } + } + } + + class FinaliseSpeedySpendFlow(val id: SecureHash, val sigs: List) : FlowLogic() { + + @Suspendable + override fun call(): SignedTransaction { + // Mimic ReceiveFinalityFlow finalisation + val stx = serviceHub.validatedTransactions.getTransaction(id) ?: throw FlowException("Missing transaction: $id") + (serviceHub as ServiceHubCoreInternal).finalizeTransactionWithExtraSignatures(stx + sigs, sigs, StatesToRecord.ONLY_RELEVANT) + logger.info("Peer finalised transaction with notary signature.") + + return stx + sigs + } + } + + class FinalisationFailedException(val notarisedTxn: SignedTransaction) : FlowException("Failed to finalise transaction with notary signature.") + + private fun createBob(cordapps: List = emptyList(), platformVersion: Int = PLATFORM_VERSION): TestStartedNode { + return mockNet.createNode(InternalMockNodeParameters(legalName = BOB_NAME, additionalCordapps = cordapps, + version = MOCK_VERSION_INFO.copy(platformVersion = platformVersion))) } private fun TestStartedNode.issuesCashTo(recipient: TestStartedNode): SignedTransaction { diff --git a/core/src/main/kotlin/net/corda/core/flows/FinalityFlow.kt b/core/src/main/kotlin/net/corda/core/flows/FinalityFlow.kt index 7d5a1505c1..c44654caad 100644 --- a/core/src/main/kotlin/net/corda/core/flows/FinalityFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/FinalityFlow.kt @@ -3,9 +3,14 @@ package net.corda.core.flows import co.paralleluniverse.fibers.Suspendable import net.corda.core.CordaInternal import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.TransactionSignature import net.corda.core.crypto.isFulfilledBy +import net.corda.core.flows.NotarySigCheck.needsNotarySignature import net.corda.core.identity.Party import net.corda.core.identity.groupAbstractPartyByWellKnownParty +import net.corda.core.internal.FetchDataFlow +import net.corda.core.internal.PlatformVersionSwitches +import net.corda.core.internal.ServiceHubCoreInternal import net.corda.core.internal.pushToLoggingContext import net.corda.core.internal.telemetry.telemetryServiceInternal import net.corda.core.internal.warnOnce @@ -15,6 +20,7 @@ import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.debug +import net.corda.core.utilities.unwrap /** * Verifies the given transaction, then sends it to the named notary. If the notary agrees that the transaction @@ -133,10 +139,18 @@ class FinalityFlow private constructor(val transaction: SignedTransaction, override fun childProgressTracker() = NotaryFlow.Client.tracker() } - object BROADCASTING : ProgressTracker.Step("Broadcasting transaction to participants") + @Suppress("ClassNaming") + object RECORD_UNNOTARISED : ProgressTracker.Step("Recording un-notarised transaction locally") + @Suppress("ClassNaming") + object BROADCASTING_PRE_NOTARISATION : ProgressTracker.Step("Broadcasting un-notarised transaction") + @Suppress("ClassNaming") + object BROADCASTING_POST_NOTARISATION : ProgressTracker.Step("Broadcasting notary signature") + @Suppress("ClassNaming") + object FINALISING_TRANSACTION : ProgressTracker.Step("Finalising transaction locally") + object BROADCASTING : ProgressTracker.Step("Broadcasting notarised transaction to other participants") @JvmStatic - fun tracker() = ProgressTracker(NOTARISING, BROADCASTING) + fun tracker() = ProgressTracker(RECORD_UNNOTARISED, BROADCASTING_PRE_NOTARISATION, NOTARISING, BROADCASTING_POST_NOTARISATION, FINALISING_TRANSACTION, BROADCASTING) } @Suspendable @@ -155,7 +169,6 @@ class FinalityFlow private constructor(val transaction: SignedTransaction, // the point where subFlow is invoked, as that minimizes the checkpointing work to be done. // // Lookup the resolved transactions and use them to map each signed transaction to the list of participants. - // Then send to the notary if needed, record locally and distribute. transaction.pushToLoggingContext() logCommandData() @@ -173,32 +186,121 @@ class FinalityFlow private constructor(val transaction: SignedTransaction, } } - val notarised = notariseAndRecord() + // Recoverability + // As of platform version 13 we introduce a 2-phase finality protocol whereby + // - record un-notarised transaction locally and broadcast to external participants to record + // - notarise transaction + // - broadcast notary signature to external participants (finalise remotely) + // - finalise locally - progressTracker.currentStep = BROADCASTING + val (oldPlatformSessions, newPlatformSessions) = sessions.partition { + serviceHub.networkMapCache.getNodeByLegalName(it.counterparty.name)?.platformVersion!! < PlatformVersionSwitches.TWO_PHASE_FINALITY + } - if (newApi) { - oldV3Broadcast(notarised, oldParticipants.toSet()) - for (session in sessions) { + val useTwoPhaseFinality = serviceHub.myInfo.platformVersion >= PlatformVersionSwitches.TWO_PHASE_FINALITY + if (useTwoPhaseFinality && needsNotarySignature(transaction)) { + recordLocallyAndBroadcast(newPlatformSessions, transaction) + } + + val stxn = notariseOrRecord() + val notarySignatures = stxn.sigs - transaction.sigs.toSet() + if (notarySignatures.isNotEmpty()) { + if (useTwoPhaseFinality && newPlatformSessions.isNotEmpty()) { + broadcastSignaturesAndFinalise(newPlatformSessions, notarySignatures) + } + else { + progressTracker.currentStep = FINALISING_TRANSACTION + serviceHub.telemetryServiceInternal.span("${this::class.java.name}#finalizeTransactionWithExtraSignatures", flowLogic = this) { + (serviceHub as ServiceHubCoreInternal).finalizeTransactionWithExtraSignatures(transaction, notarySignatures, statesToRecord) + logger.info("Finalised transaction locally.") + } + } + } + + if (!useTwoPhaseFinality || !needsNotarySignature(transaction)) { + broadcastToOtherParticipants(externalTxParticipants, newPlatformSessions + oldPlatformSessions, stxn) + } else if (useTwoPhaseFinality && oldPlatformSessions.isNotEmpty()) { + broadcastToOtherParticipants(externalTxParticipants, oldPlatformSessions, stxn) + } + + return stxn + } + + @Suspendable + private fun recordLocallyAndBroadcast(sessions: Collection, tx: SignedTransaction) { + serviceHub.telemetryServiceInternal.span("${this::class.java.name}#recordLocallyAndBroadcast", flowLogic = this) { + recordUnnotarisedTransaction(tx) + logger.info("Recorded transaction without notary signature locally.") + progressTracker.currentStep = BROADCASTING_PRE_NOTARISATION + sessions.forEach { session -> try { - subFlow(SendTransactionFlow(session, notarised)) - logger.info("Party ${session.counterparty} received the transaction.") + logger.debug { "Sending transaction to party $session." } + subFlow(SendTransactionFlow(session, tx)) } catch (e: UnexpectedFlowEndException) { throw UnexpectedFlowEndException( - "${session.counterparty} has finished prematurely and we're trying to send them the finalised transaction. " + + "${session.counterparty} has finished prematurely and we're trying to send them a transaction without notary signature. " + "Did they forget to call ReceiveFinalityFlow? (${e.message})", e.cause, e.originalErrorId ) } } - } else { - oldV3Broadcast(notarised, (externalTxParticipants + oldParticipants).toSet()) } + } - logger.info("All parties received the transaction successfully.") + @Suspendable + private fun broadcastSignaturesAndFinalise(sessions: Collection, notarySignatures: List) { + progressTracker.currentStep = BROADCASTING_POST_NOTARISATION + serviceHub.telemetryServiceInternal.span("${this::class.java.name}#broadcastSignaturesAndFinalise", flowLogic = this) { + logger.info("Transaction notarised and broadcasting notary signature.") + sessions.forEach { session -> + try { + logger.debug { "Sending notary signature to party $session." } + session.send(notarySignatures) + // remote will finalise txn with notary signature + } catch (e: UnexpectedFlowEndException) { + throw UnexpectedFlowEndException( + "${session.counterparty} has finished prematurely and we're trying to send them the notary signature. " + + "Did they forget to call ReceiveFinalityFlow? (${e.message})", + e.cause, + e.originalErrorId + ) + } + } + progressTracker.currentStep = FINALISING_TRANSACTION + serviceHub.telemetryServiceInternal.span("${this::class.java.name}#finalizeTransactionWithExtraSignatures", flowLogic = this) { + (serviceHub as ServiceHubCoreInternal).finalizeTransactionWithExtraSignatures(transaction, notarySignatures, statesToRecord) + logger.info("Finalised transaction locally with notary signature.") + } + } + } - return notarised + @Suspendable + private fun broadcastToOtherParticipants(externalTxParticipants: Set, sessions: Collection, tx: SignedTransaction) { + if (externalTxParticipants.isEmpty() && sessions.isEmpty() && oldParticipants.isEmpty()) return + progressTracker.currentStep = BROADCASTING + serviceHub.telemetryServiceInternal.span("${this::class.java.name}#broadcastToOtherParticipants", flowLogic = this) { + logger.info("Broadcasting complete transaction to other participants.") + if (newApi) { + oldV3Broadcast(tx, oldParticipants.toSet()) + for (session in sessions) { + try { + logger.debug { "Sending transaction to party $session." } + subFlow(SendTransactionFlow(session, tx)) + } catch (e: UnexpectedFlowEndException) { + throw UnexpectedFlowEndException( + "${session.counterparty} has finished prematurely and we're trying to send them the finalised transaction. " + + "Did they forget to call ReceiveFinalityFlow? (${e.message})", + e.cause, + e.originalErrorId + ) + } + } + } else { + oldV3Broadcast(tx, (externalTxParticipants + oldParticipants).toSet()) + } + logger.info("Broadcasted complete transaction to other participants.") + } } @Suspendable @@ -221,22 +323,39 @@ class FinalityFlow private constructor(val transaction: SignedTransaction, } @Suspendable - private fun notariseAndRecord(): SignedTransaction { - serviceHub.telemetryServiceInternal.span("${this::class.java.name}#notariseAndRecord", flowLogic = this) { - val notarised = if (needsNotarySignature(transaction)) { + private fun recordTransactionLocally(tx: SignedTransaction): SignedTransaction { + serviceHub.telemetryServiceInternal.span("${this::class.java.name}#recordTransactionLocally", flowLogic = this) { + serviceHub.recordTransactions(statesToRecord, listOf(tx)) + logger.info("Recorded transaction locally.") + return tx + } + } + + @Suspendable + private fun recordUnnotarisedTransaction(tx: SignedTransaction): SignedTransaction { + progressTracker.currentStep = RECORD_UNNOTARISED + serviceHub.telemetryServiceInternal.span("${this::class.java.name}#recordUnnotarisedTransaction", flowLogic = this) { + (serviceHub as ServiceHubCoreInternal).recordUnnotarisedTransaction(tx, + FlowTransactionMetadata( + serviceHub.myInfo.legalIdentities.first().name, + statesToRecord, + sessions.map { it.counterparty.name }.toSet())) + return tx + } + } + + @Suspendable + private fun notariseOrRecord(): SignedTransaction { + serviceHub.telemetryServiceInternal.span("${this::class.java.name}#notariseOrRecord", flowLogic = this) { + return if (needsNotarySignature(transaction)) { progressTracker.currentStep = NOTARISING val notarySignatures = subFlow(NotaryFlow.Client(transaction, skipVerification = true)) transaction + notarySignatures } else { - logger.info("No need to notarise this transaction.") + logger.info("No need to notarise this transaction. Recording locally.") + recordTransactionLocally(transaction) transaction } - serviceHub.telemetryServiceInternal.span("${this::class.java.name}#notariseAndRecord:recordTransactions", flowLogic = this) { - logger.info("Recording transaction locally.") - serviceHub.recordTransactions(statesToRecord, listOf(notarised)) - logger.info("Recorded transaction locally successfully.") - } - return notarised } } @@ -268,6 +387,20 @@ class FinalityFlow private constructor(val transaction: SignedTransaction, } } +object NotarySigCheck { + fun needsNotarySignature(stx: SignedTransaction): Boolean { + val wtx = stx.tx + val needsNotarisation = wtx.inputs.isNotEmpty() || wtx.references.isNotEmpty() || wtx.timeWindow != null + return needsNotarisation && hasNoNotarySignature(stx) + } + + private fun hasNoNotarySignature(stx: SignedTransaction): Boolean { + val notaryKey = stx.tx.notary?.owningKey + val signers = stx.sigs.asSequence().map { it.by }.toSet() + return notaryKey?.isFulfilledBy(signers) != true + } +} + /** * The receiving counterpart to [FinalityFlow]. * @@ -285,15 +418,34 @@ class FinalityFlow private constructor(val transaction: SignedTransaction, class ReceiveFinalityFlow @JvmOverloads constructor(private val otherSideSession: FlowSession, private val expectedTxId: SecureHash? = null, private val statesToRecord: StatesToRecord = ONLY_RELEVANT) : FlowLogic() { + @Suppress("ComplexMethod") @Suspendable override fun call(): SignedTransaction { - return subFlow(object : ReceiveTransactionFlow(otherSideSession, checkSufficientSignatures = true, statesToRecord = statesToRecord) { - override fun checkBeforeRecording(stx: SignedTransaction) { - require(expectedTxId == null || expectedTxId == stx.id) { - "We expected to receive transaction with ID $expectedTxId but instead got ${stx.id}. Transaction was" + - "not recorded and nor its states sent to the vault." - } + val stx = subFlow(ReceiveTransactionFlow(otherSideSession, checkSufficientSignatures = false, statesToRecord = statesToRecord, deferredAck = true)) + + val fromTwoPhaseFinalityNode = serviceHub.networkMapCache.getNodeByLegalName(otherSideSession.counterparty.name)?.platformVersion!! >= PlatformVersionSwitches.TWO_PHASE_FINALITY + if (fromTwoPhaseFinalityNode && needsNotarySignature(stx)) { + serviceHub.telemetryServiceInternal.span("${this::class.java.name}#recordUnnotarisedTransaction", flowLogic = this) { + logger.debug { "Peer recording transaction without notary signature." } + (serviceHub as ServiceHubCoreInternal).recordUnnotarisedTransaction(stx, + FlowTransactionMetadata(otherSideSession.counterparty.name, statesToRecord)) } - }) + otherSideSession.send(FetchDataFlow.Request.End) // Finish fetching data (deferredAck) + logger.info("Peer recorded transaction without notary signature. Waiting to receive notary signature.") + val notarySignatures = otherSideSession.receive>() + .unwrap { it } + serviceHub.telemetryServiceInternal.span("${this::class.java.name}#finalizeTransactionWithExtraSignatures", flowLogic = this) { + logger.debug { "Peer received notarised signature." } + (serviceHub as ServiceHubCoreInternal).finalizeTransactionWithExtraSignatures(stx, notarySignatures, statesToRecord) + logger.info("Peer finalised transaction with notary signature.") + } + } else { + serviceHub.telemetryServiceInternal.span("${this::class.java.name}#recordTransactions", flowLogic = this) { + serviceHub.recordTransactions(statesToRecord, setOf(stx)) + } + otherSideSession.send(FetchDataFlow.Request.End) // Finish fetching data (deferredAck) + logger.info("Peer successfully recorded received transaction.") + } + return stx } } diff --git a/core/src/main/kotlin/net/corda/core/flows/FlowTransaction.kt b/core/src/main/kotlin/net/corda/core/flows/FlowTransaction.kt new file mode 100644 index 0000000000..49ecae6151 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/flows/FlowTransaction.kt @@ -0,0 +1,37 @@ +package net.corda.core.flows + +import net.corda.core.identity.CordaX500Name +import net.corda.core.node.StatesToRecord +import net.corda.core.serialization.CordaSerializable +import java.time.Instant + +/** + * Flow data object representing key information required for recovery. + */ + +@CordaSerializable +data class FlowTransaction( + val stateMachineRunId: StateMachineRunId, + val txId: String, + val status: TransactionStatus, + val signatures: ByteArray?, + val timestamp: Instant, + val metadata: FlowTransactionMetadata?) { + + fun isInitiator(myCordaX500Name: CordaX500Name) = + this.metadata?.initiator == myCordaX500Name +} + +@CordaSerializable +data class FlowTransactionMetadata( + val initiator: CordaX500Name, + val statesToRecord: StatesToRecord? = StatesToRecord.ONLY_RELEVANT, + val peers: Set? = null +) + +@CordaSerializable +enum class TransactionStatus { + UNVERIFIED, + VERIFIED, + MISSING_NOTARY_SIG; +} \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/flows/ReceiveTransactionFlow.kt b/core/src/main/kotlin/net/corda/core/flows/ReceiveTransactionFlow.kt index 413f01db3f..4f5d04b6d9 100644 --- a/core/src/main/kotlin/net/corda/core/flows/ReceiveTransactionFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/ReceiveTransactionFlow.kt @@ -27,10 +27,20 @@ import java.security.SignatureException * @property otherSideSession session to the other side which is calling [SendTransactionFlow]. * @property checkSufficientSignatures if true checks all required signatures are present. See [SignedTransaction.verify]. * @property statesToRecord which transaction states should be recorded in the vault, if any. + * @property deferredAck if set then the caller of this flow is responsible for explicitly sending a FetchDataFlow.Request.End + * acknowledgement to indicate transaction resolution is complete. See usage within [FinalityFlow]. + * Not recommended for 3rd party use. */ -open class ReceiveTransactionFlow @JvmOverloads constructor(private val otherSideSession: FlowSession, - private val checkSufficientSignatures: Boolean = true, - private val statesToRecord: StatesToRecord = StatesToRecord.NONE) : FlowLogic() { +open class ReceiveTransactionFlow constructor(private val otherSideSession: FlowSession, + private val checkSufficientSignatures: Boolean = true, + private val statesToRecord: StatesToRecord = StatesToRecord.NONE, + private val deferredAck: Boolean = false) : FlowLogic() { + @JvmOverloads constructor( + otherSideSession: FlowSession, + checkSufficientSignatures: Boolean = true, + statesToRecord: StatesToRecord = StatesToRecord.NONE + ) : this(otherSideSession, checkSufficientSignatures, statesToRecord, false) + @Suppress("KDocMissingDocumentation") @Suspendable @Throws(SignatureException::class, @@ -47,7 +57,7 @@ open class ReceiveTransactionFlow @JvmOverloads constructor(private val otherSid it.pushToLoggingContext() logger.info("Received transaction acknowledgement request from party ${otherSideSession.counterparty}.") checkParameterHash(it.networkParametersHash) - subFlow(ResolveTransactionsFlow(it, otherSideSession, statesToRecord)) + subFlow(ResolveTransactionsFlow(it, otherSideSession, statesToRecord, deferredAck)) logger.info("Transaction dependencies resolution completed.") try { it.verify(serviceHub, checkSufficientSignatures) diff --git a/core/src/main/kotlin/net/corda/core/internal/PlatformVersionSwitches.kt b/core/src/main/kotlin/net/corda/core/internal/PlatformVersionSwitches.kt index c6d93f272f..f40ea96c6c 100644 --- a/core/src/main/kotlin/net/corda/core/internal/PlatformVersionSwitches.kt +++ b/core/src/main/kotlin/net/corda/core/internal/PlatformVersionSwitches.kt @@ -18,4 +18,5 @@ object PlatformVersionSwitches { const val ENABLE_P2P_COMPRESSION = 7 const val RESTRICTED_DATABASE_OPERATIONS = 7 const val CERTIFICATE_ROTATION = 9 + const val TWO_PHASE_FINALITY = 13 } \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/internal/ResolveTransactionsFlow.kt b/core/src/main/kotlin/net/corda/core/internal/ResolveTransactionsFlow.kt index 66b525692f..cf3359f2e7 100644 --- a/core/src/main/kotlin/net/corda/core/internal/ResolveTransactionsFlow.kt +++ b/core/src/main/kotlin/net/corda/core/internal/ResolveTransactionsFlow.kt @@ -21,7 +21,8 @@ class ResolveTransactionsFlow private constructor( val initialTx: SignedTransaction?, val txHashes: Set, val otherSide: FlowSession, - val statesToRecord: StatesToRecord + val statesToRecord: StatesToRecord, + val deferredAck: Boolean = false ) : FlowLogic() { constructor(txHashes: Set, otherSide: FlowSession, statesToRecord: StatesToRecord = StatesToRecord.NONE) @@ -36,6 +37,9 @@ class ResolveTransactionsFlow private constructor( constructor(transaction: SignedTransaction, otherSide: FlowSession, statesToRecord: StatesToRecord = StatesToRecord.NONE) : this(transaction, transaction.dependencies, otherSide, statesToRecord) + constructor(transaction: SignedTransaction, otherSide: FlowSession, statesToRecord: StatesToRecord = StatesToRecord.NONE, deferredAck: Boolean = false) + : this(transaction, transaction.dependencies, otherSide, statesToRecord, deferredAck) + private var fetchNetParamsFromCounterpart = false @Suppress("MagicNumber") @@ -60,8 +64,10 @@ class ResolveTransactionsFlow private constructor( val resolver = (serviceHub as ServiceHubCoreInternal).createTransactionsResolver(this) resolver.downloadDependencies(batchMode) - logger.trace { "ResolveTransactionsFlow: Sending END." } - otherSide.send(FetchDataFlow.Request.End) // Finish fetching data. + if (!deferredAck) { + logger.trace { "ResolveTransactionsFlow: Sending END." } + otherSide.send(FetchDataFlow.Request.End) // Finish fetching data. + } // If transaction resolution is performed for a transaction where some states are relevant, then those should be // recorded if this has not already occurred. diff --git a/core/src/main/kotlin/net/corda/core/internal/ServiceHubCoreInternal.kt b/core/src/main/kotlin/net/corda/core/internal/ServiceHubCoreInternal.kt index 4b7d856699..af7ce40179 100644 --- a/core/src/main/kotlin/net/corda/core/internal/ServiceHubCoreInternal.kt +++ b/core/src/main/kotlin/net/corda/core/internal/ServiceHubCoreInternal.kt @@ -2,10 +2,13 @@ package net.corda.core.internal import co.paralleluniverse.fibers.Suspendable import net.corda.core.DeleteForDJVM +import net.corda.core.crypto.TransactionSignature +import net.corda.core.flows.FlowTransactionMetadata import net.corda.core.internal.notary.NotaryService import net.corda.core.node.ServiceHub import net.corda.core.node.StatesToRecord import net.corda.core.serialization.internal.AttachmentsClassLoaderCache +import net.corda.core.transactions.SignedTransaction import java.util.concurrent.ExecutorService // TODO: This should really be called ServiceHubInternal but that name is already taken by net.corda.node.services.api.ServiceHubInternal. @@ -24,6 +27,24 @@ interface ServiceHubCoreInternal : ServiceHub { fun createTransactionsResolver(flow: ResolveTransactionsFlow): TransactionsResolver val attachmentsClassLoaderCache: AttachmentsClassLoaderCache + + /** + * Stores [SignedTransaction] and participant signatures without the notary signature in the local transaction storage. + * Optionally add finality flow recovery metadata. + * This is expected to be run within a database transaction. + * + * @param txn The transaction to record. + */ + fun recordUnnotarisedTransaction(txn: SignedTransaction, metadata: FlowTransactionMetadata?= null) + + /** + * Stores [SignedTransaction] with extra signatures in the local transaction storage + * + * @param sigs The signatures to add to the transaction. + * @param txn The transactions to record. + * @param statesToRecord how the vault should treat the output states of the transaction. + */ + fun finalizeTransactionWithExtraSignatures(txn: SignedTransaction, sigs: Collection, statesToRecord: StatesToRecord) } interface TransactionsResolver { diff --git a/node/src/integration-test/kotlin/net/corda/node/flows/FlowReloadAfterCheckpointTest.kt b/node/src/integration-test/kotlin/net/corda/node/flows/FlowReloadAfterCheckpointTest.kt index add9ecb651..7f83f94cb6 100644 --- a/node/src/integration-test/kotlin/net/corda/node/flows/FlowReloadAfterCheckpointTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/flows/FlowReloadAfterCheckpointTest.kt @@ -336,8 +336,8 @@ class FlowReloadAfterCheckpointTest { .toSet() .single() reloads.await(DEFAULT_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS) - assertEquals(7, reloads.filter { it == flowStartedByAlice }.size) - assertEquals(6, reloads.filter { it == flowStartedByBob }.size) + assertEquals(8, reloads.filter { it == flowStartedByAlice }.size) + assertEquals(7, reloads.filter { it == flowStartedByBob }.size) } } diff --git a/node/src/integration-test/kotlin/net/corda/node/services/statemachine/FlowHospitalTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/statemachine/FlowHospitalTest.kt index 139bb89505..5c17bb7bac 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/statemachine/FlowHospitalTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/statemachine/FlowHospitalTest.kt @@ -19,7 +19,10 @@ import net.corda.core.flows.ReceiveFinalityFlow import net.corda.core.flows.SignTransactionFlow import net.corda.core.flows.StartableByRPC import net.corda.core.flows.UnexpectedFlowEndException +import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party +import net.corda.core.internal.PLATFORM_VERSION +import net.corda.core.internal.PlatformVersionSwitches import net.corda.core.internal.concurrent.transpose import net.corda.core.messaging.StateMachineUpdate import net.corda.core.messaging.startFlow @@ -40,14 +43,22 @@ import net.corda.testing.core.singleIdentity import net.corda.testing.driver.DriverParameters import net.corda.testing.driver.driver import net.corda.testing.node.User +import net.corda.testing.node.internal.CustomCordapp +import net.corda.testing.node.internal.DUMMY_CONTRACTS_CORDAPP +import net.corda.testing.node.internal.InternalMockNetwork +import net.corda.testing.node.internal.InternalMockNodeParameters +import net.corda.testing.node.internal.MOCK_VERSION_INFO +import net.corda.testing.node.internal.TestCordappInternal +import net.corda.testing.node.internal.TestStartedNode import net.corda.testing.node.internal.enclosedCordapp import net.corda.testing.node.internal.findCordapp +import net.corda.testing.node.testContext import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatThrownBy import org.junit.Before import org.junit.Test import java.sql.SQLException -import java.util.* +import java.util.Random import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit import java.util.concurrent.TimeoutException @@ -59,6 +70,11 @@ class FlowHospitalTest { private val rpcUser = User("user1", "test", permissions = setOf(Permissions.all())) + companion object { + private val mockNet = InternalMockNetwork(cordappsForAllNodes = setOf(DUMMY_CONTRACTS_CORDAPP, enclosedCordapp(), + CustomCordapp(targetPlatformVersion = 3, classes = setOf(FinalityFlow::class.java)))) + } + @Before fun before() { SpendStateAndCatchDoubleSpendResponderFlow.exceptionSeenInUserFlow = false @@ -238,6 +254,78 @@ class FlowHospitalTest { assertTrue(SpendStateAndCatchDoubleSpendResponderFlow.exceptionSeenInUserFlow) } + @Test(timeout=300_000) + fun `catching a notary error - two phase finality flow initiator to pre-2PF peer`() { + var dischargedCounter = 0 + StaffedFlowHospital.onFlowErrorPropagated.add { _, _ -> + ++dischargedCounter + } + val aliceNode = createNode(ALICE_NAME, platformVersion = PlatformVersionSwitches.TWO_PHASE_FINALITY) + val bobNode = createNode(BOB_NAME, platformVersion = PlatformVersionSwitches.TWO_PHASE_FINALITY - 1) + + val handle = aliceNode.services.startFlow(CreateTransactionFlow(bobNode.info.singleIdentity()), testContext()) + mockNet.runNetwork() + val ref = handle.getOrThrow().resultFuture.get() + aliceNode.services.startFlow(SpendStateAndCatchDoubleSpendFlow(bobNode.info.singleIdentity(), ref), testContext()).getOrThrow() + aliceNode.services.startFlow(SpendStateAndCatchDoubleSpendFlow(bobNode.info.singleIdentity(), ref), testContext()).getOrThrow() + mockNet.runNetwork() + + // 1 is the notary failing to notarise and propagating the error + // 2 is the receiving flow failing due to the unexpected session end error + assertEquals(2, dischargedCounter) + assertTrue(SpendStateAndCatchDoubleSpendResponderFlow.exceptionSeenInUserFlow) + } + + @Test(timeout=300_000) + fun `catching a notary error - pre-2PF initiator to two phase finality flow peer`() { + var dischargedCounter = 0 + StaffedFlowHospital.onFlowErrorPropagated.add { _, _ -> + ++dischargedCounter + } + val aliceNode = createNode(ALICE_NAME, platformVersion = PlatformVersionSwitches.TWO_PHASE_FINALITY - 1) + val bobNode = createNode(BOB_NAME, platformVersion = PlatformVersionSwitches.TWO_PHASE_FINALITY) + + val handle = aliceNode.services.startFlow(CreateTransactionFlow(bobNode.info.singleIdentity()), testContext()) + mockNet.runNetwork() + val ref = handle.getOrThrow().resultFuture.get() + aliceNode.services.startFlow(SpendStateAndCatchDoubleSpendFlow(bobNode.info.singleIdentity(), ref), testContext()).getOrThrow() + aliceNode.services.startFlow(SpendStateAndCatchDoubleSpendFlow(bobNode.info.singleIdentity(), ref), testContext()).getOrThrow() + mockNet.runNetwork() + + // 1 is the notary failing to notarise and propagating the error + // 2 is the receiving flow failing due to the unexpected session end error + assertEquals(2, dischargedCounter) + assertTrue(SpendStateAndCatchDoubleSpendResponderFlow.exceptionSeenInUserFlow) + } + + private fun createNode(legalName: CordaX500Name, cordapps: List = emptyList(), platformVersion: Int = PLATFORM_VERSION): TestStartedNode { + return mockNet.createNode(InternalMockNodeParameters(legalName = legalName, additionalCordapps = cordapps, + version = MOCK_VERSION_INFO.copy(platformVersion = platformVersion))) + } + + @Test(timeout = 300_000) + fun `old finality flow catching a notary error will cause a peer to fail with unexpected session end during ReceiveFinalityFlow that passes through user code`() { + var dischargedCounter = 0 + StaffedFlowHospital.onFlowErrorPropagated.add { _, _ -> + ++dischargedCounter + } + val user = User("mark", "dadada", setOf(Permissions.all())) + driver(DriverParameters(isDebug = false, startNodesInProcess = true)) { + + val nodeAHandle = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user)).getOrThrow() + val nodeBHandle = startNode(providedName = BOB_NAME, rpcUsers = listOf(user)).getOrThrow() + nodeAHandle.rpc.let { + val ref = it.startFlow(::CreateTransactionFlow, nodeBHandle.nodeInfo.singleIdentity()).returnValue.getOrThrow(20.seconds) + it.startFlow(::SpendStateAndCatchDoubleSpendOldFinalityFlow, nodeBHandle.nodeInfo.singleIdentity(), ref).returnValue.getOrThrow(20.seconds) + it.startFlow(::SpendStateAndCatchDoubleSpendOldFinalityFlow, nodeBHandle.nodeInfo.singleIdentity(), ref).returnValue.getOrThrow(20.seconds) + } + } + // 1 is the notary failing to notarise and propagating the error + // 2 is the receiving flow failing due to the unexpected session end error + assertEquals(2, dischargedCounter) + assertTrue(SpendStateAndCatchDoubleSpendResponderOldFinalityFlow.exceptionSeenInUserFlow) + } + @Test(timeout = 300_000) fun `unexpected session end errors outside of ReceiveFinalityFlow are not handled`() { var dischargedCounter = 0 @@ -483,6 +571,62 @@ class FlowHospitalTest { } } + @InitiatingFlow + @StartableByRPC + class SpendStateAndCatchDoubleSpendOldFinalityFlow( + private val peer: Party, + private val ref: StateAndRef, + private val consumePeerError: Boolean + ) : FlowLogic>() { + + constructor(peer: Party, ref: StateAndRef): this(peer, ref, false) + + @Suspendable + override fun call(): StateAndRef { + val tx = TransactionBuilder(serviceHub.networkMapCache.notaryIdentities.first()).apply { + addInputState(ref) + addOutputState(DummyState(participants = listOf(ourIdentity))) + addCommand(DummyContract.Commands.Move(), listOf(ourIdentity.owningKey, peer.owningKey)) + } + val stx = serviceHub.signInitialTransaction(tx) + val session = initiateFlow(peer) + session.send(consumePeerError) + val ftx = subFlow(CollectSignaturesFlow(stx, listOf(session))) + try { + subFlow(OldFinalityFlow(ftx, session)) + } catch(e: NotaryException) { + logger.info("Caught notary exception") + } + return ftx.coreTransaction.outRef(0) + } + } + + @InitiatedBy(SpendStateAndCatchDoubleSpendOldFinalityFlow::class) + class SpendStateAndCatchDoubleSpendResponderOldFinalityFlow(private val session: FlowSession) : FlowLogic() { + + companion object { + var exceptionSeenInUserFlow = false + } + + @Suspendable + override fun call() { + val consumeError = session.receive().unwrap { it } + val stx = subFlow(object : SignTransactionFlow(session) { + override fun checkTransaction(stx: SignedTransaction) { + + } + }) + try { + subFlow(OldReceiveFinalityFlow(session, stx.id)) + } catch (e: UnexpectedFlowEndException) { + exceptionSeenInUserFlow = true + if (!consumeError) { + throw e + } + } + } + } + @InitiatingFlow @StartableByRPC class CreateTransactionButDontFinalizeFlow(private val peer: Party, private val ref: StateAndRef) : FlowLogic() { diff --git a/node/src/integration-test/kotlin/net/corda/node/services/statemachine/OldFinalityFlow.kt b/node/src/integration-test/kotlin/net/corda/node/services/statemachine/OldFinalityFlow.kt new file mode 100644 index 0000000000..e352ac02e9 --- /dev/null +++ b/node/src/integration-test/kotlin/net/corda/node/services/statemachine/OldFinalityFlow.kt @@ -0,0 +1,306 @@ +package net.corda.node.services.statemachine + +import co.paralleluniverse.fibers.Suspendable +import net.corda.core.CordaInternal +import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.isFulfilledBy +import net.corda.core.flows.FlowLogic +import net.corda.core.flows.FlowSession +import net.corda.core.flows.InitiatingFlow +import net.corda.core.flows.NotaryException +import net.corda.core.flows.NotaryFlow +import net.corda.core.flows.ReceiveTransactionFlow +import net.corda.core.flows.SendTransactionFlow +import net.corda.core.flows.UnexpectedFlowEndException +import net.corda.core.identity.Party +import net.corda.core.identity.groupAbstractPartyByWellKnownParty +import net.corda.core.internal.telemetry.telemetryServiceInternal +import net.corda.core.internal.warnOnce +import net.corda.core.node.StatesToRecord +import net.corda.core.node.StatesToRecord.ONLY_RELEVANT +import net.corda.core.transactions.LedgerTransaction +import net.corda.core.transactions.SignedTransaction +import net.corda.core.utilities.ProgressTracker +import net.corda.core.utilities.debug + +/** + * Verifies the given transaction, then sends it to the named notary. If the notary agrees that the transaction + * is acceptable then it is from that point onwards committed to the ledger, and will be written through to the + * vault. Additionally it will be distributed to the parties reflected in the participants list of the states. + * + * By default, the initiating flow will commit states that are relevant to the initiating party as indicated by + * [StatesToRecord.ONLY_RELEVANT]. Relevance is determined by the union of all participants to states which have been + * included in the transaction. This default behaviour may be modified by passing in an alternate value for [StatesToRecord]. + * + * The transaction is expected to have already been resolved: if its dependencies are not available in local + * storage, verification will fail. It must have signatures from all necessary parties other than the notary. + * + * A list of [FlowSession]s is required for each non-local participant of the transaction. These participants will receive + * the final notarised transaction by calling [ReceiveFinalityFlow] in their counterpart flows. Sessions with non-participants + * can also be included, but they must specify [StatesToRecord.ALL_VISIBLE] for statesToRecord if they wish to record the + * contract states into their vaults. + * + * The flow returns the same transaction but with the additional signatures from the notary. + * + * NOTE: This is an inlined flow but for backwards compatibility is annotated with [InitiatingFlow]. + */ +// To maintain backwards compatibility with the old API, FinalityFlow can act both as an initiating flow and as an inlined flow. +// This is only possible because a flow is only truly initiating when the first call to initiateFlow is made (where the +// presence of @InitiatingFlow is checked). So the new API is inlined simply because that code path doesn't call initiateFlow. +@InitiatingFlow +class OldFinalityFlow private constructor(val transaction: SignedTransaction, + private val oldParticipants: Collection, + override val progressTracker: ProgressTracker, + private val sessions: Collection, + private val newApi: Boolean, + private val statesToRecord: StatesToRecord = ONLY_RELEVANT) : FlowLogic() { + + @CordaInternal + data class ExtraConstructorArgs(val oldParticipants: Collection, val sessions: Collection, val newApi: Boolean, val statesToRecord: StatesToRecord) + + @CordaInternal + fun getExtraConstructorArgs() = ExtraConstructorArgs(oldParticipants, sessions, newApi, statesToRecord) + + @Deprecated(DEPRECATION_MSG) + constructor(transaction: SignedTransaction, extraRecipients: Set, progressTracker: ProgressTracker) : this( + transaction, extraRecipients, progressTracker, emptyList(), false + ) + @Deprecated(DEPRECATION_MSG) + constructor(transaction: SignedTransaction, extraRecipients: Set) : this(transaction, extraRecipients, tracker(), emptyList(), false) + @Deprecated(DEPRECATION_MSG) + constructor(transaction: SignedTransaction) : this(transaction, emptySet(), tracker(), emptyList(), false) + @Deprecated(DEPRECATION_MSG) + constructor(transaction: SignedTransaction, progressTracker: ProgressTracker) : this(transaction, emptySet(), progressTracker, emptyList(), false) + + /** + * Notarise the given transaction and broadcast it to the given [FlowSession]s. This list **must** at least include + * all the non-local participants of the transaction. Sessions to non-participants can also be provided. + * + * @param transaction What to commit. + */ + constructor(transaction: SignedTransaction, firstSession: FlowSession, vararg restSessions: FlowSession) : this( + transaction, listOf(firstSession) + restSessions.asList() + ) + + /** + * Notarise the given transaction and broadcast it to all the participants. + * + * @param transaction What to commit. + * @param sessions A collection of [FlowSession]s for each non-local participant of the transaction. Sessions to non-participants can + * also be provided. + */ + @JvmOverloads + constructor( + transaction: SignedTransaction, + sessions: Collection, + progressTracker: ProgressTracker = tracker() + ) : this(transaction, emptyList(), progressTracker, sessions, true) + + /** + * Notarise the given transaction and broadcast it to all the participants. + * + * @param transaction What to commit. + * @param sessions A collection of [FlowSession]s for each non-local participant of the transaction. Sessions to non-participants can + * also be provided. + * @param statesToRecord Which states to commit to the vault. + */ + @JvmOverloads + constructor( + transaction: SignedTransaction, + sessions: Collection, + statesToRecord: StatesToRecord, + progressTracker: ProgressTracker = tracker() + ) : this(transaction, emptyList(), progressTracker, sessions, true, statesToRecord) + + /** + * Notarise the given transaction and broadcast it to all the participants. + * + * @param transaction What to commit. + * @param sessions A collection of [FlowSession]s for each non-local participant. + * @param oldParticipants An **optional** collection of parties for participants who are still using the old API. + * + * You will only need to use this parameter if you have upgraded your CorDapp from the V3 FinalityFlow API but are required to provide + * backwards compatibility with participants running V3 nodes. If you're writing a new CorDapp then this does not apply and this + * parameter should be ignored. + */ + @Deprecated(DEPRECATION_MSG) + constructor( + transaction: SignedTransaction, + sessions: Collection, + oldParticipants: Collection, + progressTracker: ProgressTracker + ) : this(transaction, oldParticipants, progressTracker, sessions, true) + + companion object { + private const val DEPRECATION_MSG = "It is unsafe to use this constructor as it requires nodes to automatically " + + "accept notarised transactions without first checking their relevancy. Instead, use one of the constructors " + + "that requires only FlowSessions." + + object NOTARISING : ProgressTracker.Step("Requesting signature by notary service") { + override fun childProgressTracker() = NotaryFlow.Client.tracker() + } + + object BROADCASTING : ProgressTracker.Step("Broadcasting transaction to participants") + + @JvmStatic + fun tracker() = ProgressTracker(NOTARISING, BROADCASTING) + } + + @Suppress("ComplexMethod") + @Suspendable + @Throws(NotaryException::class) + override fun call(): SignedTransaction { + if (!newApi) { + logger.warnOnce("The current usage of FinalityFlow is unsafe. Please consider upgrading your CorDapp to use " + + "FinalityFlow with FlowSessions. (${serviceHub.getAppContext().cordapp.info})") + } else { + require(sessions.none { serviceHub.myInfo.isLegalIdentity(it.counterparty) }) { + "Do not provide flow sessions for the local node. FinalityFlow will record the notarised transaction locally." + } + } + + // Note: this method is carefully broken up to minimize the amount of data reachable from the stack at + // the point where subFlow is invoked, as that minimizes the checkpointing work to be done. + // + // Lookup the resolved transactions and use them to map each signed transaction to the list of participants. + // Then send to the notary if needed, record locally and distribute. + + logCommandData() + val ledgerTransaction = verifyTx() + val externalTxParticipants = extractExternalParticipants(ledgerTransaction) + + if (newApi) { + val sessionParties = sessions.map { it.counterparty } + val missingRecipients = externalTxParticipants - sessionParties - oldParticipants + require(missingRecipients.isEmpty()) { + "Flow sessions were not provided for the following transaction participants: $missingRecipients" + } + sessionParties.intersect(oldParticipants).let { + require(it.isEmpty()) { "The following parties are specified both in flow sessions and in the oldParticipants list: $it" } + } + } + + val notarised = notariseAndRecord() + + progressTracker.currentStep = BROADCASTING + + if (newApi) { + oldV3Broadcast(notarised, oldParticipants.toSet()) + for (session in sessions) { + try { + subFlow(SendTransactionFlow(session, notarised)) + logger.info("Party ${session.counterparty} received the transaction.") + } catch (e: UnexpectedFlowEndException) { + throw UnexpectedFlowEndException( + "${session.counterparty} has finished prematurely and we're trying to send them the finalised transaction. " + + "Did they forget to call ReceiveFinalityFlow? (${e.message})", + e.cause, + e.originalErrorId + ) + } + } + } else { + oldV3Broadcast(notarised, (externalTxParticipants + oldParticipants).toSet()) + } + + logger.info("All parties received the transaction successfully.") + + return notarised + } + + @Suspendable + private fun oldV3Broadcast(notarised: SignedTransaction, recipients: Set) { + for (recipient in recipients) { + if (!serviceHub.myInfo.isLegalIdentity(recipient)) { + logger.debug { "Sending transaction to party $recipient." } + val session = initiateFlow(recipient) + subFlow(SendTransactionFlow(session, notarised)) + logger.info("Party $recipient received the transaction.") + } + } + } + + private fun logCommandData() { + if (logger.isDebugEnabled) { + val commandDataTypes = transaction.tx.commands.asSequence().mapNotNull { it.value::class.qualifiedName }.distinct() + logger.debug("Started finalization, commands are ${commandDataTypes.joinToString(", ", "[", "]")}.") + } + } + + @Suspendable + private fun notariseAndRecord(): SignedTransaction { + serviceHub.telemetryServiceInternal.span("${this::class.java.name}#notariseAndRecord", flowLogic = this) { + val notarised = if (needsNotarySignature(transaction)) { + progressTracker.currentStep = NOTARISING + val notarySignatures = subFlow(NotaryFlow.Client(transaction, skipVerification = true)) + transaction + notarySignatures + } else { + logger.info("No need to notarise this transaction.") + transaction + } + serviceHub.telemetryServiceInternal.span("${this::class.java.name}#notariseAndRecord:recordTransactions", flowLogic = this) { + logger.info("Recording transaction locally.") + serviceHub.recordTransactions(statesToRecord, listOf(notarised)) + logger.info("Recorded transaction locally successfully.") + } + return notarised + } + } + + private fun needsNotarySignature(stx: SignedTransaction): Boolean { + val wtx = stx.tx + val needsNotarisation = wtx.inputs.isNotEmpty() || wtx.references.isNotEmpty() || wtx.timeWindow != null + return needsNotarisation && hasNoNotarySignature(stx) + } + + private fun hasNoNotarySignature(stx: SignedTransaction): Boolean { + val notaryKey = stx.tx.notary?.owningKey + val signers = stx.sigs.asSequence().map { it.by }.toSet() + return notaryKey?.isFulfilledBy(signers) != true + } + + private fun extractExternalParticipants(ltx: LedgerTransaction): Set { + val participants = ltx.outputStates.flatMap { it.participants } + ltx.inputStates.flatMap { it.participants } + return groupAbstractPartyByWellKnownParty(serviceHub, participants).keys - serviceHub.myInfo.legalIdentities + } + + private fun verifyTx(): LedgerTransaction { + val notary = transaction.tx.notary + // The notary signature(s) are allowed to be missing but no others. + if (notary != null) transaction.verifySignaturesExcept(notary.owningKey) else transaction.verifyRequiredSignatures() + // TODO= [CORDA-3267] Remove duplicate signature verification + val ltx = transaction.toLedgerTransaction(serviceHub, false) + ltx.verify() + return ltx + } +} + +/** + * The receiving counterpart to [FinalityFlow]. + * + * All parties who are receiving a finalised transaction from a sender flow must subcall this flow in their own flows. + * + * It's typical to have already signed the transaction proposal in the same workflow using [SignTransactionFlow]. If so + * then the transaction ID can be passed in as an extra check to ensure the finalised transaction is the one that was signed + * before it's committed to the vault. + * + * @param otherSideSession The session which is providing the transaction to record. + * @param expectedTxId Expected ID of the transaction that's about to be received. This is typically retrieved from + * [SignTransactionFlow]. Setting it to null disables the expected transaction ID check. + * @param statesToRecord Which states to commit to the vault. Defaults to [StatesToRecord.ONLY_RELEVANT]. + */ +class OldReceiveFinalityFlow @JvmOverloads constructor(private val otherSideSession: FlowSession, + private val expectedTxId: SecureHash? = null, + private val statesToRecord: StatesToRecord = ONLY_RELEVANT) : FlowLogic() { + @Suspendable + override fun call(): SignedTransaction { + return subFlow(object : ReceiveTransactionFlow(otherSideSession, checkSufficientSignatures = true, statesToRecord = statesToRecord) { + override fun checkBeforeRecording(stx: SignedTransaction) { + require(expectedTxId == null || expectedTxId == stx.id) { + "We expected to receive transaction with ID $expectedTxId but instead got ${stx.id}. Transaction was" + + "not recorded and nor its states sent to the vault." + } + } + }) + } +} diff --git a/node/src/integration-test/kotlin/net/corda/node/services/vault/VaultObserverExceptionTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/vault/VaultObserverExceptionTest.kt index 198b9d3ab6..10ff0364d7 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/vault/VaultObserverExceptionTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/vault/VaultObserverExceptionTest.kt @@ -39,7 +39,6 @@ import org.assertj.core.api.Assertions import org.junit.After import org.junit.Assert import org.junit.Test -import java.lang.IllegalStateException import java.sql.SQLException import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.TimeUnit @@ -422,9 +421,10 @@ class VaultObserverExceptionTest { /** * An error is thrown inside of the [VaultService.rawUpdates] observable while recording a transaction inside of the initiating node. * - * This causes the transaction to not be saved on the local node but the notary still records the transaction as spent. The transaction - * also is not send to the counterparty node since it failed before reaching the send. Therefore no subscriber events occur on the - * counterparty node. + * Two Phase Finality update: + * This causes the transaction to not be finalised on the local node but the notary still records the transaction as spent. The transaction + * does finalize at the counterparty node since the notary signatures are broadcast to peers prior to initiator node finalisation. + * Subscriber events will occur on the counterparty node. * * More importantly, the observer listening to the [VaultService.rawUpdates] observable should not unsubscribe. * @@ -487,29 +487,32 @@ class VaultObserverExceptionTest { println("First set of flows") val stateId = startErrorInObservableWhenConsumingState() assertEquals(0, aliceNode.getStatesById(stateId, Vault.StateStatus.CONSUMED).size) - assertEquals(0, bobNode.getStatesById(stateId, Vault.StateStatus.UNCONSUMED).size) + // Ledger is temporarily inconsistent as peer finalised transaction but initiator error'ed before finalisation. + assertEquals(1, bobNode.getStatesById(stateId, Vault.StateStatus.UNCONSUMED).size) assertEquals(1, notary.getNotarisedTransactionIds().size) assertEquals(1, observationCounter) assertEquals(2, rawUpdatesCount[aliceNode.nodeInfo.singleIdentity()]) - assertEquals(0, rawUpdatesCount.getOrDefault(bobNode.nodeInfo.singleIdentity(), 0)) + assertEquals(1, rawUpdatesCount.getOrDefault(bobNode.nodeInfo.singleIdentity(), 0)) println("Second set of flows") val stateId2 = startErrorInObservableWhenConsumingState() assertEquals(0, aliceNode.getStatesById(stateId2, Vault.StateStatus.CONSUMED).size) - assertEquals(0, bobNode.getStatesById(stateId2, Vault.StateStatus.UNCONSUMED).size) + // Ledger is temporarily inconsistent as peer finalised transaction but initiator error'ed before finalisation. + assertEquals(1, bobNode.getStatesById(stateId2, Vault.StateStatus.UNCONSUMED).size) assertEquals(2, notary.getNotarisedTransactionIds().size) assertEquals(2, observationCounter) assertEquals(4, rawUpdatesCount[aliceNode.nodeInfo.singleIdentity()]) - assertEquals(0, rawUpdatesCount.getOrDefault(bobNode.nodeInfo.singleIdentity(), 0)) + assertEquals(2, rawUpdatesCount.getOrDefault(bobNode.nodeInfo.singleIdentity(), 0)) } } /** * An error is thrown inside of the [VaultService.rawUpdates] observable while recording a transaction inside of the initiating node. * - * This causes the transaction to not be saved on the local node but the notary still records the transaction as spent. The transaction - * also is not send to the counterparty node since it failed before reaching the send. Therefore no subscriber events occur on the - * counterparty node. + * Two Phase Finality update: + * This causes the transaction to not be finalised on the local node but the notary still records the transaction as spent. The transaction + * does finalize at the counterparty node since the notary signatures are broadcast to peers prior to initiator node finalisation. + * Subscriber events will occur on the counterparty node. * * More importantly, the observer listening to the [VaultService.rawUpdates] observable should not unsubscribe. * @@ -578,19 +581,21 @@ class VaultObserverExceptionTest { val stateId = startErrorInObservableWhenConsumingState() assertEquals(0, aliceNode.getStatesById(stateId, Vault.StateStatus.CONSUMED).size) - assertEquals(0, bobNode.getStatesById(stateId, Vault.StateStatus.UNCONSUMED).size) + // Ledger is temporarily inconsistent as peer finalised transaction but initiator error'ed before finalisation. + assertEquals(1, bobNode.getStatesById(stateId, Vault.StateStatus.UNCONSUMED).size) assertEquals(1, notary.getNotarisedTransactionIds().size) assertEquals(1, observationCounter) assertEquals(3, rawUpdatesCount[aliceNode.nodeInfo.singleIdentity()]) - assertEquals(0, rawUpdatesCount.getOrDefault(bobNode.nodeInfo.singleIdentity(), 0)) + assertEquals(1, rawUpdatesCount.getOrDefault(bobNode.nodeInfo.singleIdentity(), 0)) val stateId2 = startErrorInObservableWhenConsumingState() assertEquals(0, aliceNode.getStatesById(stateId2, Vault.StateStatus.CONSUMED).size) - assertEquals(0, bobNode.getStatesById(stateId2, Vault.StateStatus.UNCONSUMED).size) + // Ledger is temporarily inconsistent as peer finalised transaction but initiator error'ed before finalisation. + assertEquals(1, bobNode.getStatesById(stateId2, Vault.StateStatus.UNCONSUMED).size) assertEquals(2, notary.getNotarisedTransactionIds().size) assertEquals(2, observationCounter) assertEquals(6, rawUpdatesCount[aliceNode.nodeInfo.singleIdentity()]) - assertEquals(0, rawUpdatesCount.getOrDefault(bobNode.nodeInfo.singleIdentity(), 0)) + assertEquals(2, rawUpdatesCount.getOrDefault(bobNode.nodeInfo.singleIdentity(), 0)) } } @@ -681,9 +686,10 @@ class VaultObserverExceptionTest { /** * An error is thrown inside of the [VaultService.updates] observable while recording a transaction inside of the initiating node. * - * This causes the transaction to not be saved on the local node but the notary still records the transaction as spent. The transaction - * also is not send to the counterparty node since it failed before reaching the send. Therefore no subscriber events occur on the - * counterparty node. + * Two Phase Finality update: + * This causes the transaction to not be finalised on the local node but the notary still records the transaction as spent. The transaction + * does finalize at the counterparty node since the notary signatures are broadcast to peers prior to initiator node finalisation. + * Subscriber events will occur on the counterparty node. * * More importantly, the observer listening to the [VaultService.updates] observable should not unsubscribe. * @@ -743,19 +749,21 @@ class VaultObserverExceptionTest { val stateId = startErrorInObservableWhenConsumingState() assertEquals(0, aliceNode.getStatesById(stateId, Vault.StateStatus.CONSUMED).size) assertEquals(1, aliceNode.getStatesById(stateId, Vault.StateStatus.UNCONSUMED).size) - assertEquals(0, bobNode.getStatesById(stateId, Vault.StateStatus.UNCONSUMED).size) + // Ledger is temporarily inconsistent as peer finalised transaction but initiator error'ed before finalisation. + assertEquals(1, bobNode.getStatesById(stateId, Vault.StateStatus.UNCONSUMED).size) assertEquals(1, notary.getNotarisedTransactionIds().size) assertEquals(1, observationCounter) assertEquals(2, rawUpdatesCount[aliceNode.nodeInfo.singleIdentity()]) - assertEquals(0, rawUpdatesCount.getOrDefault(bobNode.nodeInfo.singleIdentity(), 0)) + assertEquals(1, rawUpdatesCount.getOrDefault(bobNode.nodeInfo.singleIdentity(), 0)) val stateId2 = startErrorInObservableWhenConsumingState() assertEquals(0, aliceNode.getStatesById(stateId2, Vault.StateStatus.CONSUMED).size) assertEquals(2, aliceNode.getAllStates(Vault.StateStatus.UNCONSUMED).size) - assertEquals(0, bobNode.getStatesById(stateId2, Vault.StateStatus.UNCONSUMED).size) + // Ledger is temporarily inconsistent as peer finalised transaction but initiator error'ed before finalisation. + assertEquals(1, bobNode.getStatesById(stateId2, Vault.StateStatus.UNCONSUMED).size) assertEquals(2, notary.getNotarisedTransactionIds().size) assertEquals(4, rawUpdatesCount[aliceNode.nodeInfo.singleIdentity()]) - assertEquals(0, rawUpdatesCount.getOrDefault(bobNode.nodeInfo.singleIdentity(), 0)) + assertEquals(2, rawUpdatesCount.getOrDefault(bobNode.nodeInfo.singleIdentity(), 0)) } } diff --git a/node/src/main/kotlin/net/corda/node/services/DbTransactionsResolver.kt b/node/src/main/kotlin/net/corda/node/services/DbTransactionsResolver.kt index bc6cf3d2af..3d737ea422 100644 --- a/node/src/main/kotlin/net/corda/node/services/DbTransactionsResolver.kt +++ b/node/src/main/kotlin/net/corda/node/services/DbTransactionsResolver.kt @@ -3,6 +3,7 @@ package net.corda.node.services import co.paralleluniverse.fibers.Suspendable import net.corda.core.crypto.SecureHash import net.corda.core.flows.FlowLogic +import net.corda.core.flows.TransactionStatus import net.corda.core.internal.FetchTransactionsFlow import net.corda.core.internal.ResolveTransactionsFlow import net.corda.core.internal.TransactionsResolver @@ -101,10 +102,10 @@ class DbTransactionsResolver(private val flow: ResolveTransactionsFlow) : Transa val transactionStorage = flow.serviceHub.validatedTransactions as WritableTransactionStorage for (txId in sortedDependencies) { // Retrieve and delete the transaction from the unverified store. - val (tx, isVerified) = checkNotNull(transactionStorage.getTransactionInternal(txId)) { + val (tx, txStatus) = checkNotNull(transactionStorage.getTransactionInternal(txId)) { "Somehow the unverified transaction ($txId) that we stored previously is no longer there." } - if (!isVerified) { + if (txStatus != TransactionStatus.VERIFIED) { tx.verify(flow.serviceHub) flow.serviceHub.recordTransactions(usedStatesToRecord, listOf(tx)) } else { diff --git a/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt b/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt index 8139b3b0a4..a0bd6121d1 100644 --- a/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt +++ b/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt @@ -3,10 +3,19 @@ package net.corda.node.services.api import net.corda.core.concurrent.CordaFuture import net.corda.core.context.InvocationContext import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.TransactionSignature import net.corda.core.flows.FlowLogic +import net.corda.core.flows.FlowTransactionMetadata import net.corda.core.flows.StateMachineRunId -import net.corda.core.internal.* +import net.corda.core.flows.TransactionStatus +import net.corda.core.internal.FlowStateMachineHandle +import net.corda.core.internal.NamedCacheFactory +import net.corda.core.internal.ResolveTransactionsFlow +import net.corda.core.internal.ServiceHubCoreInternal +import net.corda.core.internal.TransactionsResolver import net.corda.core.internal.concurrent.OpenFuture +import net.corda.core.internal.dependencies +import net.corda.core.internal.requireSupportedHashType import net.corda.core.messaging.DataFeed import net.corda.core.messaging.StateMachineTransactionMapping import net.corda.core.node.NodeInfo @@ -15,6 +24,7 @@ import net.corda.core.node.services.NetworkMapCache import net.corda.core.node.services.NetworkMapCacheBase import net.corda.core.node.services.TransactionStorage import net.corda.core.transactions.SignedTransaction +import net.corda.core.transactions.WireTransaction import net.corda.core.utilities.contextLogger import net.corda.node.internal.InitiatedFlowFactory import net.corda.node.internal.cordapp.CordappProviderInternal @@ -27,7 +37,11 @@ import net.corda.node.services.statemachine.ExternalEvent import net.corda.node.services.statemachine.FlowStateMachineImpl import net.corda.nodeapi.internal.persistence.CordaPersistence import java.security.PublicKey -import java.util.* +import java.util.ArrayList +import java.util.Collections +import java.util.HashMap +import java.util.HashSet +import java.util.LinkedHashSet interface NetworkMapCacheInternal : NetworkMapCache, NetworkMapCacheBase { override val nodeReady: OpenFuture @@ -63,12 +77,15 @@ interface ServiceHubInternal : ServiceHubCoreInternal { return sort.complete() } + @Suppress("LongParameterList") fun recordTransactions(statesToRecord: StatesToRecord, txs: Collection, validatedTransactions: WritableTransactionStorage, stateMachineRecordedTransactionMapping: StateMachineRecordedTransactionMappingStorage, vaultService: VaultServiceInternal, - database: CordaPersistence) { + database: CordaPersistence, + updateFn: (SignedTransaction) -> Boolean = validatedTransactions::addTransaction + ) { database.transaction { require(txs.isNotEmpty()) { "No transactions passed in for recording" } @@ -79,9 +96,9 @@ interface ServiceHubInternal : ServiceHubCoreInternal { // for transactions being recorded at ONLY_RELEVANT, if this transaction has been seen before its outputs should already // have been recorded at ONLY_RELEVANT, so there shouldn't be anything to re-record here. val (recordedTransactions, previouslySeenTxs) = if (statesToRecord != StatesToRecord.ALL_VISIBLE) { - orderedTxs.filter(validatedTransactions::addTransaction) to emptyList() + orderedTxs.filter(updateFn) to emptyList() } else { - orderedTxs.partition(validatedTransactions::addTransaction) + orderedTxs.partition(updateFn) } val stateMachineRunId = FlowStateMachineImpl.currentStateMachine()?.id if (stateMachineRunId != null) { @@ -129,6 +146,22 @@ interface ServiceHubInternal : ServiceHubCoreInternal { vaultService.notifyAll(statesToRecord, recordedTransactions.map { it.coreTransaction }, previouslySeenTxs.map { it.coreTransaction }) } } + + @Suppress("LongParameterList") + fun finalizeTransactionWithExtraSignatures(statesToRecord: StatesToRecord, + txn: SignedTransaction, + sigs: Collection, + validatedTransactions: WritableTransactionStorage, + stateMachineRecordedTransactionMapping: StateMachineRecordedTransactionMappingStorage, + vaultService: VaultServiceInternal, + database: CordaPersistence) { + database.transaction { + require(sigs.isNotEmpty()) { "No signatures passed in for recording" } + recordTransactions(statesToRecord, listOf(txn), validatedTransactions, stateMachineRecordedTransactionMapping, vaultService, database) { + validatedTransactions.finalizeTransactionWithExtraSignatures(it, sigs) + } + } + } } override val attachments: AttachmentStorageInternal @@ -156,7 +189,9 @@ interface ServiceHubInternal : ServiceHubCoreInternal { val cacheFactory: NamedCacheFactory override fun recordTransactions(statesToRecord: StatesToRecord, txs: Iterable) { - txs.forEach { requireSupportedHashType(it) } + txs.forEach { + requireSupportedHashType(it) + } recordTransactions( statesToRecord, txs as? Collection ?: txs.toList(), // We can't change txs to a Collection as it's now part of the public API @@ -167,6 +202,32 @@ interface ServiceHubInternal : ServiceHubCoreInternal { ) } + override fun finalizeTransactionWithExtraSignatures(txn: SignedTransaction, sigs: Collection, statesToRecord: StatesToRecord) { + requireSupportedHashType(txn) + if (txn.coreTransaction is WireTransaction) + (txn + sigs).verifyRequiredSignatures() + finalizeTransactionWithExtraSignatures( + statesToRecord, + txn, + sigs, + validatedTransactions, + stateMachineRecordedTransactionMapping, + vaultService, + database + ) + } + + override fun recordUnnotarisedTransaction(txn: SignedTransaction, metadata: FlowTransactionMetadata?) { + if (txn.coreTransaction is WireTransaction) { + txn.notary?.let { notary -> + txn.verifySignaturesExcept(notary.owningKey) + } ?: txn.verifyRequiredSignatures() + } + database.transaction { + validatedTransactions.addUnnotarisedTransaction(txn, metadata) + } + } + override fun createTransactionsResolver(flow: ResolveTransactionsFlow): TransactionsResolver = DbTransactionsResolver(flow) /** @@ -253,16 +314,33 @@ interface WritableTransactionStorage : TransactionStorage { // TODO: Throw an exception if trying to add a transaction with fewer signatures than an existing entry. fun addTransaction(transaction: SignedTransaction): Boolean + /** + * Add an un-notarised transaction to the store with a status of *MISSING_TRANSACTION_SIG*. + * Optionally add finality flow recovery metadata. + * @param transaction The transaction to be recorded. + * @param metadata Finality flow recovery metadata. + * @return true if the transaction was recorded as a *new* transaction, false if the transaction already exists. + */ + fun addUnnotarisedTransaction(transaction: SignedTransaction, metadata: FlowTransactionMetadata? = null): Boolean + + /** + * Update a previously un-notarised transaction including associated notary signatures. + * @param transaction The notarised transaction to be finalized. + * @param signatures The notary signatures. + * @return true if the transaction is recorded as a *finalized* transaction, false if the transaction already exists. + */ + fun finalizeTransactionWithExtraSignatures(transaction: SignedTransaction, signatures: Collection) : Boolean + /** * Add a new *unverified* transaction to the store. */ fun addUnverifiedTransaction(transaction: SignedTransaction) /** - * Return the transaction with the given ID from the store, and a flag of whether it's verified. Returns null if no transaction with the - * ID exists. + * Return the transaction with the given ID from the store, and its associated [TransactionStatus]. + * Returns null if no transaction with the ID exists. */ - fun getTransactionInternal(id: SecureHash): Pair? + fun getTransactionInternal(id: SecureHash): Pair? /** * Returns a future that completes with the transaction corresponding to [id] once it has been committed. Do not warn when run inside diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt index 24046f2941..a9651af587 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt @@ -3,17 +3,26 @@ package net.corda.node.services.persistence import net.corda.core.concurrent.CordaFuture import net.corda.core.crypto.SecureHash import net.corda.core.crypto.TransactionSignature +import net.corda.core.flows.FlowTransactionMetadata +import net.corda.core.identity.CordaX500Name import net.corda.core.internal.NamedCacheFactory import net.corda.core.internal.ThreadBox import net.corda.core.internal.VisibleForTesting import net.corda.core.internal.bufferUntilSubscribed import net.corda.core.internal.concurrent.doneFuture import net.corda.core.messaging.DataFeed -import net.corda.core.serialization.* +import net.corda.core.node.StatesToRecord +import net.corda.core.serialization.SerializationContext +import net.corda.core.serialization.SerializationDefaults +import net.corda.core.serialization.SerializedBytes +import net.corda.core.serialization.SingletonSerializeAsToken +import net.corda.core.serialization.deserialize import net.corda.core.serialization.internal.effectiveSerializationEnv +import net.corda.core.serialization.serialize import net.corda.core.toFuture import net.corda.core.transactions.CoreTransaction import net.corda.core.transactions.SignedTransaction +import net.corda.core.transactions.WireTransaction import net.corda.core.utilities.contextLogger import net.corda.core.utilities.debug import net.corda.node.CordaClock @@ -21,22 +30,35 @@ import net.corda.node.services.api.WritableTransactionStorage import net.corda.node.services.statemachine.FlowStateMachineImpl import net.corda.node.utilities.AppendOnlyPersistentMapBase import net.corda.node.utilities.WeightBasedAppendOnlyPersistentMap -import net.corda.nodeapi.internal.persistence.* +import net.corda.nodeapi.internal.persistence.CordaPersistence +import net.corda.nodeapi.internal.persistence.NODE_DATABASE_PREFIX +import net.corda.nodeapi.internal.persistence.bufferUntilDatabaseCommit +import net.corda.nodeapi.internal.persistence.contextTransactionOrNull +import net.corda.nodeapi.internal.persistence.currentDBSession +import net.corda.nodeapi.internal.persistence.wrapWithDatabaseTransaction import net.corda.serialization.internal.CordaSerializationEncoding.SNAPPY import rx.Observable import rx.subjects.PublishSubject import java.time.Instant -import java.util.* -import javax.persistence.* +import java.util.Collections +import javax.persistence.AttributeConverter +import javax.persistence.Column +import javax.persistence.Convert +import javax.persistence.Converter +import javax.persistence.Entity +import javax.persistence.Id +import javax.persistence.Lob +import javax.persistence.Table import kotlin.streams.toList +@Suppress("TooManyFunctions") class DBTransactionStorage(private val database: CordaPersistence, cacheFactory: NamedCacheFactory, private val clock: CordaClock) : WritableTransactionStorage, SingletonSerializeAsToken() { @Suppress("MagicNumber") // database column width @Entity @Table(name = "${NODE_DATABASE_PREFIX}transactions") - class DBTransaction( + data class DBTransaction( @Id @Column(name = "tx_id", length = 144, nullable = false) val txId: String, @@ -53,17 +75,41 @@ class DBTransactionStorage(private val database: CordaPersistence, cacheFactory: val status: TransactionStatus, @Column(name = "timestamp", nullable = false) - val timestamp: Instant - ) + val timestamp: Instant, + + @Column(name = "signatures") + val signatures: ByteArray?, + + /** + * Flow finality metadata used for recovery + * TODO: create association table solely for Flow metadata and recovery purposes. + * See https://r3-cev.atlassian.net/browse/ENT-9521 + */ + + /** X500Name of flow initiator **/ + @Column(name = "initiator") + val initiator: String? = null, + + /** X500Name of flow participant parties **/ + @Column(name = "participants") + @Convert(converter = StringListConverter::class) + val participants: List? = null, + + /** states to record: NONE, ALL_VISIBLE, ONLY_RELEVANT */ + @Column(name = "states_to_record") + val statesToRecord: StatesToRecord? = null + ) enum class TransactionStatus { UNVERIFIED, - VERIFIED; + VERIFIED, + MISSING_NOTARY_SIG; fun toDatabaseValue(): String { return when (this) { UNVERIFIED -> "U" VERIFIED -> "V" + MISSING_NOTARY_SIG -> "M" } } @@ -71,11 +117,20 @@ class DBTransactionStorage(private val database: CordaPersistence, cacheFactory: return this == VERIFIED } + fun toTransactionStatus(): net.corda.core.flows.TransactionStatus { + return when(this) { + UNVERIFIED -> net.corda.core.flows.TransactionStatus.UNVERIFIED + VERIFIED -> net.corda.core.flows.TransactionStatus.VERIFIED + MISSING_NOTARY_SIG -> net.corda.core.flows.TransactionStatus.MISSING_NOTARY_SIG + } + } + companion object { fun fromDatabaseValue(databaseValue: String): TransactionStatus { return when (databaseValue) { "V" -> VERIFIED "U" -> UNVERIFIED + "M" -> MISSING_NOTARY_SIG else -> throw UnexpectedStatusValueException(databaseValue) } } @@ -95,6 +150,21 @@ class DBTransactionStorage(private val database: CordaPersistence, cacheFactory: } } + @Converter + class StringListConverter : AttributeConverter?, String?> { + override fun convertToDatabaseColumn(stringList: List?): String? { + return stringList?.let { if (it.isEmpty()) null else it.joinToString(SPLIT_CHAR) } + } + + override fun convertToEntityAttribute(string: String?): List? { + return string?.split(SPLIT_CHAR) + } + + companion object { + private const val SPLIT_CHAR = ";" + } + } + internal companion object { const val TRANSACTION_ALREADY_IN_PROGRESS_WARNING = "trackTransaction is called with an already existing, open DB transaction. As a result, there might be transactions missing from the returned data feed, because of race conditions." @@ -107,7 +177,7 @@ class DBTransactionStorage(private val database: CordaPersistence, cacheFactory: private val logger = contextLogger() - private fun contextToUse(): SerializationContext { + fun contextToUse(): SerializationContext { return if (effectiveSerializationEnv.serializationFactory.currentContext?.useCase == SerializationContext.UseCase.Storage) { effectiveSerializationEnv.serializationFactory.currentContext!! } else { @@ -121,10 +191,19 @@ class DBTransactionStorage(private val database: CordaPersistence, cacheFactory: cacheFactory = cacheFactory, name = "DBTransactionStorage_transactions", toPersistentEntityKey = SecureHash::toString, - fromPersistentEntity = { - SecureHash.create(it.txId) to TxCacheValue( - it.transaction.deserialize(context = contextToUse()), - it.status) + fromPersistentEntity = { dbTxn -> + SecureHash.create(dbTxn.txId) to TxCacheValue( + dbTxn.transaction.deserialize(context = contextToUse()), + dbTxn.status, + dbTxn.signatures?.deserialize(context = contextToUse()), + dbTxn.initiator?.let { initiator -> + FlowTransactionMetadata( + CordaX500Name.parse(initiator), + dbTxn.statesToRecord!!, + dbTxn.participants?.let { it.map { CordaX500Name.parse(it) }.toSet() } + ) + } + ) }, toPersistentEntity = { key: SecureHash, value: TxCacheValue -> DBTransaction( @@ -132,7 +211,11 @@ class DBTransactionStorage(private val database: CordaPersistence, cacheFactory: stateMachineRunId = FlowStateMachineImpl.currentStateMachine()?.id?.uuid?.toString(), transaction = value.toSignedTx().serialize(context = contextToUse().withEncoding(SNAPPY)).bytes, status = value.status, - timestamp = clock.instant() + timestamp = clock.instant(), + signatures = value.sigs.serialize(context = contextToUse().withEncoding(SNAPPY)).bytes, + statesToRecord = value.metadata?.statesToRecord, + initiator = value.metadata?.initiator?.toString(), + participants = value.metadata?.peers?.map { it.toString() } ) }, persistentEntityClass = DBTransaction::class.java, @@ -158,19 +241,43 @@ class DBTransactionStorage(private val database: CordaPersistence, cacheFactory: criteriaUpdate.set(updateRoot.get(DBTransaction::status.name), TransactionStatus.VERIFIED) criteriaUpdate.where(criteriaBuilder.and( criteriaBuilder.equal(updateRoot.get(DBTransaction::txId.name), txId.toString()), - criteriaBuilder.equal(updateRoot.get(DBTransaction::status.name), TransactionStatus.UNVERIFIED) - )) + criteriaBuilder.and(updateRoot.get(DBTransaction::status.name).`in`(setOf(TransactionStatus.UNVERIFIED, TransactionStatus.MISSING_NOTARY_SIG)) + ))) criteriaUpdate.set(updateRoot.get(DBTransaction::timestamp.name), clock.instant()) val update = session.createQuery(criteriaUpdate) val rowsUpdated = update.executeUpdate() return rowsUpdated != 0 } - override fun addTransaction(transaction: SignedTransaction): Boolean { + override fun addTransaction(transaction: SignedTransaction) = + addTransaction(transaction) { + updateTransaction(transaction.id) + } + + override fun addUnnotarisedTransaction(transaction: SignedTransaction, metadata: FlowTransactionMetadata?) = + database.transaction { + txStorage.locked { + val cacheValue = TxCacheValue(transaction, status = TransactionStatus.MISSING_NOTARY_SIG, metadata = metadata) + val added = addWithDuplicatesAllowed(transaction.id, cacheValue) + if (added) { + logger.info ("Transaction ${transaction.id} recorded as un-notarised.") + } else { + logger.info("Transaction ${transaction.id} (un-notarised) already exists so no need to record.") + } + added + } + } + + override fun finalizeTransactionWithExtraSignatures(transaction: SignedTransaction, signatures: Collection) = + addTransaction(transaction + signatures) { + finalizeTransactionWithExtraSignatures(transaction.id, signatures) + } + + private fun addTransaction(transaction: SignedTransaction, updateFn: (SecureHash) -> Boolean): Boolean { return database.transaction { txStorage.locked { val cachedValue = TxCacheValue(transaction, TransactionStatus.VERIFIED) - val addedOrUpdated = addOrUpdate(transaction.id, cachedValue) { k, _ -> updateTransaction(k) } + val addedOrUpdated = addOrUpdate(transaction.id, cachedValue) { k, _ -> updateFn(k) } if (addedOrUpdated) { logger.debug { "Transaction ${transaction.id} has been recorded as verified" } onNewTx(transaction) @@ -182,6 +289,40 @@ class DBTransactionStorage(private val database: CordaPersistence, cacheFactory: } } + private fun finalizeTransactionWithExtraSignatures(txId: SecureHash, signatures: Collection): Boolean { + return txStorage.locked { + val session = currentDBSession() + val criteriaBuilder = session.criteriaBuilder + val criteriaUpdate = criteriaBuilder.createCriteriaUpdate(DBTransaction::class.java) + val updateRoot = criteriaUpdate.from(DBTransaction::class.java) + criteriaUpdate.set(updateRoot.get(DBTransaction::signatures.name), signatures.serialize(context = contextToUse().withEncoding(SNAPPY)).bytes) + criteriaUpdate.set(updateRoot.get(DBTransaction::status.name), TransactionStatus.VERIFIED) + criteriaUpdate.where(criteriaBuilder.and( + criteriaBuilder.equal(updateRoot.get(DBTransaction::txId.name), txId.toString()), + criteriaBuilder.equal(updateRoot.get(DBTransaction::status.name), TransactionStatus.MISSING_NOTARY_SIG) + )) + criteriaUpdate.set(updateRoot.get(DBTransaction::timestamp.name), clock.instant()) + val update = session.createQuery(criteriaUpdate) + val rowsUpdated = update.executeUpdate() + if (rowsUpdated == 0) { + // indicates race condition whereby ReceiverFinality MISSING_NOTARY_SIG overwritten to UNVERIFIED by ResolveTransactionsFlow (in follow-up txn) + // TO-DO: ensure unverified txn signatures are validated prior to recording (https://r3-cev.atlassian.net/browse/ENT-9566) + val criteriaUpdateUnverified = criteriaBuilder.createCriteriaUpdate(DBTransaction::class.java) + val updateRootUnverified = criteriaUpdateUnverified.from(DBTransaction::class.java) + criteriaUpdateUnverified.set(updateRootUnverified.get(DBTransaction::signatures.name), signatures.serialize(context = contextToUse().withEncoding(SNAPPY)).bytes) + criteriaUpdateUnverified.set(updateRootUnverified.get(DBTransaction::status.name), TransactionStatus.VERIFIED) + criteriaUpdateUnverified.where(criteriaBuilder.and( + criteriaBuilder.equal(updateRootUnverified.get(DBTransaction::txId.name), txId.toString()), + criteriaBuilder.equal(updateRootUnverified.get(DBTransaction::status.name), TransactionStatus.UNVERIFIED) + )) + criteriaUpdateUnverified.set(updateRootUnverified.get(DBTransaction::timestamp.name), clock.instant()) + val updateUnverified = session.createQuery(criteriaUpdateUnverified) + val rowsUpdatedUnverified = updateUnverified.executeUpdate() + rowsUpdatedUnverified != 0 + } else true + } + } + private fun onNewTx(transaction: SignedTransaction): Boolean { updatesPublisher.bufferUntilDatabaseCommit().onNext(transaction) return true @@ -194,10 +335,18 @@ class DBTransactionStorage(private val database: CordaPersistence, cacheFactory: } override fun addUnverifiedTransaction(transaction: SignedTransaction) { + if (transaction.coreTransaction is WireTransaction) + transaction.verifyRequiredSignatures() database.transaction { txStorage.locked { val cacheValue = TxCacheValue(transaction, status = TransactionStatus.UNVERIFIED) - val added = addWithDuplicatesAllowed(transaction.id, cacheValue) + val added = addWithDuplicatesAllowed(transaction.id, cacheValue) { k, v, existingEntry -> + if (existingEntry.status == TransactionStatus.MISSING_NOTARY_SIG) { + // TODO verify signatures on passed in transaction include notary (See https://r3-cev.atlassian.net/browse/ENT-9566)) + session.merge(toPersistentEntity(k, v)) + true + } else false + } if (added) { logger.debug { "Transaction ${transaction.id} recorded as unverified." } } else { @@ -207,9 +356,9 @@ class DBTransactionStorage(private val database: CordaPersistence, cacheFactory: } } - override fun getTransactionInternal(id: SecureHash): Pair? { + override fun getTransactionInternal(id: SecureHash): Pair? { return database.transaction { - txStorage.content[id]?.let { it.toSignedTx() to it.status.isVerified() } + txStorage.content[id]?.let { it.toSignedTx() to it.status.toTransactionStatus() } } } @@ -269,16 +418,30 @@ class DBTransactionStorage(private val database: CordaPersistence, cacheFactory: } // Cache value type to just store the immutable bits of a signed transaction plus conversion helpers - private data class TxCacheValue( + private class TxCacheValue( val txBits: SerializedBytes, val sigs: List, - val status: TransactionStatus + val status: TransactionStatus, + // flow metadata recorded for recovery + val metadata: FlowTransactionMetadata? = null ) { constructor(stx: SignedTransaction, status: TransactionStatus) : this( stx.txBits, Collections.unmodifiableList(stx.sigs), - status) - + status + ) + constructor(stx: SignedTransaction, status: TransactionStatus, metadata: FlowTransactionMetadata?) : this( + stx.txBits, + Collections.unmodifiableList(stx.sigs), + status, + metadata + ) + constructor(stx: SignedTransaction, status: TransactionStatus, sigs: List?, metadata: FlowTransactionMetadata?) : this( + stx.txBits, + if (sigs == null) Collections.unmodifiableList(stx.sigs) else Collections.unmodifiableList(stx.sigs + sigs).distinct(), + status, + metadata + ) fun toSignedTx() = SignedTransaction(txBits, sigs) } } diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/StaffedFlowHospital.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/StaffedFlowHospital.kt index 2d314e9c3b..e853cd66a5 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/StaffedFlowHospital.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/StaffedFlowHospital.kt @@ -469,6 +469,7 @@ class StaffedFlowHospital(private val flowMessaging: FlowMessaging, } object FinalityDoctor : Staff { + @Suppress("ComplexMethod") override fun consult(flowFiber: FlowFiber, currentState: StateMachineState, newError: Throwable, history: FlowMedicalHistory): Diagnosis { return if (currentState.flowLogic is FinalityHandler) { log.warn("Flow ${flowFiber.id} failed to be finalised. Manual intervention may be required before retrying " + @@ -480,10 +481,18 @@ class StaffedFlowHospital(private val flowMessaging: FlowMessaging, // no need to keep around the flow, since notarisation has already failed at the counterparty. Diagnosis.NOT_MY_SPECIALTY } + isErrorPropagatedFromCounterparty(newError) && isErrorThrownDuringReceiveFinalityFlow(newError) -> { + // no need to keep around the flow, since notarisation has already failed at the counterparty. + Diagnosis.NOT_MY_SPECIALTY + } isEndSessionErrorThrownDuringReceiveTransactionFlow(newError) -> { // Typically occurs if the initiating flow catches a notary exception and ends their flow successfully. Diagnosis.NOT_MY_SPECIALTY } + isEndSessionErrorThrownDuringReceiveFinalityFlow(newError) -> { + // Typically occurs if the initiating flow catches a notary exception and ends their flow successfully. + Diagnosis.NOT_MY_SPECIALTY + } else -> { log.warn( "Flow ${flowFiber.id} failed to be finalised. Manual intervention may be required before retrying " + @@ -530,6 +539,19 @@ class StaffedFlowHospital(private val flowMessaging: FlowMessaging, && strippedStacktrace.first().className.startsWith(ReceiveTransactionFlow::class.qualifiedName!!) } + /** + * This method will return true if [ReceiveFinalityFlow] is at the top of the stack during the error. + * This may happen in the post-notarisation logic of Two Phase Finality upon receiving a notarisation exception + * from the peer running [FinalityFlow]. + */ + private fun isErrorThrownDuringReceiveFinalityFlow(error: Throwable): Boolean { + val strippedStacktrace = error.stackTrace + .filterNot { it?.className?.contains("counter-flow exception from peer") ?: false } + .filterNot { it?.className?.startsWith("net.corda.node.services.statemachine.") ?: false } + return strippedStacktrace.isNotEmpty() + && strippedStacktrace.first().className.startsWith(ReceiveFinalityFlow::class.qualifiedName!!) + } + /** * Checks if an end session error exception was thrown and that it did so within [ReceiveTransactionFlow]. * @@ -542,6 +564,15 @@ class StaffedFlowHospital(private val flowMessaging: FlowMessaging, && error.message?.contains(StartedFlowTransition.UNEXPECTED_SESSION_END_MESSAGE) == true && isErrorThrownDuringReceiveTransactionFlow(error) } + + /** + * Checks if an end session error exception was thrown and that it did so within [ReceiveFinalityFlow]. + */ + private fun isEndSessionErrorThrownDuringReceiveFinalityFlow(error: Throwable): Boolean { + return error is UnexpectedFlowEndException + && error.message?.contains(StartedFlowTransition.UNEXPECTED_SESSION_END_MESSAGE) == true + && isErrorThrownDuringReceiveFinalityFlow(error) + } } /** diff --git a/node/src/main/kotlin/net/corda/node/utilities/AppendOnlyPersistentMap.kt b/node/src/main/kotlin/net/corda/node/utilities/AppendOnlyPersistentMap.kt index 570172fa06..27d493b0a4 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/AppendOnlyPersistentMap.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/AppendOnlyPersistentMap.kt @@ -142,18 +142,22 @@ abstract class AppendOnlyPersistentMapBase( * Associates the specified value with the specified key in this map and persists it. * If the map previously contained a committed mapping for the key, the old value is not replaced. It may throw an error from the * underlying storage if this races with another database transaction to store a value for the same key. + * An optional [forceUpdate] function allows performing additional checks/updates on an existingEntry to determine whether the map + * should be updated. * @return true if added key was unique, otherwise false */ - fun addWithDuplicatesAllowed(key: K, value: V, logWarning: Boolean = true): Boolean { + fun addWithDuplicatesAllowed(key: K, value: V, logWarning: Boolean = true, + forceUpdate: (K, V, E) -> Boolean = { _, _, _ -> false }): Boolean { return set(key, value, logWarning) { k, v -> val session = currentDBSession() val existingEntry = session.find(persistentEntityClass, toPersistentEntityKey(k)) if (existingEntry == null) { session.save(toPersistentEntity(k, v)) null - } else { - fromPersistentEntity(existingEntry).second } + else if (!forceUpdate(key, value, existingEntry)) { + fromPersistentEntity(existingEntry).second + } else null } } diff --git a/node/src/main/resources/migration/node-core.changelog-master.xml b/node/src/main/resources/migration/node-core.changelog-master.xml index fe333ea9df..a7949838ce 100644 --- a/node/src/main/resources/migration/node-core.changelog-master.xml +++ b/node/src/main/resources/migration/node-core.changelog-master.xml @@ -28,7 +28,8 @@ - + + diff --git a/node/src/main/resources/migration/node-core.changelog-v23.xml b/node/src/main/resources/migration/node-core.changelog-v23.xml new file mode 100644 index 0000000000..095f5e6bee --- /dev/null +++ b/node/src/main/resources/migration/node-core.changelog-v23.xml @@ -0,0 +1,12 @@ + + + + + + + + + \ No newline at end of file diff --git a/node/src/main/resources/migration/node-core.changelog-v24.xml b/node/src/main/resources/migration/node-core.changelog-v24.xml new file mode 100644 index 0000000000..041633ddcf --- /dev/null +++ b/node/src/main/resources/migration/node-core.changelog-v24.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt b/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt index 0c49ee44ac..cddecab98a 100644 --- a/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt +++ b/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt @@ -2,9 +2,26 @@ package net.corda.node.messaging import co.paralleluniverse.fibers.Suspendable import net.corda.core.concurrent.CordaFuture -import net.corda.core.contracts.* -import net.corda.core.crypto.* -import net.corda.core.flows.* +import net.corda.core.contracts.Amount +import net.corda.core.contracts.ContractState +import net.corda.core.contracts.InsufficientBalanceException +import net.corda.core.contracts.Issued +import net.corda.core.contracts.OwnableState +import net.corda.core.contracts.PartyAndReference +import net.corda.core.contracts.StateAndRef +import net.corda.core.contracts.TransactionVerificationException +import net.corda.core.crypto.Crypto +import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.SignableData +import net.corda.core.crypto.SignatureMetadata +import net.corda.core.crypto.TransactionSignature +import net.corda.core.flows.FlowLogic +import net.corda.core.flows.FlowSession +import net.corda.core.flows.FlowTransactionMetadata +import net.corda.core.flows.InitiatedBy +import net.corda.core.flows.InitiatingFlow +import net.corda.core.flows.StateMachineRunId +import net.corda.core.flows.TransactionStatus import net.corda.core.identity.AbstractParty import net.corda.core.identity.AnonymousParty import net.corda.core.identity.CordaX500Name @@ -38,14 +55,26 @@ import net.corda.node.services.api.WritableTransactionStorage import net.corda.node.services.persistence.DBTransactionStorage import net.corda.node.services.statemachine.Checkpoint import net.corda.nodeapi.internal.persistence.CordaPersistence -import net.corda.testing.core.* +import net.corda.testing.core.ALICE_NAME +import net.corda.testing.core.BOB_NAME +import net.corda.testing.core.BOC_NAME +import net.corda.testing.core.DUMMY_NOTARY_NAME +import net.corda.testing.core.TestIdentity +import net.corda.testing.core.expect +import net.corda.testing.core.expectEvents +import net.corda.testing.core.sequence +import net.corda.testing.core.singleIdentity import net.corda.testing.dsl.LedgerDSL import net.corda.testing.dsl.TestLedgerDSLInterpreter import net.corda.testing.dsl.TestTransactionDSLInterpreter import net.corda.testing.internal.IS_OPENJ9 import net.corda.testing.internal.LogHelper import net.corda.testing.internal.vault.VaultFiller -import net.corda.testing.node.internal.* +import net.corda.testing.node.internal.FINANCE_CONTRACTS_CORDAPP +import net.corda.testing.node.internal.InternalMockNetwork +import net.corda.testing.node.internal.InternalMockNodeParameters +import net.corda.testing.node.internal.TestStartedNode +import net.corda.testing.node.internal.startFlow import net.corda.testing.node.ledger import org.assertj.core.api.Assertions.assertThat import org.junit.After @@ -56,7 +85,11 @@ import org.junit.runner.RunWith import org.junit.runners.Parameterized import rx.Observable import java.io.ByteArrayOutputStream -import java.util.* +import java.util.ArrayList +import java.util.Collections +import java.util.Currency +import java.util.Random +import java.util.UUID import java.util.jar.JarOutputStream import java.util.zip.ZipEntry import kotlin.streams.toList @@ -139,7 +172,7 @@ class TwoPartyTradeFlowTests(private val anonymous: Boolean) { // TODO: Verify that the result was inserted into the transaction database. // assertEquals(bobResult.get(), aliceNode.storage.validatedTransactions[aliceResult.get().id]) - assertEquals(aliceResult.getOrThrow(), bobStateMachine.getOrThrow().resultFuture.getOrThrow()) + assertEquals(aliceResult.getOrThrow().id, (bobStateMachine.getOrThrow().resultFuture.getOrThrow() as SignedTransaction).id) aliceNode.dispose() bobNode.dispose() @@ -285,7 +318,7 @@ class TwoPartyTradeFlowTests(private val anonymous: Boolean) { mockNet.runNetwork() // Bob is now finished and has the same transaction as Alice. - assertThat(bobFuture.getOrThrow()).isEqualTo(aliceFuture.getOrThrow()) + assertThat((bobFuture.getOrThrow() as SignedTransaction).id).isEqualTo((aliceFuture.getOrThrow().id)) assertThat(bobNode.smm.findStateMachines(Buyer::class.java)).isEmpty() bobNode.database.transaction { @@ -768,6 +801,21 @@ class TwoPartyTradeFlowTests(private val anonymous: Boolean) { return true } + override fun addUnnotarisedTransaction(transaction: SignedTransaction, metadata: FlowTransactionMetadata?): Boolean { + database.transaction { + records.add(TxRecord.Add(transaction)) + delegate.addUnnotarisedTransaction(transaction) + } + return true + } + + override fun finalizeTransactionWithExtraSignatures(transaction: SignedTransaction, signatures: Collection) : Boolean { + database.transaction { + delegate.finalizeTransactionWithExtraSignatures(transaction, signatures) + } + return true + } + override fun addUnverifiedTransaction(transaction: SignedTransaction) { database.transaction { delegate.addUnverifiedTransaction(transaction) @@ -781,11 +829,12 @@ class TwoPartyTradeFlowTests(private val anonymous: Boolean) { } } - override fun getTransactionInternal(id: SecureHash): Pair? { + override fun getTransactionInternal(id: SecureHash): Pair? { return database.transaction { delegate.getTransactionInternal(id) } } + } interface TxRecord { diff --git a/node/src/test/kotlin/net/corda/node/migration/VaultStateMigrationTest.kt b/node/src/test/kotlin/net/corda/node/migration/VaultStateMigrationTest.kt index da138f9d15..9688afca81 100644 --- a/node/src/test/kotlin/net/corda/node/migration/VaultStateMigrationTest.kt +++ b/node/src/test/kotlin/net/corda/node/migration/VaultStateMigrationTest.kt @@ -212,7 +212,8 @@ class VaultStateMigrationTest { stateMachineRunId = null, transaction = tx.serialize(context = SerializationDefaults.STORAGE_CONTEXT).bytes, status = DBTransactionStorage.TransactionStatus.VERIFIED, - timestamp = Instant.now() + timestamp = Instant.now(), + signatures = null ) session.save(persistentTx) } diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageTests.kt b/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageTests.kt index 51b400c321..fd086f6ff9 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageTests.kt @@ -1,21 +1,32 @@ package net.corda.node.services.persistence +import junit.framework.TestCase.assertNotNull import junit.framework.TestCase.assertTrue import net.corda.core.concurrent.CordaFuture import net.corda.core.contracts.StateRef import net.corda.core.crypto.Crypto import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.SignableData import net.corda.core.crypto.SignatureMetadata import net.corda.core.crypto.TransactionSignature +import net.corda.core.crypto.sign +import net.corda.core.flows.FlowTransactionMetadata +import net.corda.core.node.StatesToRecord +import net.corda.core.serialization.deserialize import net.corda.core.toFuture import net.corda.core.transactions.SignedTransaction +import net.corda.core.transactions.WireTransaction import net.corda.node.CordaClock import net.corda.node.MutableClock import net.corda.node.SimpleClock +import net.corda.node.services.persistence.DBTransactionStorage.TransactionStatus.MISSING_NOTARY_SIG +import net.corda.node.services.persistence.DBTransactionStorage.TransactionStatus.UNVERIFIED +import net.corda.node.services.persistence.DBTransactionStorage.TransactionStatus.VERIFIED import net.corda.node.services.transactions.PersistentUniquenessProvider import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.testing.core.ALICE_NAME +import net.corda.testing.core.BOB_NAME import net.corda.testing.core.DUMMY_NOTARY_NAME import net.corda.testing.core.SerializationEnvironmentRule import net.corda.testing.core.TestIdentity @@ -32,17 +43,21 @@ import org.junit.Before import org.junit.Rule import org.junit.Test import rx.plugins.RxJavaHooks +import java.security.KeyPair import java.time.Clock import java.time.Instant import java.util.concurrent.Semaphore import java.util.concurrent.TimeUnit import kotlin.concurrent.thread import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNull class DBTransactionStorageTests { private companion object { - val ALICE_PUBKEY = TestIdentity(ALICE_NAME, 70).publicKey - val DUMMY_NOTARY = TestIdentity(DUMMY_NOTARY_NAME, 20).party + val ALICE = TestIdentity(ALICE_NAME, 70) + val BOB_PARTY = TestIdentity(BOB_NAME, 80).party + val DUMMY_NOTARY = TestIdentity(DUMMY_NOTARY_NAME, 20) } @Rule @@ -90,6 +105,140 @@ class DBTransactionStorageTests { assertEquals(now, readTransactionTimestampFromDB(transaction.id)) } + @Test(timeout = 300_000) + fun `create transaction missing notary signature and validate status in db`() { + val now = Instant.ofEpochSecond(333444555L) + val transactionClock = TransactionClock(now) + newTransactionStorage(clock = transactionClock) + val transaction = newTransaction() + transactionStorage.addUnnotarisedTransaction(transaction) + assertEquals(MISSING_NOTARY_SIG, readTransactionFromDB(transaction.id).status) + } + + @Test(timeout = 300_000) + fun `create un-notarised transaction with flow metadata and validate status in db`() { + val now = Instant.ofEpochSecond(333444555L) + val transactionClock = TransactionClock(now) + newTransactionStorage(clock = transactionClock) + val transaction = newTransaction() + transactionStorage.addUnnotarisedTransaction(transaction, FlowTransactionMetadata(ALICE.party.name, StatesToRecord.ALL_VISIBLE, setOf(BOB_PARTY.name))) + val txn = readTransactionFromDB(transaction.id) + assertEquals(MISSING_NOTARY_SIG, txn.status) + assertEquals(StatesToRecord.ALL_VISIBLE, txn.statesToRecord) + assertEquals(ALICE_NAME.toString(), txn.initiator) + assertEquals(listOf(BOB_NAME.toString()), txn.participants) + } + + @Test(timeout = 300_000) + fun `finalize transaction with no prior recording of un-notarised transaction`() { + val now = Instant.ofEpochSecond(333444555L) + val transactionClock = TransactionClock(now) + newTransactionStorage(clock = transactionClock) + val transaction = newTransaction() + transactionStorage.finalizeTransactionWithExtraSignatures(transaction, listOf(notarySig(transaction.id))) + readTransactionFromDB(transaction.id).let { + assertSignatures(it.transaction, it.signatures, transaction.sigs) + assertEquals(VERIFIED, it.status) + } + } + + @Test(timeout = 300_000) + fun `finalize transaction with extra signatures after recording transaction as un-notarised`() { + val now = Instant.ofEpochSecond(333444555L) + val transactionClock = TransactionClock(now) + newTransactionStorage(clock = transactionClock) + val transaction = newTransaction(notarySig = false) + transactionStorage.addUnnotarisedTransaction(transaction) + assertNull(transactionStorage.getTransaction(transaction.id)) + assertEquals(MISSING_NOTARY_SIG, readTransactionFromDB(transaction.id).status) + val notarySig = notarySig(transaction.id) + transactionStorage.finalizeTransactionWithExtraSignatures(transaction, listOf(notarySig)) + readTransactionFromDB(transaction.id).let { + assertSignatures(it.transaction, it.signatures, transaction.sigs + notarySig) + assertEquals(VERIFIED, it.status) + } + } + + @Test(timeout = 300_000) + fun `finalize unverified transaction and verify no additional signatures are added`() { + val now = Instant.ofEpochSecond(333444555L) + val transactionClock = TransactionClock(now) + newTransactionStorage(clock = transactionClock) + val transaction = newTransaction() + transactionStorage.addUnverifiedTransaction(transaction) + assertNull(transactionStorage.getTransaction(transaction.id)) + assertEquals(UNVERIFIED, readTransactionFromDB(transaction.id).status) + // attempt to finalise with another notary signature + transactionStorage.finalizeTransactionWithExtraSignatures(transaction, listOf(notarySig(transaction.id))) + readTransactionFromDB(transaction.id).let { + assertSignatures(it.transaction, it.signatures, transaction.sigs) + assertEquals(VERIFIED, it.status) + } + } + + @Test(timeout = 300_000) + fun `simulate finalize race condition where first transaction trumps follow-up transaction`() { + val now = Instant.ofEpochSecond(333444555L) + val transactionClock = TransactionClock(now) + newTransactionStorage(clock = transactionClock) + val transactionWithoutNotarySig = newTransaction(notarySig = false) + + // txn recorded as un-notarised (simulate ReceiverFinalityFlow in initial flow) + transactionStorage.addUnnotarisedTransaction(transactionWithoutNotarySig) + assertEquals(MISSING_NOTARY_SIG, readTransactionFromDB(transactionWithoutNotarySig.id).status) + + // txn then recorded as unverified (simulate ResolveTransactionFlow in follow-up flow) + val notarySig = notarySig(transactionWithoutNotarySig.id) + transactionStorage.addUnverifiedTransaction(transactionWithoutNotarySig + notarySig) + assertEquals(UNVERIFIED, readTransactionFromDB(transactionWithoutNotarySig.id).status) + + // txn finalised with notary signatures (even though in UNVERIFIED state) + assertTrue(transactionStorage.finalizeTransactionWithExtraSignatures(transactionWithoutNotarySig, listOf(notarySig))) + readTransactionFromDB(transactionWithoutNotarySig.id).let { + assertSignatures(it.transaction, it.signatures, transactionWithoutNotarySig.sigs + notarySig) + assertEquals(VERIFIED, it.status) + } + + // attempt to record follow-up txn + assertFalse(transactionStorage.addTransaction(transactionWithoutNotarySig + notarySig)) + readTransactionFromDB(transactionWithoutNotarySig.id).let { + assertSignatures(it.transaction, it.signatures, transactionWithoutNotarySig.sigs + notarySig) + assertEquals(VERIFIED, it.status) + } + } + + @Test(timeout = 300_000) + fun `simulate finalize race condition where follow-up transaction races ahead of initial transaction`() { + val now = Instant.ofEpochSecond(333444555L) + val transactionClock = TransactionClock(now) + newTransactionStorage(clock = transactionClock) + val transactionWithoutNotarySigs = newTransaction(notarySig = false) + + // txn recorded as un-notarised (simulate ReceiverFinalityFlow in initial flow) + transactionStorage.addUnnotarisedTransaction(transactionWithoutNotarySigs) + assertEquals(MISSING_NOTARY_SIG, readTransactionFromDB(transactionWithoutNotarySigs.id).status) + + // txn then recorded as unverified (simulate ResolveTransactionFlow in follow-up flow) + val notarySig = notarySig(transactionWithoutNotarySigs.id) + val transactionWithNotarySigs = transactionWithoutNotarySigs + notarySig + transactionStorage.addUnverifiedTransaction(transactionWithNotarySigs) + assertEquals(UNVERIFIED, readTransactionFromDB(transactionWithoutNotarySigs.id).status) + + // txn then recorded as verified (simulate ResolveTransactions recording in follow-up flow) + assertTrue(transactionStorage.addTransaction(transactionWithNotarySigs)) + readTransactionFromDB(transactionWithoutNotarySigs.id).let { + assertSignatures(it.transaction, it.signatures, expectedSigs = transactionWithNotarySigs.sigs) + assertEquals(VERIFIED, it.status) + } + + // attempt to finalise original txn + assertFalse(transactionStorage.finalizeTransactionWithExtraSignatures(transactionWithoutNotarySigs, listOf(notarySig))) + readTransactionFromDB(transactionWithoutNotarySigs.id).let { + assertSignatures(it.transaction, it.signatures, expectedSigs = transactionWithNotarySigs.sigs) + assertEquals(VERIFIED, it.status) + } + } + @Test(timeout = 300_000) fun `create unverified then verified transaction and validate timestamps in db`() { val unverifiedTime = Instant.ofEpochSecond(555666777L) @@ -175,6 +324,17 @@ class DBTransactionStorageTests { return fromDb[0].timestamp } + private fun readTransactionFromDB(id: SecureHash): DBTransactionStorage.DBTransaction { + val fromDb = database.transaction { + session.createQuery( + "from ${DBTransactionStorage.DBTransaction::class.java.name} where tx_id = :transactionId", + DBTransactionStorage.DBTransaction::class.java + ).setParameter("transactionId", id.toString()).resultList.map { it } + } + assertEquals(1, fromDb.size) + return fromDb[0] + } + @Test(timeout = 300_000) fun `empty store`() { assertThat(transactionStorage.getTransaction(newTransaction().id)).isNull() @@ -369,7 +529,7 @@ class DBTransactionStorageTests { // Assert - assertThat(result).isNotNull() + assertThat(result).isNotNull assertThat(result?.get(20, TimeUnit.SECONDS)?.id).isEqualTo(signedTransaction.id) } @@ -399,18 +559,36 @@ class DBTransactionStorageTests { assertThat(transactionStorage.getTransaction(transaction.id)).isEqualTo(transaction) } - private fun newTransaction(): SignedTransaction { + private fun newTransaction(notarySig: Boolean = true): SignedTransaction { val wtx = createWireTransaction( inputs = listOf(StateRef(SecureHash.randomSHA256(), 0)), attachments = emptyList(), outputs = emptyList(), - commands = listOf(dummyCommand()), - notary = DUMMY_NOTARY, + commands = listOf(dummyCommand(ALICE.publicKey)), + notary = DUMMY_NOTARY.party, timeWindow = null ) - return SignedTransaction( - wtx, - listOf(TransactionSignature(ByteArray(1), ALICE_PUBKEY, SignatureMetadata(1, Crypto.findSignatureScheme(ALICE_PUBKEY).schemeNumberID))) - ) + return makeSigned(wtx, ALICE.keyPair, notarySig = notarySig) + } + + private fun makeSigned(wtx: WireTransaction, vararg keys: KeyPair, notarySig: Boolean = true): SignedTransaction { + val keySigs = keys.map { it.sign(SignableData(wtx.id, SignatureMetadata(1, Crypto.findSignatureScheme(it.public).schemeNumberID))) } + val sigs = if (notarySig) { + keySigs + notarySig(wtx.id) + } else { + keySigs + } + return SignedTransaction(wtx, sigs) + } + + private fun notarySig(txId: SecureHash) = + DUMMY_NOTARY.keyPair.sign(SignableData(txId, SignatureMetadata(1, Crypto.findSignatureScheme(DUMMY_NOTARY.publicKey).schemeNumberID))) + + private fun assertSignatures(transaction: ByteArray, extraSigs: ByteArray?, + expectedSigs: List) { + assertNotNull(extraSigs) + assertEquals(expectedSigs, + (transaction.deserialize(context = DBTransactionStorage.contextToUse()).sigs + + extraSigs!!.deserialize>()).distinct()) } } diff --git a/node/src/test/kotlin/net/corda/node/services/statemachine/RetryFlowMockTest.kt b/node/src/test/kotlin/net/corda/node/services/statemachine/RetryFlowMockTest.kt index 2e4ccdecc2..608d1ad7f4 100644 --- a/node/src/test/kotlin/net/corda/node/services/statemachine/RetryFlowMockTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/statemachine/RetryFlowMockTest.kt @@ -319,7 +319,7 @@ class RetryFlowMockTest { private fun doInsert() { val tx = DBTransactionStorage.DBTransaction("Foo", null, Utils.EMPTY_BYTES, - DBTransactionStorage.TransactionStatus.VERIFIED, Instant.now()) + DBTransactionStorage.TransactionStatus.VERIFIED, Instant.now(), null) contextTransaction.session.save(tx) } } diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt index 85b3aaceb4..7456a63cd5 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt @@ -13,27 +13,47 @@ import net.corda.core.identity.Party import net.corda.core.identity.PartyAndCertificate import net.corda.core.internal.PLATFORM_VERSION import net.corda.core.internal.requireSupportedHashType +import net.corda.core.internal.telemetry.TelemetryComponent +import net.corda.core.internal.telemetry.TelemetryServiceImpl import net.corda.core.messaging.DataFeed import net.corda.core.messaging.FlowHandle import net.corda.core.messaging.FlowProgressHandle import net.corda.core.messaging.StateMachineTransactionMapping -import net.corda.core.node.* -import net.corda.core.node.services.* +import net.corda.core.node.AppServiceHub +import net.corda.core.node.NetworkParameters +import net.corda.core.node.NodeInfo +import net.corda.core.node.ServiceHub +import net.corda.core.node.ServicesForResolution +import net.corda.core.node.StatesToRecord +import net.corda.core.node.services.ContractUpgradeService +import net.corda.core.node.services.CordaService +import net.corda.core.node.services.IdentityService +import net.corda.core.node.services.KeyManagementService +import net.corda.core.node.services.NetworkMapCache +import net.corda.core.node.services.NetworkParametersService +import net.corda.core.node.services.ServiceLifecycleObserver +import net.corda.core.node.services.TransactionStorage +import net.corda.core.node.services.TransactionVerifierService +import net.corda.core.node.services.VaultService import net.corda.core.node.services.diagnostics.DiagnosticsService -import net.corda.core.internal.telemetry.TelemetryComponent -import net.corda.core.internal.telemetry.TelemetryServiceImpl import net.corda.core.node.services.vault.CordaTransactionSupport import net.corda.core.serialization.SerializeAsToken import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.NetworkHostAndPort +import net.corda.coretesting.internal.DEV_ROOT_CA import net.corda.node.VersionInfo import net.corda.node.internal.ServicesForResolutionImpl import net.corda.node.internal.cordapp.JarScanningCordappLoader -import net.corda.node.services.api.* +import net.corda.node.services.api.SchemaService +import net.corda.node.services.api.ServiceHubInternal +import net.corda.node.services.api.StateMachineRecordedTransactionMappingStorage +import net.corda.node.services.api.VaultServiceInternal +import net.corda.node.services.api.WritableTransactionStorage import net.corda.node.services.diagnostics.NodeDiagnosticsService import net.corda.node.services.identity.InMemoryIdentityService import net.corda.node.services.identity.PersistentIdentityService import net.corda.node.services.keys.BasicHSMKeyManagementService +import net.corda.node.services.network.PersistentNetworkMapCache import net.corda.node.services.persistence.PublicKeyToOwningIdentityCacheImpl import net.corda.node.services.schema.NodeSchemaService import net.corda.node.services.transactions.InMemoryTransactionVerifierService @@ -44,12 +64,16 @@ import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.nodeapi.internal.persistence.contextTransaction import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.core.TestIdentity -import net.corda.coretesting.internal.DEV_ROOT_CA -import net.corda.node.services.network.PersistentNetworkMapCache import net.corda.testing.internal.MockCordappProvider import net.corda.testing.internal.TestingNamedCacheFactory import net.corda.testing.internal.configureDatabase -import net.corda.testing.node.internal.* +import net.corda.testing.node.internal.DriverDSLImpl +import net.corda.testing.node.internal.MockCryptoService +import net.corda.testing.node.internal.MockKeyManagementService +import net.corda.testing.node.internal.MockNetworkParametersStorage +import net.corda.testing.node.internal.MockTransactionStorage +import net.corda.testing.node.internal.cordappsForPackages +import net.corda.testing.node.internal.getCallerPackage import net.corda.testing.services.MockAttachmentStorage import java.io.ByteArrayOutputStream import java.nio.file.Paths @@ -57,7 +81,7 @@ import java.security.KeyPair import java.sql.Connection import java.time.Clock import java.time.Instant -import java.util.* +import java.util.Properties import java.util.function.Consumer import java.util.jar.JarFile import java.util.zip.ZipEntry diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt index 5c430d575e..e5486ffaf9 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt @@ -569,12 +569,15 @@ open class InternalMockNetwork(cordappPackages: List = emptyList(), val allActiveFlows = allNodes.flatMap { it.smm.snapshot() } return allActiveFlows.any { - val flowState = it.snapshot().checkpoint.flowState - flowState is FlowState.Started && when (flowState.flowIORequest) { - is FlowIORequest.ExecuteAsyncOperation -> true - is FlowIORequest.Sleep -> true - else -> false - } + val flowSnapshot = it.snapshot() + if (!flowSnapshot.isFlowResumed && flowSnapshot.isWaitingForFuture) { + val flowState = flowSnapshot.checkpoint.flowState + flowState is FlowState.Started && when (flowState.flowIORequest) { + is FlowIORequest.ExecuteAsyncOperation -> true + is FlowIORequest.Sleep -> true + else -> false + } + } else false } } diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/MockTransactionStorage.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/MockTransactionStorage.kt index c1cebf95e1..c54dceba55 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/MockTransactionStorage.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/MockTransactionStorage.kt @@ -2,12 +2,15 @@ package net.corda.testing.node.internal import net.corda.core.concurrent.CordaFuture import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.TransactionSignature import net.corda.core.internal.concurrent.doneFuture import net.corda.core.messaging.DataFeed import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.toFuture import net.corda.core.transactions.SignedTransaction import net.corda.node.services.api.WritableTransactionStorage +import net.corda.core.flows.FlowTransactionMetadata +import net.corda.core.flows.TransactionStatus import net.corda.testing.node.MockServices import rx.Observable import rx.subjects.PublishSubject @@ -42,11 +45,23 @@ open class MockTransactionStorage : WritableTransactionStorage, SingletonSeriali } override fun addTransaction(transaction: SignedTransaction): Boolean { - val current = txns.putIfAbsent(transaction.id, TxHolder(transaction, isVerified = true)) + val current = txns.putIfAbsent(transaction.id, TxHolder(transaction, status = TransactionStatus.VERIFIED)) return if (current == null) { notify(transaction) } else if (!current.isVerified) { - current.isVerified = true + notify(transaction) + } else { + false + } + } + + override fun addUnnotarisedTransaction(transaction: SignedTransaction, metadata: FlowTransactionMetadata?): Boolean { + return txns.putIfAbsent(transaction.id, TxHolder(transaction, status = TransactionStatus.MISSING_NOTARY_SIG)) == null + } + + override fun finalizeTransactionWithExtraSignatures(transaction: SignedTransaction, signatures: Collection): Boolean { + val current = txns.replace(transaction.id, TxHolder(transaction, status = TransactionStatus.VERIFIED)) + return if (current != null) { notify(transaction) } else { false @@ -54,12 +69,14 @@ open class MockTransactionStorage : WritableTransactionStorage, SingletonSeriali } override fun addUnverifiedTransaction(transaction: SignedTransaction) { - txns.putIfAbsent(transaction.id, TxHolder(transaction, isVerified = false)) + txns.putIfAbsent(transaction.id, TxHolder(transaction, status = TransactionStatus.UNVERIFIED)) } - override fun getTransaction(id: SecureHash): SignedTransaction? = txns[id]?.let { if (it.isVerified) it.stx else null } + override fun getTransaction(id: SecureHash): SignedTransaction? = txns[id]?.let { if (it.status == TransactionStatus.VERIFIED) it.stx else null } - override fun getTransactionInternal(id: SecureHash): Pair? = txns[id]?.let { Pair(it.stx, it.isVerified) } + override fun getTransactionInternal(id: SecureHash): Pair? = txns[id]?.let { Pair(it.stx, it.status) } - private class TxHolder(val stx: SignedTransaction, var isVerified: Boolean) + private class TxHolder(val stx: SignedTransaction, var status: TransactionStatus) { + val isVerified = status == TransactionStatus.VERIFIED + } } \ No newline at end of file diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TestDSL.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TestDSL.kt index 9a25595d63..4ebca902f3 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TestDSL.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TestDSL.kt @@ -6,12 +6,15 @@ import net.corda.core.contracts.* import net.corda.core.cordapp.CordappProvider import net.corda.core.crypto.NullKeys.NULL_SIGNATURE import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.TransactionSignature import net.corda.core.flows.FlowException +import net.corda.core.flows.FlowTransactionMetadata import net.corda.core.identity.Party import net.corda.core.internal.* import net.corda.core.internal.notary.NotaryService import net.corda.core.node.ServiceHub import net.corda.core.node.ServicesForResolution +import net.corda.core.node.StatesToRecord import net.corda.core.node.services.AttachmentId import net.corda.core.node.services.TransactionStorage import net.corda.core.serialization.internal.AttachmentsClassLoaderCache @@ -134,6 +137,10 @@ data class TestTransactionDSLInterpreter private constructor( override val notaryService: NotaryService? = null override val attachmentsClassLoaderCache: AttachmentsClassLoaderCache = AttachmentsClassLoaderCacheImpl(TestingNamedCacheFactory()) + + override fun recordUnnotarisedTransaction(txn: SignedTransaction, metadata: FlowTransactionMetadata?) {} + + override fun finalizeTransactionWithExtraSignatures(txn: SignedTransaction, sigs: Collection, statesToRecord: StatesToRecord) {} } private fun copy(): TestTransactionDSLInterpreter = From b4983597e29c1bd1ab7b6376aeb02366571816c9 Mon Sep 17 00:00:00 2001 From: Jose Coll Date: Fri, 24 Mar 2023 08:55:37 +0000 Subject: [PATCH 011/126] ENT-6875 Two Phase Finality - CLEAN-UP (#7321) * Remove completed TODOs * Prevent mis-leading progress tracker message. --- core/src/main/kotlin/net/corda/core/flows/FinalityFlow.kt | 1 + .../corda/node/services/persistence/DBTransactionStorage.kt | 3 --- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/flows/FinalityFlow.kt b/core/src/main/kotlin/net/corda/core/flows/FinalityFlow.kt index c44654caad..932d2a01f4 100644 --- a/core/src/main/kotlin/net/corda/core/flows/FinalityFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/FinalityFlow.kt @@ -231,6 +231,7 @@ class FinalityFlow private constructor(val transaction: SignedTransaction, serviceHub.telemetryServiceInternal.span("${this::class.java.name}#recordLocallyAndBroadcast", flowLogic = this) { recordUnnotarisedTransaction(tx) logger.info("Recorded transaction without notary signature locally.") + if (sessions.isEmpty()) return progressTracker.currentStep = BROADCASTING_PRE_NOTARISATION sessions.forEach { session -> try { diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt index a9651af587..d9149e1f34 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt @@ -305,8 +305,6 @@ class DBTransactionStorage(private val database: CordaPersistence, cacheFactory: val update = session.createQuery(criteriaUpdate) val rowsUpdated = update.executeUpdate() if (rowsUpdated == 0) { - // indicates race condition whereby ReceiverFinality MISSING_NOTARY_SIG overwritten to UNVERIFIED by ResolveTransactionsFlow (in follow-up txn) - // TO-DO: ensure unverified txn signatures are validated prior to recording (https://r3-cev.atlassian.net/browse/ENT-9566) val criteriaUpdateUnverified = criteriaBuilder.createCriteriaUpdate(DBTransaction::class.java) val updateRootUnverified = criteriaUpdateUnverified.from(DBTransaction::class.java) criteriaUpdateUnverified.set(updateRootUnverified.get(DBTransaction::signatures.name), signatures.serialize(context = contextToUse().withEncoding(SNAPPY)).bytes) @@ -342,7 +340,6 @@ class DBTransactionStorage(private val database: CordaPersistence, cacheFactory: val cacheValue = TxCacheValue(transaction, status = TransactionStatus.UNVERIFIED) val added = addWithDuplicatesAllowed(transaction.id, cacheValue) { k, v, existingEntry -> if (existingEntry.status == TransactionStatus.MISSING_NOTARY_SIG) { - // TODO verify signatures on passed in transaction include notary (See https://r3-cev.atlassian.net/browse/ENT-9566)) session.merge(toPersistentEntity(k, v)) true } else false From 4beeb470df9de1f988057453ff0950f8d05c1e35 Mon Sep 17 00:00:00 2001 From: Jose Coll Date: Tue, 28 Mar 2023 12:48:33 +0100 Subject: [PATCH 012/126] Additional signature verification and validation: recordTransactions() --- .../contracts/ConstraintsPropagationTests.kt | 5 ++- .../internal/ResolveTransactionsFlowTest.kt | 17 +++++---- .../LedgerTransactionQueryTests.kt | 2 +- .../kotlin/net/corda/core/node/ServiceHub.kt | 35 ++++++++++++++++++- ...traintMigrationFromHashConstraintsTests.kt | 5 ++- ...ntMigrationFromWhitelistConstraintTests.kt | 7 ++-- .../SignatureConstraintVersioningTests.kt | 3 +- .../node/services/api/ServiceHubInternal.kt | 27 ++++++++++++-- .../corda/node/services/NotaryChangeTests.kt | 4 ++- .../node/services/vault/VaultWithCashTest.kt | 6 ++-- .../net/corda/testing/node/MockServices.kt | 13 ++++++- .../kotlin/net/corda/testing/dsl/TestDSL.kt | 8 +++-- 12 files changed, 107 insertions(+), 25 deletions(-) diff --git a/core-tests/src/test/kotlin/net/corda/coretests/contracts/ConstraintsPropagationTests.kt b/core-tests/src/test/kotlin/net/corda/coretests/contracts/ConstraintsPropagationTests.kt index 0e77bcbed1..67d0a8e7cb 100644 --- a/core-tests/src/test/kotlin/net/corda/coretests/contracts/ConstraintsPropagationTests.kt +++ b/core-tests/src/test/kotlin/net/corda/coretests/contracts/ConstraintsPropagationTests.kt @@ -10,6 +10,7 @@ import net.corda.core.crypto.SecureHash.Companion.allOnesHash import net.corda.core.crypto.SecureHash.Companion.zeroHash import net.corda.core.crypto.SignableData import net.corda.core.crypto.SignatureMetadata +import net.corda.core.crypto.sign import net.corda.core.identity.AbstractParty import net.corda.core.identity.CordaX500Name import net.corda.core.internal.canBeTransitionedFrom @@ -49,6 +50,7 @@ class ConstraintsPropagationTests { val testSerialization = SerializationEnvironmentRule() private companion object { + val DUMMY_NOTARY_IDENTITY = TestIdentity(DUMMY_NOTARY_NAME, 20) val DUMMY_NOTARY = TestIdentity(DUMMY_NOTARY_NAME, 20).party val ALICE = TestIdentity(CordaX500Name("ALICE", "London", "GB")) val ALICE_PARTY get() = ALICE.party @@ -376,7 +378,8 @@ class ConstraintsPropagationTests { requireSupportedHashType(wireTransaction) val nodeKey = ALICE_PUBKEY val sigs = listOf(keyManagementService.sign( - SignableData(wireTransaction.id, SignatureMetadata(4, Crypto.findSignatureScheme(nodeKey).schemeNumberID)), nodeKey)) + SignableData(wireTransaction.id, SignatureMetadata(4, Crypto.findSignatureScheme(nodeKey).schemeNumberID)), nodeKey), + DUMMY_NOTARY_IDENTITY.keyPair.sign(SignableData(wireTransaction.id, SignatureMetadata(4, Crypto.findSignatureScheme(DUMMY_NOTARY_IDENTITY.publicKey).schemeNumberID)))) recordTransactions(SignedTransaction(wireTransaction, sigs)) } diff --git a/core-tests/src/test/kotlin/net/corda/coretests/internal/ResolveTransactionsFlowTest.kt b/core-tests/src/test/kotlin/net/corda/coretests/internal/ResolveTransactionsFlowTest.kt index ec7986082c..c6b9858914 100644 --- a/core-tests/src/test/kotlin/net/corda/coretests/internal/ResolveTransactionsFlowTest.kt +++ b/core-tests/src/test/kotlin/net/corda/coretests/internal/ResolveTransactionsFlowTest.kt @@ -99,12 +99,17 @@ class ResolveTransactionsFlowTest { // DOCEND 1 @Test(timeout=300_000) - fun `dependency with an error`() { - val stx = makeTransactions(signFirstTX = false).second - val p = TestFlow(setOf(stx.id), megaCorp) - val future = miniCorpNode.startFlow(p) - mockNet.runNetwork() - assertFailsWith(SignedTransaction.SignaturesMissingException::class) { future.getOrThrow() } + fun `dependency with an error fails fast upon prior attempt to record transaction with missing signature`() { + val exception = assertFailsWith(IllegalStateException::class) { + val stx = makeTransactions(signFirstTX = false).second + // fails fast in above operation + // prior to platform version 13, same failure would occur upon transaction resolution + val p = TestFlow(setOf(stx.id), megaCorp) + val future = miniCorpNode.startFlow(p) + mockNet.runNetwork() + future.getOrThrow() + } + assertTrue(exception.cause.toString().contains("SignaturesMissingException")) } @Test(timeout=300_000) diff --git a/core-tests/src/test/kotlin/net/corda/coretests/transactions/LedgerTransactionQueryTests.kt b/core-tests/src/test/kotlin/net/corda/coretests/transactions/LedgerTransactionQueryTests.kt index 81cba121e6..f222925b30 100644 --- a/core-tests/src/test/kotlin/net/corda/coretests/transactions/LedgerTransactionQueryTests.kt +++ b/core-tests/src/test/kotlin/net/corda/coretests/transactions/LedgerTransactionQueryTests.kt @@ -80,7 +80,7 @@ class LedgerTransactionQueryTests { .addOutputState(dummyState, DummyContract.PROGRAM_ID) .addCommand(dummyCommand()) ) - services.recordTransactions(fakeIssueTx) + services.recordTransactions(fakeIssueTx, disableSignatureVerification = true) val dummyStateRef = StateRef(fakeIssueTx.id, 0) return StateAndRef(TransactionState(dummyState, DummyContract.PROGRAM_ID, DUMMY_NOTARY, constraint = AlwaysAcceptAttachmentConstraint), dummyStateRef) } diff --git a/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt b/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt index b1e464de6d..655119fa19 100644 --- a/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt +++ b/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt @@ -10,9 +10,10 @@ import net.corda.core.crypto.SignableData import net.corda.core.crypto.SignatureMetadata import net.corda.core.crypto.TransactionSignature import net.corda.core.flows.ContractUpgradeFlow +import net.corda.core.internal.PlatformVersionSwitches.TWO_PHASE_FINALITY +import net.corda.core.internal.telemetry.TelemetryComponent import net.corda.core.node.services.* import net.corda.core.node.services.diagnostics.DiagnosticsService -import net.corda.core.internal.telemetry.TelemetryComponent import net.corda.core.serialization.SerializeAsToken import net.corda.core.transactions.FilteredTransaction import net.corda.core.transactions.LedgerTransaction @@ -204,6 +205,12 @@ interface ServiceHub : ServicesForResolution { * Stores the given [SignedTransaction]s in the local transaction storage and then sends them to the vault for * further processing if [notifyVault] is true. This is expected to be run within a database transaction. * + * As of platform version [TWO_PHASE_FINALITY] also performs signature verification and will throw an + * [IllegalStateException] with details of the cause of error upon failure. + * Of course, you should not be recording transactions to the ledger that are not fully signed. + * It is possible, but not recommended, to revert to non-signature verification behaviour by setting the system property + * "signature.verification.disabled" to true upon node start-up. + * * @param txs The transactions to record. * @param notifyVault indicate if the vault should be notified for the update. */ @@ -214,6 +221,12 @@ interface ServiceHub : ServicesForResolution { /** * Stores the given [SignedTransaction]s in the local transaction storage and then sends them to the vault for * further processing if [notifyVault] is true. This is expected to be run within a database transaction. + * + * As of platform version [TWO_PHASE_FINALITY] also performs signature verification and will throw an + * [IllegalStateException] with details of the cause of error upon failure. + * Of course, you should not be recording transactions to the ledger that are not fully signed. + * It is possible, but not recommended, to revert to non-signature verification behaviour by setting the system property + * "signature.verification.disabled" to true upon node start-up. */ fun recordTransactions(notifyVault: Boolean, first: SignedTransaction, vararg remaining: SignedTransaction) { recordTransactions(notifyVault, listOf(first, *remaining)) @@ -224,6 +237,12 @@ interface ServiceHub : ServicesForResolution { * further processing if [statesToRecord] is not [StatesToRecord.NONE]. * This is expected to be run within a database transaction. * + * As of platform version [TWO_PHASE_FINALITY] also performs signature verification and will throw an + * [IllegalStateException] with details of the cause of error upon failure. + * Of course, you should not be recording transactions to the ledger that are not fully signed. + * It is possible, but not recommended, to revert to non-signature verification behaviour by setting the system property + * "signature.verification.disabled" to true upon node start-up. + * * @param txs The transactions to record. * @param statesToRecord how the vault should treat the output states of the transaction. */ @@ -232,6 +251,13 @@ interface ServiceHub : ServicesForResolution { /** * Stores the given [SignedTransaction]s in the local transaction storage and then sends them to the vault for * further processing. This is expected to be run within a database transaction. + * + * As of platform version [TWO_PHASE_FINALITY] also performs signature verification and will throw an + * [IllegalStateException] with details of the cause of error upon failure. + * Of course, you should not be recording transactions to the ledger that are not fully signed. + * It is possible, but not recommended, to revert to non-signature verification behaviour by setting the system property + * "signature.verification.disabled" to true upon node start-up. + * */ fun recordTransactions(first: SignedTransaction, vararg remaining: SignedTransaction) { recordTransactions(listOf(first, *remaining)) @@ -240,6 +266,13 @@ interface ServiceHub : ServicesForResolution { /** * Stores the given [SignedTransaction]s in the local transaction storage and then sends them to the vault for * further processing. This is expected to be run within a database transaction. + * + * As of platform version [TWO_PHASE_FINALITY] also performs signature verification and will throw an + * [IllegalStateException] with details of the cause of error upon failure. + * Of course, you should not be recording transactions to the ledger that are not fully signed. + * It is possible, but not recommended, to revert to non-signature verification behaviour by setting the system property + * "signature.verification.disabled" to true upon node start-up. + * */ fun recordTransactions(txs: Iterable) { recordTransactions(StatesToRecord.ONLY_RELEVANT, txs) diff --git a/node/src/integration-test/kotlin/net/corda/contracts/SignatureConstraintMigrationFromHashConstraintsTests.kt b/node/src/integration-test/kotlin/net/corda/contracts/SignatureConstraintMigrationFromHashConstraintsTests.kt index f430a6fa70..a69723cdc3 100644 --- a/node/src/integration-test/kotlin/net/corda/contracts/SignatureConstraintMigrationFromHashConstraintsTests.kt +++ b/node/src/integration-test/kotlin/net/corda/contracts/SignatureConstraintMigrationFromHashConstraintsTests.kt @@ -8,7 +8,6 @@ import net.corda.core.internal.deleteRecursively import net.corda.core.internal.div import net.corda.core.messaging.startFlow import net.corda.core.utilities.getOrThrow -import net.corda.node.flows.isQuasarAgentSpecified import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.core.singleIdentity import net.corda.testing.driver.NodeParameters @@ -28,8 +27,8 @@ open class SignatureConstraintMigrationFromHashConstraintsTests : SignatureConst val stateAndRef: StateAndRef? = internalDriver( inMemoryDB = false, - startNodesInProcess = isQuasarAgentSpecified(), - networkParameters = testNetworkParameters(notaries = emptyList(), minimumPlatformVersion = 4) + networkParameters = testNetworkParameters(notaries = emptyList(), minimumPlatformVersion = 4), + systemProperties = mapOf("net.corda.recordtransaction.signature.verification.disabled" to true.toString()) ) { val nodeName = { val nodeHandle = startNode(NodeParameters(rpcUsers = listOf(user), additionalCordapps = listOf(oldCordapp))).getOrThrow() diff --git a/node/src/integration-test/kotlin/net/corda/contracts/SignatureConstraintMigrationFromWhitelistConstraintTests.kt b/node/src/integration-test/kotlin/net/corda/contracts/SignatureConstraintMigrationFromWhitelistConstraintTests.kt index c3e0688548..8c194ac2a9 100644 --- a/node/src/integration-test/kotlin/net/corda/contracts/SignatureConstraintMigrationFromWhitelistConstraintTests.kt +++ b/node/src/integration-test/kotlin/net/corda/contracts/SignatureConstraintMigrationFromWhitelistConstraintTests.kt @@ -9,7 +9,6 @@ import net.corda.core.internal.deleteRecursively import net.corda.core.internal.div import net.corda.core.messaging.startFlow import net.corda.core.utilities.getOrThrow -import net.corda.node.flows.isQuasarAgentSpecified import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.core.singleIdentity import net.corda.testing.driver.NodeParameters @@ -30,8 +29,8 @@ open class SignatureConstraintMigrationFromWhitelistConstraintTests : Signature val stateAndRef: StateAndRef? = internalDriver( inMemoryDB = false, - startNodesInProcess = isQuasarAgentSpecified(), - networkParameters = testNetworkParameters(notaries = emptyList(), minimumPlatformVersion = 4) + networkParameters = testNetworkParameters(notaries = emptyList(), minimumPlatformVersion = 4), + systemProperties = mapOf("net.corda.recordtransaction.signature.verification.disabled" to true.toString()) ) { val nodeName = { val nodeHandle = startNode(NodeParameters(rpcUsers = listOf(user), additionalCordapps = listOf(oldCordapp))).getOrThrow() @@ -142,7 +141,7 @@ open class SignatureConstraintMigrationFromWhitelistConstraintTests : Signature ) ), systemProperties = emptyMap(), - startNodesInProcess = true, + startNodesInProcess = false, specifyExistingConstraint = true, addAnotherAutomaticConstraintState = true ) diff --git a/node/src/integration-test/kotlin/net/corda/contracts/SignatureConstraintVersioningTests.kt b/node/src/integration-test/kotlin/net/corda/contracts/SignatureConstraintVersioningTests.kt index a574d1d2d6..bd243e7127 100644 --- a/node/src/integration-test/kotlin/net/corda/contracts/SignatureConstraintVersioningTests.kt +++ b/node/src/integration-test/kotlin/net/corda/contracts/SignatureConstraintVersioningTests.kt @@ -74,7 +74,8 @@ open class SignatureConstraintVersioningTests { minimumPlatformVersion = minimumPlatformVersion, whitelistedContractImplementations = whitelistedAttachmentHashes ), - systemProperties = systemProperties + systemProperties = systemProperties + + ("net.corda.recordtransaction.signature.verification.disabled" to true.toString()) ) { // create transaction using first Cordapp val (nodeName, baseDirectory, issuanceTransaction) = createIssuanceTransaction(cordapp) diff --git a/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt b/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt index a0bd6121d1..cf7623f825 100644 --- a/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt +++ b/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt @@ -13,9 +13,11 @@ import net.corda.core.internal.NamedCacheFactory import net.corda.core.internal.ResolveTransactionsFlow import net.corda.core.internal.ServiceHubCoreInternal import net.corda.core.internal.TransactionsResolver +import net.corda.core.internal.VisibleForTesting import net.corda.core.internal.concurrent.OpenFuture import net.corda.core.internal.dependencies import net.corda.core.internal.requireSupportedHashType +import net.corda.core.internal.warnOnce import net.corda.core.messaging.DataFeed import net.corda.core.messaging.StateMachineTransactionMapping import net.corda.core.node.NodeInfo @@ -36,7 +38,8 @@ import net.corda.node.services.persistence.AttachmentStorageInternal import net.corda.node.services.statemachine.ExternalEvent import net.corda.node.services.statemachine.FlowStateMachineImpl import net.corda.nodeapi.internal.persistence.CordaPersistence -import java.security.PublicKey +import java.lang.IllegalStateException +import java.security.SignatureException import java.util.ArrayList import java.util.Collections import java.util.HashMap @@ -67,6 +70,7 @@ interface NetworkMapCacheInternal : NetworkMapCache, NetworkMapCacheBase { interface ServiceHubInternal : ServiceHubCoreInternal { companion object { private val log = contextLogger() + private val SIGNATURE_VERIFICATION_DISABLED = java.lang.Boolean.getBoolean("net.corda.recordtransaction.signature.verification.disabled") private fun topologicalSort(transactions: Collection): Collection { if (transactions.size == 1) return transactions @@ -188,9 +192,28 @@ interface ServiceHubInternal : ServiceHubCoreInternal { fun getFlowFactory(initiatingFlowClass: Class>): InitiatedFlowFactory<*>? val cacheFactory: NamedCacheFactory - override fun recordTransactions(statesToRecord: StatesToRecord, txs: Iterable) { + override fun recordTransactions(statesToRecord: StatesToRecord, txs: Iterable) = + recordTransactions(statesToRecord, txs, SIGNATURE_VERIFICATION_DISABLED) + + @Suppress("NestedBlockDepth") + @VisibleForTesting + fun recordTransactions(statesToRecord: StatesToRecord, txs: Iterable, disableSignatureVerification: Boolean) { txs.forEach { requireSupportedHashType(it) + if (it.coreTransaction is WireTransaction) { + if (disableSignatureVerification) { + log.warnOnce("The current usage of recordTransactions is unsafe." + + "Recording transactions without signature verification may lead to severe problems with ledger consistency.") + } + else { + try { + it.verifyRequiredSignatures() + } + catch (e: SignatureException) { + throw IllegalStateException("Signature verification failed", e) + } + } + } } recordTransactions( statesToRecord, diff --git a/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt b/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt index 883b389072..44b44bcf40 100644 --- a/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt @@ -10,10 +10,12 @@ import net.corda.core.flows.StateReplacementException import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party import net.corda.core.node.ServiceHub +import net.corda.core.node.StatesToRecord import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.WireTransaction import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.seconds +import net.corda.node.services.api.ServiceHubInternal import net.corda.testing.contracts.DummyContract import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.BOB_NAME @@ -224,6 +226,6 @@ fun issueInvalidState(services: ServiceHub, identity: Party, notary: Party): Sta val tx = DummyContract.generateInitial(Random().nextInt(), notary, identity.ref(0)) tx.setTimeWindow(Instant.now(), 30.seconds) val stx = services.signInitialTransaction(tx) - services.recordTransactions(stx) + (services as ServiceHubInternal).recordTransactions(StatesToRecord.ONLY_RELEVANT, listOf(stx), disableSignatureVerification = true) return stx.tx.outRef(0) } \ No newline at end of file diff --git a/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt b/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt index 24c2f89e36..6e7dabd747 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt @@ -382,8 +382,10 @@ class VaultWithCashTest { linearStates.forEach { println(it.state.data.linearId) } //copy transactions to notary - simulates transaction resolution - services.validatedTransactions.getTransaction(deals.first().ref.txhash)?.apply { notaryServices.recordTransactions(this) } - services.validatedTransactions.getTransaction(linearStates.first().ref.txhash)?.apply { notaryServices.recordTransactions(this) } + services.validatedTransactions.getTransaction(deals.first().ref.txhash)?.apply { + notaryServices.recordTransactions(this, disableSignatureVerification = true) } + services.validatedTransactions.getTransaction(linearStates.first().ref.txhash)?.apply { + notaryServices.recordTransactions(this, disableSignatureVerification = true) } // Create a txn consuming different contract types val dummyMoveBuilder = TransactionBuilder(notary = notary).apply { diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt index 7456a63cd5..f8a369ca0d 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt @@ -12,6 +12,7 @@ import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party import net.corda.core.identity.PartyAndCertificate import net.corda.core.internal.PLATFORM_VERSION +import net.corda.core.internal.VisibleForTesting import net.corda.core.internal.requireSupportedHashType import net.corda.core.internal.telemetry.TelemetryComponent import net.corda.core.internal.telemetry.TelemetryServiceImpl @@ -451,8 +452,18 @@ open class MockServices private constructor( val cordappClassloader: ClassLoader get() = cordappLoader.appClassLoader - override fun recordTransactions(statesToRecord: StatesToRecord, txs: Iterable) { + override fun recordTransactions(statesToRecord: StatesToRecord, txs: Iterable) = + recordTransactions(txs, false) + + @VisibleForTesting + fun recordTransactions(txn: SignedTransaction, disableSignatureVerification: Boolean) = + recordTransactions(listOf(txn), disableSignatureVerification) + + @VisibleForTesting + fun recordTransactions(txs: Iterable, disableSignatureVerification: Boolean) { txs.forEach { + if (!disableSignatureVerification) + it.verifyRequiredSignatures() (validatedTransactions as WritableTransactionStorage).addTransaction(it) } } diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TestDSL.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TestDSL.kt index 4ebca902f3..983ca29b8f 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TestDSL.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TestDSL.kt @@ -23,6 +23,7 @@ import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.WireTransaction import net.corda.node.services.DbTransactionsResolver +import net.corda.node.services.api.WritableTransactionStorage import net.corda.node.services.attachments.NodeAttachmentTrustCalculator import net.corda.node.services.persistence.AttachmentStorageInternal import net.corda.testing.core.dummyCommand @@ -367,7 +368,10 @@ data class TestLedgerDSLInterpreter private constructor( override fun verifies(): EnforceVerifyOrFail { try { val usedInputs = mutableSetOf() - services.recordTransactions(transactionsUnverified.map { SignedTransaction(it, listOf(NULL_SIGNATURE)) }) + transactionsUnverified.map { + (services.validatedTransactions as WritableTransactionStorage).addTransaction(SignedTransaction(it, listOf(NULL_SIGNATURE))) + } + for ((_, value) in transactionWithLocations) { val wtx = value.transaction val ltx = wtx.toLedgerTransaction(services) @@ -380,7 +384,7 @@ data class TestLedgerDSLInterpreter private constructor( throw DoubleSpentInputs(txIds) } usedInputs.addAll(wtx.inputs) - services.recordTransactions(SignedTransaction(wtx, listOf(NULL_SIGNATURE))) + (services.validatedTransactions as WritableTransactionStorage).addTransaction(SignedTransaction(wtx, listOf(NULL_SIGNATURE))) } return EnforceVerifyOrFail.Token } catch (exception: TransactionVerificationException) { From 18690dba9483ba584d6fa12053c718cc3538e24f Mon Sep 17 00:00:00 2001 From: Jose Coll Date: Thu, 30 Mar 2023 09:08:09 +0100 Subject: [PATCH 013/126] Update JavaDoc. --- core/src/main/kotlin/net/corda/core/node/ServiceHub.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt b/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt index 655119fa19..30163d6442 100644 --- a/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt +++ b/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt @@ -209,7 +209,7 @@ interface ServiceHub : ServicesForResolution { * [IllegalStateException] with details of the cause of error upon failure. * Of course, you should not be recording transactions to the ledger that are not fully signed. * It is possible, but not recommended, to revert to non-signature verification behaviour by setting the system property - * "signature.verification.disabled" to true upon node start-up. + * "net.corda.recordtransaction.signature.verification.disabled" to true upon node start-up. * * @param txs The transactions to record. * @param notifyVault indicate if the vault should be notified for the update. @@ -226,7 +226,7 @@ interface ServiceHub : ServicesForResolution { * [IllegalStateException] with details of the cause of error upon failure. * Of course, you should not be recording transactions to the ledger that are not fully signed. * It is possible, but not recommended, to revert to non-signature verification behaviour by setting the system property - * "signature.verification.disabled" to true upon node start-up. + * "net.corda.recordtransaction.signature.verification.disabled" to true upon node start-up. */ fun recordTransactions(notifyVault: Boolean, first: SignedTransaction, vararg remaining: SignedTransaction) { recordTransactions(notifyVault, listOf(first, *remaining)) @@ -241,7 +241,7 @@ interface ServiceHub : ServicesForResolution { * [IllegalStateException] with details of the cause of error upon failure. * Of course, you should not be recording transactions to the ledger that are not fully signed. * It is possible, but not recommended, to revert to non-signature verification behaviour by setting the system property - * "signature.verification.disabled" to true upon node start-up. + * "net.corda.recordtransaction.signature.verification.disabled" to true upon node start-up. * * @param txs The transactions to record. * @param statesToRecord how the vault should treat the output states of the transaction. @@ -256,7 +256,7 @@ interface ServiceHub : ServicesForResolution { * [IllegalStateException] with details of the cause of error upon failure. * Of course, you should not be recording transactions to the ledger that are not fully signed. * It is possible, but not recommended, to revert to non-signature verification behaviour by setting the system property - * "signature.verification.disabled" to true upon node start-up. + * "net.corda.recordtransaction.signature.verification.disabled" to true upon node start-up. * */ fun recordTransactions(first: SignedTransaction, vararg remaining: SignedTransaction) { @@ -271,7 +271,7 @@ interface ServiceHub : ServicesForResolution { * [IllegalStateException] with details of the cause of error upon failure. * Of course, you should not be recording transactions to the ledger that are not fully signed. * It is possible, but not recommended, to revert to non-signature verification behaviour by setting the system property - * "signature.verification.disabled" to true upon node start-up. + * "net.corda.recordtransaction.signature.verification.disabled" to true upon node start-up. * */ fun recordTransactions(txs: Iterable) { From 7bd3f5dd33eba746caf918b1d5c37183aca8d732 Mon Sep 17 00:00:00 2001 From: Jose Coll Date: Wed, 19 Apr 2023 15:31:47 +0100 Subject: [PATCH 014/126] ENT-9147 Remove un-notarised transactions upon Double Spend. (#7324) --- .ci/api-current.txt | 2 - .../coretests/flows/FinalityFlowTests.kt | 51 +++++++- .../net/corda/core/flows/FinalityFlow.kt | 113 +++++++++++++----- .../core/internal/ServiceHubCoreInternal.kt | 9 ++ .../services/statemachine/FlowHospitalTest.kt | 18 ++- .../node/services/api/ServiceHubInternal.kt | 12 ++ .../persistence/DBTransactionStorage.kt | 21 ++++ .../node/utilities/AppendOnlyPersistentMap.kt | 3 + .../node/messaging/TwoPartyTradeFlowTests.kt | 6 + .../persistence/DBTransactionStorageTests.kt | 17 +++ .../net/corda/testing/flows/FlowTestsUtils.kt | 11 ++ .../node/internal/MockTransactionStorage.kt | 4 + .../kotlin/net/corda/testing/dsl/TestDSL.kt | 2 + 13 files changed, 226 insertions(+), 43 deletions(-) diff --git a/.ci/api-current.txt b/.ci/api-current.txt index 56123e9a97..b1923bb3d1 100644 --- a/.ci/api-current.txt +++ b/.ci/api-current.txt @@ -2542,9 +2542,7 @@ public final class net.corda.core.flows.FinalityFlow extends net.corda.core.flow public (net.corda.core.transactions.SignedTransaction, java.util.Collection, java.util.Collection, net.corda.core.utilities.ProgressTracker) public (net.corda.core.transactions.SignedTransaction, java.util.Collection, net.corda.core.node.StatesToRecord) public (net.corda.core.transactions.SignedTransaction, java.util.Collection, net.corda.core.node.StatesToRecord, net.corda.core.utilities.ProgressTracker) - public (net.corda.core.transactions.SignedTransaction, java.util.Collection, net.corda.core.node.StatesToRecord, net.corda.core.utilities.ProgressTracker, int, kotlin.jvm.internal.DefaultConstructorMarker) public (net.corda.core.transactions.SignedTransaction, java.util.Collection, net.corda.core.utilities.ProgressTracker) - public (net.corda.core.transactions.SignedTransaction, java.util.Collection, net.corda.core.utilities.ProgressTracker, int, kotlin.jvm.internal.DefaultConstructorMarker) public (net.corda.core.transactions.SignedTransaction, java.util.Set) public (net.corda.core.transactions.SignedTransaction, java.util.Set, net.corda.core.utilities.ProgressTracker) public (net.corda.core.transactions.SignedTransaction, net.corda.core.flows.FlowSession, net.corda.core.flows.FlowSession...) diff --git a/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt b/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt index 0fac4a11ce..cf9663729c 100644 --- a/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt +++ b/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt @@ -45,6 +45,7 @@ import net.corda.finance.contracts.asset.Cash import net.corda.finance.flows.CashIssueFlow import net.corda.finance.flows.CashPaymentFlow import net.corda.finance.issuedBy +import net.corda.node.services.persistence.DBTransactionStorage import net.corda.testing.contracts.DummyContract import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.BOB_NAME @@ -182,13 +183,47 @@ class FinalityFlowTests : WithFinality { } catch (e: NotaryException) { val stxId = (e.error as NotaryError.Conflict).txId - val (_, txnDsStatusAlice) = aliceNode.services.validatedTransactions.getTransactionInternal(stxId) ?: fail() - assertEquals(TransactionStatus.MISSING_NOTARY_SIG, txnDsStatusAlice) + assertNull(aliceNode.services.validatedTransactions.getTransactionInternal(stxId)) + // Note: double spend error not propagated to peers by default val (_, txnDsStatusBob) = bobNode.services.validatedTransactions.getTransactionInternal(stxId) ?: fail() assertEquals(TransactionStatus.MISSING_NOTARY_SIG, txnDsStatusBob) } } + @Test(timeout=300_000) + fun `two phase finality flow double spend transaction with double spend handling`() { + val bobNode = createBob(platformVersion = PlatformVersionSwitches.TWO_PHASE_FINALITY) + + val ref = aliceNode.startFlowAndRunNetwork(IssueFlow(notary)).resultFuture.getOrThrow() + val stx = aliceNode.startFlowAndRunNetwork(SpendFlow(ref, bobNode.info.singleIdentity())).resultFuture.getOrThrow() + + val (_, txnStatusAlice) = aliceNode.services.validatedTransactions.getTransactionInternal(stx.id) ?: fail() + assertEquals(TransactionStatus.VERIFIED, txnStatusAlice) + val (_, txnStatusBob) = bobNode.services.validatedTransactions.getTransactionInternal(stx.id) ?: fail() + assertEquals(TransactionStatus.VERIFIED, txnStatusBob) + + try { + aliceNode.startFlowAndRunNetwork(SpendFlow(ref, bobNode.info.singleIdentity(), handleDoubleSpend = true)).resultFuture.getOrThrow() + } + catch (e: NotaryException) { + val stxId = (e.error as NotaryError.Conflict).txId + assertNull(aliceNode.services.validatedTransactions.getTransactionInternal(stxId)) + assertTxnRemovedFromDatabase(aliceNode, stxId) + assertNull(bobNode.services.validatedTransactions.getTransactionInternal(stxId)) + assertTxnRemovedFromDatabase(bobNode, stxId) + } + } + + private fun assertTxnRemovedFromDatabase(node: TestStartedNode, stxId: SecureHash) { + val fromDb = node.database.transaction { + session.createQuery( + "from ${DBTransactionStorage.DBTransaction::class.java.name} where tx_id = :transactionId", + DBTransactionStorage.DBTransaction::class.java + ).setParameter("transactionId", stxId.toString()).resultList.map { it } + } + assertEquals(0, fromDb.size) + } + @Test(timeout=300_000) fun `two phase finality flow double spend transaction from pre-2PF initiator`() { val bobNode = createBob(platformVersion = PlatformVersionSwitches.TWO_PHASE_FINALITY - 1) @@ -207,7 +242,9 @@ class FinalityFlowTests : WithFinality { catch (e: NotaryException) { val stxId = (e.error as NotaryError.Conflict).txId assertNull(bobNode.services.validatedTransactions.getTransactionInternal(stxId)) + assertTxnRemovedFromDatabase(bobNode, stxId) assertNull(aliceNode.services.validatedTransactions.getTransactionInternal(stxId)) + assertTxnRemovedFromDatabase(aliceNode, stxId) } } @@ -228,9 +265,10 @@ class FinalityFlowTests : WithFinality { } catch (e: NotaryException) { val stxId = (e.error as NotaryError.Conflict).txId - val (_, txnDsStatusAlice) = aliceNode.services.validatedTransactions.getTransactionInternal(stxId) ?: fail() - assertEquals(TransactionStatus.MISSING_NOTARY_SIG, txnDsStatusAlice) + assertNull(aliceNode.services.validatedTransactions.getTransactionInternal(stxId)) + assertTxnRemovedFromDatabase(aliceNode, stxId) assertNull(bobNode.services.validatedTransactions.getTransactionInternal(stxId)) + assertTxnRemovedFromDatabase(bobNode, stxId) } } @@ -281,7 +319,8 @@ class FinalityFlowTests : WithFinality { @StartableByRPC @InitiatingFlow - class SpendFlow(private val stateAndRef: StateAndRef, private val newOwner: Party) : FlowLogic() { + class SpendFlow(private val stateAndRef: StateAndRef, private val newOwner: Party, + private val handleDoubleSpend: Boolean? = null) : FlowLogic() { @Suspendable override fun call(): SignedTransaction { @@ -289,7 +328,7 @@ class FinalityFlowTests : WithFinality { val signedTransaction = serviceHub.signInitialTransaction(txBuilder, ourIdentity.owningKey) val sessionWithCounterParty = initiateFlow(newOwner) sessionWithCounterParty.sendAndReceive("initial-message") - return subFlow(FinalityFlow(signedTransaction, setOf(sessionWithCounterParty))) + return subFlow(FinalityFlow(signedTransaction, setOf(sessionWithCounterParty), handleDoubleSpend = handleDoubleSpend)) } } diff --git a/core/src/main/kotlin/net/corda/core/flows/FinalityFlow.kt b/core/src/main/kotlin/net/corda/core/flows/FinalityFlow.kt index 932d2a01f4..f5a58f7d58 100644 --- a/core/src/main/kotlin/net/corda/core/flows/FinalityFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/FinalityFlow.kt @@ -19,8 +19,10 @@ import net.corda.core.node.StatesToRecord.ONLY_RELEVANT import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.ProgressTracker +import net.corda.core.utilities.Try import net.corda.core.utilities.debug import net.corda.core.utilities.unwrap +import java.time.Duration /** * Verifies the given transaction, then sends it to the named notary. If the notary agrees that the transaction @@ -46,13 +48,15 @@ import net.corda.core.utilities.unwrap // To maintain backwards compatibility with the old API, FinalityFlow can act both as an initiating flow and as an inlined flow. // This is only possible because a flow is only truly initiating when the first call to initiateFlow is made (where the // presence of @InitiatingFlow is checked). So the new API is inlined simply because that code path doesn't call initiateFlow. +@Suppress("TooManyFunctions") @InitiatingFlow class FinalityFlow private constructor(val transaction: SignedTransaction, private val oldParticipants: Collection, override val progressTracker: ProgressTracker, private val sessions: Collection, private val newApi: Boolean, - private val statesToRecord: StatesToRecord = ONLY_RELEVANT) : FlowLogic() { + private val statesToRecord: StatesToRecord = ONLY_RELEVANT, + private val handleDoubleSpend: Boolean? = null) : FlowLogic() { @CordaInternal data class ExtraConstructorArgs(val oldParticipants: Collection, val sessions: Collection, val newApi: Boolean, val statesToRecord: StatesToRecord) @@ -87,13 +91,15 @@ class FinalityFlow private constructor(val transaction: SignedTransaction, * @param transaction What to commit. * @param sessions A collection of [FlowSession]s for each non-local participant of the transaction. Sessions to non-participants can * also be provided. + * @param handleDoubleSpend Whether to catch and propagate Double Spend exception to peers. */ @JvmOverloads constructor( transaction: SignedTransaction, sessions: Collection, - progressTracker: ProgressTracker = tracker() - ) : this(transaction, emptyList(), progressTracker, sessions, true) + progressTracker: ProgressTracker = tracker(), + handleDoubleSpend: Boolean? = null + ) : this(transaction, emptyList(), progressTracker, sessions, true, handleDoubleSpend = handleDoubleSpend) /** * Notarise the given transaction and broadcast it to all the participants. @@ -102,14 +108,16 @@ class FinalityFlow private constructor(val transaction: SignedTransaction, * @param sessions A collection of [FlowSession]s for each non-local participant of the transaction. Sessions to non-participants can * also be provided. * @param statesToRecord Which states to commit to the vault. + * @param handleDoubleSpend Whether to catch and propagate Double Spend exception to peers. */ @JvmOverloads constructor( transaction: SignedTransaction, sessions: Collection, statesToRecord: StatesToRecord, - progressTracker: ProgressTracker = tracker() - ) : this(transaction, emptyList(), progressTracker, sessions, true, statesToRecord) + progressTracker: ProgressTracker = tracker(), + handleDoubleSpend: Boolean? = null + ) : this(transaction, emptyList(), progressTracker, sessions, true, statesToRecord, handleDoubleSpend = handleDoubleSpend) /** * Notarise the given transaction and broadcast it to all the participants. @@ -146,11 +154,13 @@ class FinalityFlow private constructor(val transaction: SignedTransaction, @Suppress("ClassNaming") object BROADCASTING_POST_NOTARISATION : ProgressTracker.Step("Broadcasting notary signature") @Suppress("ClassNaming") + object BROADCASTING_DOUBLE_SPEND_ERROR : ProgressTracker.Step("Broadcasting notary double spend error") + @Suppress("ClassNaming") object FINALISING_TRANSACTION : ProgressTracker.Step("Finalising transaction locally") object BROADCASTING : ProgressTracker.Step("Broadcasting notarised transaction to other participants") @JvmStatic - fun tracker() = ProgressTracker(RECORD_UNNOTARISED, BROADCASTING_PRE_NOTARISATION, NOTARISING, BROADCASTING_POST_NOTARISATION, FINALISING_TRANSACTION, BROADCASTING) + fun tracker() = ProgressTracker(RECORD_UNNOTARISED, BROADCASTING_PRE_NOTARISATION, NOTARISING, BROADCASTING_POST_NOTARISATION, BROADCASTING_DOUBLE_SPEND_ERROR, FINALISING_TRANSACTION, BROADCASTING) } @Suspendable @@ -202,28 +212,39 @@ class FinalityFlow private constructor(val transaction: SignedTransaction, recordLocallyAndBroadcast(newPlatformSessions, transaction) } - val stxn = notariseOrRecord() - val notarySignatures = stxn.sigs - transaction.sigs.toSet() - if (notarySignatures.isNotEmpty()) { - if (useTwoPhaseFinality && newPlatformSessions.isNotEmpty()) { - broadcastSignaturesAndFinalise(newPlatformSessions, notarySignatures) - } - else { - progressTracker.currentStep = FINALISING_TRANSACTION - serviceHub.telemetryServiceInternal.span("${this::class.java.name}#finalizeTransactionWithExtraSignatures", flowLogic = this) { - (serviceHub as ServiceHubCoreInternal).finalizeTransactionWithExtraSignatures(transaction, notarySignatures, statesToRecord) - logger.info("Finalised transaction locally.") + try { + val stxn = notariseOrRecord() + val notarySignatures = stxn.sigs - transaction.sigs.toSet() + if (notarySignatures.isNotEmpty()) { + if (useTwoPhaseFinality && newPlatformSessions.isNotEmpty()) { + broadcastSignaturesAndFinalise(newPlatformSessions, notarySignatures) + } else { + progressTracker.currentStep = FINALISING_TRANSACTION + serviceHub.telemetryServiceInternal.span("${this::class.java.name}#finalizeTransactionWithExtraSignatures", flowLogic = this) { + (serviceHub as ServiceHubCoreInternal).finalizeTransactionWithExtraSignatures(transaction, notarySignatures, statesToRecord) + logger.info("Finalised transaction locally.") + } } } - } - if (!useTwoPhaseFinality || !needsNotarySignature(transaction)) { - broadcastToOtherParticipants(externalTxParticipants, newPlatformSessions + oldPlatformSessions, stxn) - } else if (useTwoPhaseFinality && oldPlatformSessions.isNotEmpty()) { - broadcastToOtherParticipants(externalTxParticipants, oldPlatformSessions, stxn) + if (!useTwoPhaseFinality || !needsNotarySignature(transaction)) { + broadcastToOtherParticipants(externalTxParticipants, newPlatformSessions + oldPlatformSessions, stxn) + } else if (useTwoPhaseFinality && oldPlatformSessions.isNotEmpty()) { + broadcastToOtherParticipants(externalTxParticipants, oldPlatformSessions, stxn) + } + return stxn + } + catch (e: NotaryException) { + if (e.error is NotaryError.Conflict && useTwoPhaseFinality) { + (serviceHub as ServiceHubCoreInternal).removeUnnotarisedTransaction(e.error.txId) + val overrideHandleDoubleSpend = handleDoubleSpend ?: + (serviceHub.cordappProvider.getAppContext().cordapp.targetPlatformVersion >= PlatformVersionSwitches.TWO_PHASE_FINALITY) + if (overrideHandleDoubleSpend && newPlatformSessions.isNotEmpty()) { + broadcastDoubleSpendError(newPlatformSessions, e) + } else sleep(Duration.ZERO) // force checkpoint to persist db update. + } + throw e } - - return stxn } @Suspendable @@ -257,7 +278,7 @@ class FinalityFlow private constructor(val transaction: SignedTransaction, sessions.forEach { session -> try { logger.debug { "Sending notary signature to party $session." } - session.send(notarySignatures) + session.send(Try.Success(notarySignatures)) // remote will finalise txn with notary signature } catch (e: UnexpectedFlowEndException) { throw UnexpectedFlowEndException( @@ -276,6 +297,27 @@ class FinalityFlow private constructor(val transaction: SignedTransaction, } } + @Suspendable + private fun broadcastDoubleSpendError(sessions: Collection, error: NotaryException) { + progressTracker.currentStep = BROADCASTING_DOUBLE_SPEND_ERROR + serviceHub.telemetryServiceInternal.span("${this::class.java.name}#broadcastDoubleSpendError", flowLogic = this) { + logger.info("Broadcasting notary double spend error.") + sessions.forEach { session -> + try { + logger.debug { "Sending notary double spend error to party $session." } + session.send(Try.Failure>(error)) + } catch (e: UnexpectedFlowEndException) { + throw UnexpectedFlowEndException( + "${session.counterparty} has finished prematurely and we're trying to send them a notary double spend error. " + + "Did they forget to call ReceiveFinalityFlow? (${e.message})", + e.cause, + e.originalErrorId + ) + } + } + } + } + @Suspendable private fun broadcastToOtherParticipants(externalTxParticipants: Set, sessions: Collection, tx: SignedTransaction) { if (externalTxParticipants.isEmpty() && sessions.isEmpty() && oldParticipants.isEmpty()) return @@ -433,12 +475,21 @@ class ReceiveFinalityFlow @JvmOverloads constructor(private val otherSideSession } otherSideSession.send(FetchDataFlow.Request.End) // Finish fetching data (deferredAck) logger.info("Peer recorded transaction without notary signature. Waiting to receive notary signature.") - val notarySignatures = otherSideSession.receive>() - .unwrap { it } - serviceHub.telemetryServiceInternal.span("${this::class.java.name}#finalizeTransactionWithExtraSignatures", flowLogic = this) { - logger.debug { "Peer received notarised signature." } - (serviceHub as ServiceHubCoreInternal).finalizeTransactionWithExtraSignatures(stx, notarySignatures, statesToRecord) - logger.info("Peer finalised transaction with notary signature.") + + try { + val notarySignatures = otherSideSession.receive>>().unwrap { it.getOrThrow() } + serviceHub.telemetryServiceInternal.span("${this::class.java.name}#finalizeTransactionWithExtraSignatures", flowLogic = this) { + logger.debug { "Peer received notarised signature." } + (serviceHub as ServiceHubCoreInternal).finalizeTransactionWithExtraSignatures(stx, notarySignatures, statesToRecord) + logger.info("Peer finalised transaction with notary signature.") + } + } catch(throwable: NotaryException) { + if(throwable.error is NotaryError.Conflict) { + logger.info("Peer received double spend error.") + (serviceHub as ServiceHubCoreInternal).removeUnnotarisedTransaction(stx.id) + sleep(Duration.ZERO) // force checkpoint to persist db update. + } + throw throwable } } else { serviceHub.telemetryServiceInternal.span("${this::class.java.name}#recordTransactions", flowLogic = this) { diff --git a/core/src/main/kotlin/net/corda/core/internal/ServiceHubCoreInternal.kt b/core/src/main/kotlin/net/corda/core/internal/ServiceHubCoreInternal.kt index af7ce40179..36b44ed0e1 100644 --- a/core/src/main/kotlin/net/corda/core/internal/ServiceHubCoreInternal.kt +++ b/core/src/main/kotlin/net/corda/core/internal/ServiceHubCoreInternal.kt @@ -2,6 +2,7 @@ package net.corda.core.internal import co.paralleluniverse.fibers.Suspendable import net.corda.core.DeleteForDJVM +import net.corda.core.crypto.SecureHash import net.corda.core.crypto.TransactionSignature import net.corda.core.flows.FlowTransactionMetadata import net.corda.core.internal.notary.NotaryService @@ -37,6 +38,14 @@ interface ServiceHubCoreInternal : ServiceHub { */ fun recordUnnotarisedTransaction(txn: SignedTransaction, metadata: FlowTransactionMetadata?= null) + /** + * Removes transaction from data store. + * This is expected to be run within a database transaction. + * + * @param id of transaction to remove. + */ + fun removeUnnotarisedTransaction(id: SecureHash) + /** * Stores [SignedTransaction] with extra signatures in the local transaction storage * diff --git a/node/src/integration-test/kotlin/net/corda/node/services/statemachine/FlowHospitalTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/statemachine/FlowHospitalTest.kt index 5c17bb7bac..b90f23437e 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/statemachine/FlowHospitalTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/statemachine/FlowHospitalTest.kt @@ -42,6 +42,7 @@ import net.corda.testing.core.CHARLIE_NAME import net.corda.testing.core.singleIdentity import net.corda.testing.driver.DriverParameters import net.corda.testing.driver.driver +import net.corda.testing.flows.waitForAllFlowsToComplete import net.corda.testing.node.User import net.corda.testing.node.internal.CustomCordapp import net.corda.testing.node.internal.DUMMY_CONTRACTS_CORDAPP @@ -247,6 +248,7 @@ class FlowHospitalTest { it.startFlow(::SpendStateAndCatchDoubleSpendFlow, nodeBHandle.nodeInfo.singleIdentity(), ref).returnValue.getOrThrow(20.seconds) it.startFlow(::SpendStateAndCatchDoubleSpendFlow, nodeBHandle.nodeInfo.singleIdentity(), ref).returnValue.getOrThrow(20.seconds) } + waitForAllFlowsToComplete(nodeAHandle) } // 1 is the notary failing to notarise and propagating the error // 2 is the receiving flow failing due to the unexpected session end error @@ -348,6 +350,7 @@ class FlowHospitalTest { val ref3 = it.startFlow(::SpendStateAndCatchDoubleSpendFlow, nodeCHandle.nodeInfo.singleIdentity(), ref2).returnValue.getOrThrow(20.seconds) it.startFlow(::CreateTransactionButDontFinalizeFlow, nodeBHandle.nodeInfo.singleIdentity(), ref3).returnValue.getOrThrow(20.seconds) } + waitForAllFlowsToComplete(nodeAHandle) } assertEquals(0, dischargedCounter) assertEquals(1, observationCounter) @@ -374,6 +377,7 @@ class FlowHospitalTest { it.startFlow(::SpendStateAndCatchDoubleSpendFlow, nodeBHandle.nodeInfo.singleIdentity(), ref).returnValue.getOrThrow(20.seconds) it.startFlow(::SpendStateAndCatchDoubleSpendFlow, nodeBHandle.nodeInfo.singleIdentity(), ref, true).returnValue.getOrThrow(20.seconds) } + waitForAllFlowsToComplete(nodeAHandle) } // 1 is the notary failing to notarise and propagating the error assertEquals(1, dischargedCounter) @@ -552,6 +556,7 @@ class FlowHospitalTest { var exceptionSeenInUserFlow = false } + @Suppress("TooGenericExceptionCaught") @Suspendable override fun call() { val consumeError = session.receive().unwrap { it } @@ -562,10 +567,15 @@ class FlowHospitalTest { }) try { subFlow(ReceiveFinalityFlow(session, stx.id)) - } catch (e: UnexpectedFlowEndException) { - exceptionSeenInUserFlow = true - if (!consumeError) { - throw e + } catch (ex: Exception) { + when (ex) { + is NotaryException, + is UnexpectedFlowEndException -> { + exceptionSeenInUserFlow = true + if (!consumeError) { + throw ex + } + } } } } diff --git a/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt b/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt index cf7623f825..cb917e0b51 100644 --- a/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt +++ b/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt @@ -251,6 +251,12 @@ interface ServiceHubInternal : ServiceHubCoreInternal { } } + override fun removeUnnotarisedTransaction(id: SecureHash) { + database.transaction { + validatedTransactions.removeUnnotarisedTransaction(id) + } + } + override fun createTransactionsResolver(flow: ResolveTransactionsFlow): TransactionsResolver = DbTransactionsResolver(flow) /** @@ -346,6 +352,12 @@ interface WritableTransactionStorage : TransactionStorage { */ fun addUnnotarisedTransaction(transaction: SignedTransaction, metadata: FlowTransactionMetadata? = null): Boolean + /** + * Removes an un-notarised transaction (with a status of *MISSING_TRANSACTION_SIG*) from the data store. + * Returns null if no transaction with the ID exists. + */ + fun removeUnnotarisedTransaction(id: SecureHash): Boolean + /** * Update a previously un-notarised transaction including associated notary signatures. * @param transaction The notarised transaction to be finalized. diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt index d9149e1f34..7773fdcd22 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt @@ -268,6 +268,27 @@ class DBTransactionStorage(private val database: CordaPersistence, cacheFactory: } } + override fun removeUnnotarisedTransaction(id: SecureHash): Boolean { + return database.transaction { + val session = currentDBSession() + val criteriaBuilder = session.criteriaBuilder + val delete = criteriaBuilder.createCriteriaDelete(DBTransaction::class.java) + val root = delete.from(DBTransaction::class.java) + delete.where(criteriaBuilder.and( + criteriaBuilder.equal(root.get(DBTransaction::txId.name), id.toString()), + criteriaBuilder.equal(root.get(DBTransaction::status.name), TransactionStatus.MISSING_NOTARY_SIG) + )) + if (session.createQuery(delete).executeUpdate() != 0) { + txStorage.locked { + txStorage.content.clear(id) + txStorage.content[id] + logger.debug { "Un-notarised transaction $id has been removed." } + } + true + } else false + } + } + override fun finalizeTransactionWithExtraSignatures(transaction: SignedTransaction, signatures: Collection) = addTransaction(transaction + signatures) { finalizeTransactionWithExtraSignatures(transaction.id, signatures) diff --git a/node/src/main/kotlin/net/corda/node/utilities/AppendOnlyPersistentMap.kt b/node/src/main/kotlin/net/corda/node/utilities/AppendOnlyPersistentMap.kt index 27d493b0a4..98de78ac0c 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/AppendOnlyPersistentMap.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/AppendOnlyPersistentMap.kt @@ -2,6 +2,7 @@ package net.corda.node.utilities import com.github.benmanes.caffeine.cache.LoadingCache import com.github.benmanes.caffeine.cache.Weigher +import net.corda.core.crypto.SecureHash import net.corda.core.internal.NamedCacheFactory import net.corda.core.utilities.contextLogger import net.corda.nodeapi.internal.persistence.DatabaseTransaction @@ -248,6 +249,8 @@ abstract class AppendOnlyPersistentMapBase( cache.invalidateAll() } + fun clear(id: SecureHash) = cache.invalidate(id) + // Helpers to know if transaction(s) are currently writing the given key. private fun weAreWriting(key: K): Boolean = pendingKeys[key]?.transactions?.contains(contextTransaction) ?: false diff --git a/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt b/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt index cddecab98a..7e3ed4f2f9 100644 --- a/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt +++ b/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt @@ -809,6 +809,12 @@ class TwoPartyTradeFlowTests(private val anonymous: Boolean) { return true } + override fun removeUnnotarisedTransaction(id: SecureHash): Boolean { + return database.transaction { + delegate.removeUnnotarisedTransaction(id) + } + } + override fun finalizeTransactionWithExtraSignatures(transaction: SignedTransaction, signatures: Collection) : Boolean { database.transaction { delegate.finalizeTransactionWithExtraSignatures(transaction, signatures) diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageTests.kt b/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageTests.kt index fd086f6ff9..16889e886e 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageTests.kt @@ -43,6 +43,7 @@ import org.junit.Before import org.junit.Rule import org.junit.Test import rx.plugins.RxJavaHooks +import java.lang.AssertionError import java.security.KeyPair import java.time.Clock import java.time.Instant @@ -50,6 +51,7 @@ import java.util.concurrent.Semaphore import java.util.concurrent.TimeUnit import kotlin.concurrent.thread import kotlin.test.assertEquals +import kotlin.test.assertFailsWith import kotlin.test.assertFalse import kotlin.test.assertNull @@ -159,6 +161,21 @@ class DBTransactionStorageTests { } } + @Test(timeout = 300_000) + fun `remove un-notarised transaction`() { + val now = Instant.ofEpochSecond(333444555L) + val transactionClock = TransactionClock(now) + newTransactionStorage(clock = transactionClock) + val transaction = newTransaction(notarySig = false) + transactionStorage.addUnnotarisedTransaction(transaction) + assertNull(transactionStorage.getTransaction(transaction.id)) + assertEquals(MISSING_NOTARY_SIG, readTransactionFromDB(transaction.id).status) + + assertEquals(true, transactionStorage.removeUnnotarisedTransaction(transaction.id)) + assertFailsWith { readTransactionFromDB(transaction.id).status } + assertNull(transactionStorage.getTransactionInternal(transaction.id)) + } + @Test(timeout = 300_000) fun `finalize unverified transaction and verify no additional signatures are added`() { val now = Instant.ofEpochSecond(333444555L) diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/flows/FlowTestsUtils.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/flows/FlowTestsUtils.kt index 78dd3a7035..bcbfdf85bc 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/flows/FlowTestsUtils.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/flows/FlowTestsUtils.kt @@ -1,6 +1,7 @@ package net.corda.testing.flows import co.paralleluniverse.fibers.Suspendable +import co.paralleluniverse.strands.Strand import net.corda.core.concurrent.CordaFuture import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowSession @@ -8,6 +9,7 @@ import net.corda.core.toFuture import net.corda.core.utilities.UntrustworthyData import net.corda.core.utilities.unwrap import net.corda.node.internal.InitiatedFlowFactory +import net.corda.testing.driver.NodeHandle import net.corda.testing.node.internal.TestStartedNode import rx.Observable import kotlin.reflect.KClass @@ -95,4 +97,13 @@ fun > TestStartedNode.registerCoreFlowFactory(initiatingFlowCla initiatedFlowClass: Class, flowFactory: (FlowSession) -> T, track: Boolean): Observable { return this.internals.registerInitiatedFlowFactory(initiatingFlowClass, initiatedFlowClass, InitiatedFlowFactory.Core(flowFactory), track) +} + +fun waitForAllFlowsToComplete(nodeHandle: NodeHandle, maxIterations: Int = 60, iterationDelay: Long = 500) { + repeat((0..maxIterations).count()) { + if (nodeHandle.rpc.stateMachinesSnapshot().isEmpty()) { + return + } + Strand.sleep(iterationDelay) + } } \ No newline at end of file diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/MockTransactionStorage.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/MockTransactionStorage.kt index c54dceba55..e0beb1ec00 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/MockTransactionStorage.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/MockTransactionStorage.kt @@ -59,6 +59,10 @@ open class MockTransactionStorage : WritableTransactionStorage, SingletonSeriali return txns.putIfAbsent(transaction.id, TxHolder(transaction, status = TransactionStatus.MISSING_NOTARY_SIG)) == null } + override fun removeUnnotarisedTransaction(id: SecureHash): Boolean { + return txns.remove(id) != null + } + override fun finalizeTransactionWithExtraSignatures(transaction: SignedTransaction, signatures: Collection): Boolean { val current = txns.replace(transaction.id, TxHolder(transaction, status = TransactionStatus.VERIFIED)) return if (current != null) { diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TestDSL.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TestDSL.kt index 983ca29b8f..0a63b813b1 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TestDSL.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TestDSL.kt @@ -141,6 +141,8 @@ data class TestTransactionDSLInterpreter private constructor( override fun recordUnnotarisedTransaction(txn: SignedTransaction, metadata: FlowTransactionMetadata?) {} + override fun removeUnnotarisedTransaction(id: SecureHash) {} + override fun finalizeTransactionWithExtraSignatures(txn: SignedTransaction, sigs: Collection, statesToRecord: StatesToRecord) {} } From fffc3e4c5d126f632b0c40e034105a9d80c3f944 Mon Sep 17 00:00:00 2001 From: Jose Coll Date: Thu, 20 Apr 2023 09:16:55 +0100 Subject: [PATCH 015/126] ENT-9822 Performance optimisation: use getNodeByLegalIdentity() backed by cache. (#7336) --- core/src/main/kotlin/net/corda/core/flows/FinalityFlow.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/flows/FinalityFlow.kt b/core/src/main/kotlin/net/corda/core/flows/FinalityFlow.kt index f5a58f7d58..d264a503b0 100644 --- a/core/src/main/kotlin/net/corda/core/flows/FinalityFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/FinalityFlow.kt @@ -204,7 +204,7 @@ class FinalityFlow private constructor(val transaction: SignedTransaction, // - finalise locally val (oldPlatformSessions, newPlatformSessions) = sessions.partition { - serviceHub.networkMapCache.getNodeByLegalName(it.counterparty.name)?.platformVersion!! < PlatformVersionSwitches.TWO_PHASE_FINALITY + serviceHub.networkMapCache.getNodeByLegalIdentity(it.counterparty)?.platformVersion!! < PlatformVersionSwitches.TWO_PHASE_FINALITY } val useTwoPhaseFinality = serviceHub.myInfo.platformVersion >= PlatformVersionSwitches.TWO_PHASE_FINALITY @@ -466,7 +466,7 @@ class ReceiveFinalityFlow @JvmOverloads constructor(private val otherSideSession override fun call(): SignedTransaction { val stx = subFlow(ReceiveTransactionFlow(otherSideSession, checkSufficientSignatures = false, statesToRecord = statesToRecord, deferredAck = true)) - val fromTwoPhaseFinalityNode = serviceHub.networkMapCache.getNodeByLegalName(otherSideSession.counterparty.name)?.platformVersion!! >= PlatformVersionSwitches.TWO_PHASE_FINALITY + val fromTwoPhaseFinalityNode = serviceHub.networkMapCache.getNodeByLegalIdentity(otherSideSession.counterparty)?.platformVersion!! >= PlatformVersionSwitches.TWO_PHASE_FINALITY if (fromTwoPhaseFinalityNode && needsNotarySignature(stx)) { serviceHub.telemetryServiceInternal.span("${this::class.java.name}#recordUnnotarisedTransaction", flowLogic = this) { logger.debug { "Peer recording transaction without notary signature." } From 0bd4364653f4e5625e1c12c15381fd69af67f7c6 Mon Sep 17 00:00:00 2001 From: Jose Coll Date: Thu, 20 Apr 2023 15:34:46 +0100 Subject: [PATCH 016/126] ENT-9823 Rename handleDoubleSpend -> propagateDoubleSpendErrorToPeers (#7338) --- .../coretests/flows/FinalityFlowTests.kt | 65 +++++++++++++++++-- .../net/corda/core/flows/FinalityFlow.kt | 30 +++++---- 2 files changed, 78 insertions(+), 17 deletions(-) diff --git a/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt b/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt index cf9663729c..4b26c624fa 100644 --- a/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt +++ b/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt @@ -22,6 +22,7 @@ import net.corda.core.flows.NotaryException import net.corda.core.flows.NotarySigCheck import net.corda.core.flows.ReceiveFinalityFlow import net.corda.core.flows.ReceiveTransactionFlow +import net.corda.core.flows.SendTransactionFlow import net.corda.core.flows.StartableByRPC import net.corda.core.flows.TransactionStatus import net.corda.core.flows.UnexpectedFlowEndException @@ -184,9 +185,10 @@ class FinalityFlowTests : WithFinality { catch (e: NotaryException) { val stxId = (e.error as NotaryError.Conflict).txId assertNull(aliceNode.services.validatedTransactions.getTransactionInternal(stxId)) - // Note: double spend error not propagated to peers by default - val (_, txnDsStatusBob) = bobNode.services.validatedTransactions.getTransactionInternal(stxId) ?: fail() - assertEquals(TransactionStatus.MISSING_NOTARY_SIG, txnDsStatusBob) + // Note: double spend error not propagated to peers by default (corDapp PV = 3) + // Un-notarised txn clean-up occurs in ReceiveFinalityFlow upon receipt of UnexpectedFlowEndException + assertNull(aliceNode.services.validatedTransactions.getTransactionInternal(stxId)) + assertTxnRemovedFromDatabase(aliceNode, stxId) } } @@ -203,9 +205,22 @@ class FinalityFlowTests : WithFinality { assertEquals(TransactionStatus.VERIFIED, txnStatusBob) try { - aliceNode.startFlowAndRunNetwork(SpendFlow(ref, bobNode.info.singleIdentity(), handleDoubleSpend = true)).resultFuture.getOrThrow() + aliceNode.startFlowAndRunNetwork(SpendFlow(ref, bobNode.info.singleIdentity(), propagateDoubleSpendErrorToPeers = true)).resultFuture.getOrThrow() } catch (e: NotaryException) { + // note: ReceiveFinalityFlow un-notarised transaction clean-up takes place upon catching NotaryError.Conflict + val stxId = (e.error as NotaryError.Conflict).txId + assertNull(aliceNode.services.validatedTransactions.getTransactionInternal(stxId)) + assertTxnRemovedFromDatabase(aliceNode, stxId) + assertNull(bobNode.services.validatedTransactions.getTransactionInternal(stxId)) + assertTxnRemovedFromDatabase(bobNode, stxId) + } + + try { + aliceNode.startFlowAndRunNetwork(SpendFlow(ref, bobNode.info.singleIdentity(), propagateDoubleSpendErrorToPeers = false)).resultFuture.getOrThrow() + } + catch (e: NotaryException) { + // note: ReceiveFinalityFlow un-notarised transaction clean-up takes place upon catching UnexpectedFlowEndException val stxId = (e.error as NotaryError.Conflict).txId assertNull(aliceNode.services.validatedTransactions.getTransactionInternal(stxId)) assertTxnRemovedFromDatabase(aliceNode, stxId) @@ -304,6 +319,23 @@ class FinalityFlowTests : WithFinality { assertEquals(TransactionStatus.VERIFIED, txnStatusBobYetAgain) } + @Test(timeout=300_000) + fun `two phase finality flow successfully removes un-notarised transaction where initiator fails to send notary signature`() { + val bobNode = createBob(platformVersion = PlatformVersionSwitches.TWO_PHASE_FINALITY) + + val ref = aliceNode.startFlowAndRunNetwork(IssueFlow(notary)).resultFuture.getOrThrow() + try { + aliceNode.startFlowAndRunNetwork(MimicFinalityFailureFlow(ref, bobNode.info.singleIdentity())).resultFuture.getOrThrow() + } + catch (e: UnexpectedFlowEndException) { + val stxId = SecureHash.parse(e.message) + assertNull(aliceNode.services.validatedTransactions.getTransactionInternal(stxId)) + assertTxnRemovedFromDatabase(aliceNode, stxId) + assertNull(bobNode.services.validatedTransactions.getTransactionInternal(stxId)) + assertTxnRemovedFromDatabase(bobNode, stxId) + } + } + @StartableByRPC class IssueFlow(val notary: Party) : FlowLogic>() { @@ -320,7 +352,7 @@ class FinalityFlowTests : WithFinality { @StartableByRPC @InitiatingFlow class SpendFlow(private val stateAndRef: StateAndRef, private val newOwner: Party, - private val handleDoubleSpend: Boolean? = null) : FlowLogic() { + private val propagateDoubleSpendErrorToPeers: Boolean? = null) : FlowLogic() { @Suspendable override fun call(): SignedTransaction { @@ -328,7 +360,7 @@ class FinalityFlowTests : WithFinality { val signedTransaction = serviceHub.signInitialTransaction(txBuilder, ourIdentity.owningKey) val sessionWithCounterParty = initiateFlow(newOwner) sessionWithCounterParty.sendAndReceive("initial-message") - return subFlow(FinalityFlow(signedTransaction, setOf(sessionWithCounterParty), handleDoubleSpend = handleDoubleSpend)) + return subFlow(FinalityFlow(signedTransaction, setOf(sessionWithCounterParty), propagateDoubleSpendErrorToPeers = propagateDoubleSpendErrorToPeers)) } } @@ -418,6 +450,27 @@ class FinalityFlowTests : WithFinality { } } + @InitiatingFlow + class MimicFinalityFailureFlow(private val stateAndRef: StateAndRef, private val newOwner: Party) : FlowLogic() { + // Mimic FinalityFlow but trigger UnexpectedFlowEndException in ReceiveFinality whilst awaiting receipt of notary signature + @Suspendable + override fun call(): SignedTransaction { + val txBuilder = DummyContract.move(stateAndRef, newOwner) + val stxn = serviceHub.signInitialTransaction(txBuilder, ourIdentity.owningKey) + val sessionWithCounterParty = initiateFlow(newOwner) + subFlow(SendTransactionFlow(sessionWithCounterParty, stxn)) + throw UnexpectedFlowEndException("${stxn.id}") + } + } + + @InitiatedBy(MimicFinalityFailureFlow::class) + class TriggerReceiveFinalityFlow(private val otherSide: FlowSession) : FlowLogic() { + @Suspendable + override fun call() { + subFlow(ReceiveFinalityFlow(otherSide)) + } + } + class FinalisationFailedException(val notarisedTxn: SignedTransaction) : FlowException("Failed to finalise transaction with notary signature.") private fun createBob(cordapps: List = emptyList(), platformVersion: Int = PLATFORM_VERSION): TestStartedNode { diff --git a/core/src/main/kotlin/net/corda/core/flows/FinalityFlow.kt b/core/src/main/kotlin/net/corda/core/flows/FinalityFlow.kt index d264a503b0..2c738549bd 100644 --- a/core/src/main/kotlin/net/corda/core/flows/FinalityFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/FinalityFlow.kt @@ -56,13 +56,13 @@ class FinalityFlow private constructor(val transaction: SignedTransaction, private val sessions: Collection, private val newApi: Boolean, private val statesToRecord: StatesToRecord = ONLY_RELEVANT, - private val handleDoubleSpend: Boolean? = null) : FlowLogic() { + private val propagateDoubleSpendErrorToPeers: Boolean? = null) : FlowLogic() { @CordaInternal - data class ExtraConstructorArgs(val oldParticipants: Collection, val sessions: Collection, val newApi: Boolean, val statesToRecord: StatesToRecord) + data class ExtraConstructorArgs(val oldParticipants: Collection, val sessions: Collection, val newApi: Boolean, val statesToRecord: StatesToRecord, val propagateDoubleSpendErrorToPeers: Boolean?) @CordaInternal - fun getExtraConstructorArgs() = ExtraConstructorArgs(oldParticipants, sessions, newApi, statesToRecord) + fun getExtraConstructorArgs() = ExtraConstructorArgs(oldParticipants, sessions, newApi, statesToRecord, propagateDoubleSpendErrorToPeers) @Deprecated(DEPRECATION_MSG) constructor(transaction: SignedTransaction, extraRecipients: Set, progressTracker: ProgressTracker) : this( @@ -91,15 +91,15 @@ class FinalityFlow private constructor(val transaction: SignedTransaction, * @param transaction What to commit. * @param sessions A collection of [FlowSession]s for each non-local participant of the transaction. Sessions to non-participants can * also be provided. - * @param handleDoubleSpend Whether to catch and propagate Double Spend exception to peers. + * @param propagateDoubleSpendErrorToPeers Whether to catch and propagate Double Spend exception to peers. */ @JvmOverloads constructor( transaction: SignedTransaction, sessions: Collection, progressTracker: ProgressTracker = tracker(), - handleDoubleSpend: Boolean? = null - ) : this(transaction, emptyList(), progressTracker, sessions, true, handleDoubleSpend = handleDoubleSpend) + propagateDoubleSpendErrorToPeers: Boolean? = null + ) : this(transaction, emptyList(), progressTracker, sessions, true, propagateDoubleSpendErrorToPeers = propagateDoubleSpendErrorToPeers) /** * Notarise the given transaction and broadcast it to all the participants. @@ -108,7 +108,7 @@ class FinalityFlow private constructor(val transaction: SignedTransaction, * @param sessions A collection of [FlowSession]s for each non-local participant of the transaction. Sessions to non-participants can * also be provided. * @param statesToRecord Which states to commit to the vault. - * @param handleDoubleSpend Whether to catch and propagate Double Spend exception to peers. + * @param propagateDoubleSpendErrorToPeers Whether to catch and propagate Double Spend exception to peers. */ @JvmOverloads constructor( @@ -116,8 +116,8 @@ class FinalityFlow private constructor(val transaction: SignedTransaction, sessions: Collection, statesToRecord: StatesToRecord, progressTracker: ProgressTracker = tracker(), - handleDoubleSpend: Boolean? = null - ) : this(transaction, emptyList(), progressTracker, sessions, true, statesToRecord, handleDoubleSpend = handleDoubleSpend) + propagateDoubleSpendErrorToPeers: Boolean? = null + ) : this(transaction, emptyList(), progressTracker, sessions, true, statesToRecord, propagateDoubleSpendErrorToPeers = propagateDoubleSpendErrorToPeers) /** * Notarise the given transaction and broadcast it to all the participants. @@ -237,9 +237,9 @@ class FinalityFlow private constructor(val transaction: SignedTransaction, catch (e: NotaryException) { if (e.error is NotaryError.Conflict && useTwoPhaseFinality) { (serviceHub as ServiceHubCoreInternal).removeUnnotarisedTransaction(e.error.txId) - val overrideHandleDoubleSpend = handleDoubleSpend ?: + val overridePropagateDoubleSpendErrorToPeers = propagateDoubleSpendErrorToPeers ?: (serviceHub.cordappProvider.getAppContext().cordapp.targetPlatformVersion >= PlatformVersionSwitches.TWO_PHASE_FINALITY) - if (overrideHandleDoubleSpend && newPlatformSessions.isNotEmpty()) { + if (overridePropagateDoubleSpendErrorToPeers && newPlatformSessions.isNotEmpty()) { broadcastDoubleSpendError(newPlatformSessions, e) } else sleep(Duration.ZERO) // force checkpoint to persist db update. } @@ -490,6 +490,14 @@ class ReceiveFinalityFlow @JvmOverloads constructor(private val otherSideSession sleep(Duration.ZERO) // force checkpoint to persist db update. } throw throwable + } catch (e: UnexpectedFlowEndException) { + (serviceHub as ServiceHubCoreInternal).removeUnnotarisedTransaction(stx.id) + sleep(Duration.ZERO) // force checkpoint to persist db update. + throw UnexpectedFlowEndException( + "${otherSideSession.counterparty} has finished prematurely whilst awaiting transaction notary signature.", + e.cause, + e.originalErrorId + ) } } else { serviceHub.telemetryServiceInternal.span("${this::class.java.name}#recordTransactions", flowLogic = this) { From 190acdc87cceb63eea7622646a634a0618694ef2 Mon Sep 17 00:00:00 2001 From: Jose Coll Date: Thu, 20 Apr 2023 15:45:10 +0100 Subject: [PATCH 017/126] NOTICK Fix failing StateMachineFinalityErrorHandlingTest's. (#7339) --- .../statemachine/StateMachineFinalityErrorHandlingTest.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StateMachineFinalityErrorHandlingTest.kt b/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StateMachineFinalityErrorHandlingTest.kt index d1db7445fb..86b1781442 100644 --- a/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StateMachineFinalityErrorHandlingTest.kt +++ b/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StateMachineFinalityErrorHandlingTest.kt @@ -57,7 +57,7 @@ class StateMachineFinalityErrorHandlingTest : StateMachineErrorHandlingTest() { RULE Throw exception when recording transaction INTERFACE ${ServiceHubInternal::class.java.name} - METHOD recordTransactions + METHOD finalizeTransactionWithExtraSignatures AT ENTRY IF flagged("finality_flag") && flagged("resolve_tx_flag") DO traceln("Throwing exception"); @@ -118,7 +118,7 @@ class StateMachineFinalityErrorHandlingTest : StateMachineErrorHandlingTest() { RULE Throw exception when recording transaction INTERFACE ${ServiceHubInternal::class.java.name} - METHOD recordTransactions + METHOD finalizeTransactionWithExtraSignatures AT ENTRY IF flagged("finality_flag") && flagged("resolve_tx_flag") DO traceln("Throwing exception"); From 3cbf693307bc5fd28116628f69fb6c4d1cf187b0 Mon Sep 17 00:00:00 2001 From: "rick.parker" Date: Mon, 24 Apr 2023 15:26:21 +0100 Subject: [PATCH 018/126] DeserializationInput creates a logger each time. --- .../serialization/internal/amqp/DeserializationInput.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/DeserializationInput.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/DeserializationInput.kt index 7be1425d32..2591c094e8 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/DeserializationInput.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/DeserializationInput.kt @@ -2,8 +2,8 @@ package net.corda.serialization.internal.amqp import net.corda.core.KeepForDJVM import net.corda.core.internal.VisibleForTesting -import net.corda.core.serialization.EncodingWhitelist import net.corda.core.serialization.AMQP_ENVELOPE_CACHE_PROPERTY +import net.corda.core.serialization.EncodingWhitelist import net.corda.core.serialization.SerializationContext import net.corda.core.serialization.SerializedBytes import net.corda.core.utilities.ByteSequence @@ -21,7 +21,6 @@ import org.apache.qpid.proton.amqp.UnsignedInteger import org.apache.qpid.proton.codec.Data import java.io.InputStream import java.io.NotSerializableException -import java.lang.Exception import java.lang.reflect.ParameterizedType import java.lang.reflect.Type import java.lang.reflect.TypeVariable @@ -41,16 +40,17 @@ class DeserializationInput constructor( private val serializerFactory: SerializerFactory ) { private val objectHistory: MutableList = mutableListOf() - private val logger = loggerFor() companion object { + private val logger = loggerFor() + @VisibleForTesting @Throws(AMQPNoTypeNotSerializableException::class) fun withDataBytes( byteSequence: ByteSequence, encodingWhitelist: EncodingWhitelist, task: (ByteBuffer) -> T - ) : T { + ): T { // Check that the lead bytes match expected header val amqpSequence = amqpMagic.consume(byteSequence) ?: throw AMQPNoTypeNotSerializableException("Serialization header does not match.") From 5f685be474c6fe5574b8f5e018c7d4c1ee44bcb0 Mon Sep 17 00:00:00 2001 From: Ronan Browne Date: Mon, 24 Apr 2023 17:56:02 +0100 Subject: [PATCH 019/126] ES-93: use standard AMI agent for build (#7345) --- .ci/dev/regression/Jenkinsfile | 2 +- .github/workflows/check-pr-title.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.ci/dev/regression/Jenkinsfile b/.ci/dev/regression/Jenkinsfile index bfd5703628..02dc1a403d 100644 --- a/.ci/dev/regression/Jenkinsfile +++ b/.ci/dev/regression/Jenkinsfile @@ -31,7 +31,7 @@ String COMMON_GRADLE_PARAMS = [ ].join(' ') pipeline { - agent { label 'standard-latest-ami' } + agent { label 'standard' } /* * List options in alphabetical order diff --git a/.github/workflows/check-pr-title.yml b/.github/workflows/check-pr-title.yml index 544a41c54c..99f8265078 100644 --- a/.github/workflows/check-pr-title.yml +++ b/.github/workflows/check-pr-title.yml @@ -9,6 +9,6 @@ jobs: steps: - uses: morrisoncole/pr-lint-action@v1.6.1 with: - title-regex: '^((CORDA|AG|EG|ENT|INFRA|NAAS)-\d+|NOTICK)(.*)' + title-regex: '^((CORDA|AG|EG|ENT|INFRA|NAAS|ES)-\d+)(.*)' on-failed-regex-comment: "PR title failed to match regex -> `%regex%`" repo-token: "${{ secrets.GITHUB_TOKEN }}" From 1d4feedc6247ad631641965e447e3d92ce05b061 Mon Sep 17 00:00:00 2001 From: Jose Coll Date: Wed, 26 Apr 2023 09:06:32 +0100 Subject: [PATCH 020/126] ENT-9147 Propagate and handle Notary Error (Part 2) (#7346) --- .ci/api-current.txt | 3 +- .../coretests/flows/FinalityFlowTests.kt | 30 ++++---- .../net/corda/core/flows/FinalityFlow.kt | 68 ++++++++----------- .../services/statemachine/FlowHospitalTest.kt | 4 ++ 4 files changed, 48 insertions(+), 57 deletions(-) diff --git a/.ci/api-current.txt b/.ci/api-current.txt index b1923bb3d1..e00001eee5 100644 --- a/.ci/api-current.txt +++ b/.ci/api-current.txt @@ -2542,7 +2542,9 @@ public final class net.corda.core.flows.FinalityFlow extends net.corda.core.flow public (net.corda.core.transactions.SignedTransaction, java.util.Collection, java.util.Collection, net.corda.core.utilities.ProgressTracker) public (net.corda.core.transactions.SignedTransaction, java.util.Collection, net.corda.core.node.StatesToRecord) public (net.corda.core.transactions.SignedTransaction, java.util.Collection, net.corda.core.node.StatesToRecord, net.corda.core.utilities.ProgressTracker) + public (net.corda.core.transactions.SignedTransaction, java.util.Collection, net.corda.core.node.StatesToRecord, net.corda.core.utilities.ProgressTracker, int, kotlin.jvm.internal.DefaultConstructorMarker) public (net.corda.core.transactions.SignedTransaction, java.util.Collection, net.corda.core.utilities.ProgressTracker) + public (net.corda.core.transactions.SignedTransaction, java.util.Collection, net.corda.core.utilities.ProgressTracker, int, kotlin.jvm.internal.DefaultConstructorMarker) public (net.corda.core.transactions.SignedTransaction, java.util.Set) public (net.corda.core.transactions.SignedTransaction, java.util.Set, net.corda.core.utilities.ProgressTracker) public (net.corda.core.transactions.SignedTransaction, net.corda.core.flows.FlowSession, net.corda.core.flows.FlowSession...) @@ -3140,7 +3142,6 @@ public final class net.corda.core.flows.ReceiveFinalityFlow extends net.corda.co public (net.corda.core.flows.FlowSession) public (net.corda.core.flows.FlowSession, net.corda.core.crypto.SecureHash) public (net.corda.core.flows.FlowSession, net.corda.core.crypto.SecureHash, net.corda.core.node.StatesToRecord) - public (net.corda.core.flows.FlowSession, net.corda.core.crypto.SecureHash, net.corda.core.node.StatesToRecord, int, kotlin.jvm.internal.DefaultConstructorMarker) @Suspendable @NotNull public net.corda.core.transactions.SignedTransaction call() diff --git a/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt b/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt index 4b26c624fa..d9857a3350 100644 --- a/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt +++ b/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt @@ -205,10 +205,9 @@ class FinalityFlowTests : WithFinality { assertEquals(TransactionStatus.VERIFIED, txnStatusBob) try { - aliceNode.startFlowAndRunNetwork(SpendFlow(ref, bobNode.info.singleIdentity(), propagateDoubleSpendErrorToPeers = true)).resultFuture.getOrThrow() + aliceNode.startFlowAndRunNetwork(SpendFlow(ref, bobNode.info.singleIdentity(), handlePropagatedNotaryError = true)).resultFuture.getOrThrow() } catch (e: NotaryException) { - // note: ReceiveFinalityFlow un-notarised transaction clean-up takes place upon catching NotaryError.Conflict val stxId = (e.error as NotaryError.Conflict).txId assertNull(aliceNode.services.validatedTransactions.getTransactionInternal(stxId)) assertTxnRemovedFromDatabase(aliceNode, stxId) @@ -217,15 +216,14 @@ class FinalityFlowTests : WithFinality { } try { - aliceNode.startFlowAndRunNetwork(SpendFlow(ref, bobNode.info.singleIdentity(), propagateDoubleSpendErrorToPeers = false)).resultFuture.getOrThrow() + aliceNode.startFlowAndRunNetwork(SpendFlow(ref, bobNode.info.singleIdentity(), handlePropagatedNotaryError = false)).resultFuture.getOrThrow() } catch (e: NotaryException) { - // note: ReceiveFinalityFlow un-notarised transaction clean-up takes place upon catching UnexpectedFlowEndException val stxId = (e.error as NotaryError.Conflict).txId assertNull(aliceNode.services.validatedTransactions.getTransactionInternal(stxId)) assertTxnRemovedFromDatabase(aliceNode, stxId) - assertNull(bobNode.services.validatedTransactions.getTransactionInternal(stxId)) - assertTxnRemovedFromDatabase(bobNode, stxId) + val (_, txnStatus) = bobNode.services.validatedTransactions.getTransactionInternal(stxId) ?: fail() + assertEquals(TransactionStatus.MISSING_NOTARY_SIG, txnStatus) } } @@ -320,7 +318,7 @@ class FinalityFlowTests : WithFinality { } @Test(timeout=300_000) - fun `two phase finality flow successfully removes un-notarised transaction where initiator fails to send notary signature`() { + fun `two phase finality flow keeps un-notarised transaction where initiator fails to send notary signature`() { val bobNode = createBob(platformVersion = PlatformVersionSwitches.TWO_PHASE_FINALITY) val ref = aliceNode.startFlowAndRunNetwork(IssueFlow(notary)).resultFuture.getOrThrow() @@ -329,10 +327,8 @@ class FinalityFlowTests : WithFinality { } catch (e: UnexpectedFlowEndException) { val stxId = SecureHash.parse(e.message) - assertNull(aliceNode.services.validatedTransactions.getTransactionInternal(stxId)) - assertTxnRemovedFromDatabase(aliceNode, stxId) - assertNull(bobNode.services.validatedTransactions.getTransactionInternal(stxId)) - assertTxnRemovedFromDatabase(bobNode, stxId) + val (_, txnStatusBob) = bobNode.services.validatedTransactions.getTransactionInternal(stxId) ?: fail() + assertEquals(TransactionStatus.MISSING_NOTARY_SIG, txnStatusBob) } } @@ -352,15 +348,15 @@ class FinalityFlowTests : WithFinality { @StartableByRPC @InitiatingFlow class SpendFlow(private val stateAndRef: StateAndRef, private val newOwner: Party, - private val propagateDoubleSpendErrorToPeers: Boolean? = null) : FlowLogic() { + private val handlePropagatedNotaryError: Boolean = false) : FlowLogic() { @Suspendable override fun call(): SignedTransaction { val txBuilder = DummyContract.move(stateAndRef, newOwner) val signedTransaction = serviceHub.signInitialTransaction(txBuilder, ourIdentity.owningKey) val sessionWithCounterParty = initiateFlow(newOwner) - sessionWithCounterParty.sendAndReceive("initial-message") - return subFlow(FinalityFlow(signedTransaction, setOf(sessionWithCounterParty), propagateDoubleSpendErrorToPeers = propagateDoubleSpendErrorToPeers)) + sessionWithCounterParty.send(handlePropagatedNotaryError) + return subFlow(FinalityFlow(signedTransaction, setOf(sessionWithCounterParty))) } } @@ -369,10 +365,8 @@ class FinalityFlowTests : WithFinality { @Suspendable override fun call() { - otherSide.receive() - otherSide.send("initial-response") - - subFlow(ReceiveFinalityFlow(otherSide)) + val handleNotaryError = otherSide.receive().unwrap { it } + subFlow(ReceiveFinalityFlow(otherSide, handlePropagatedNotaryError = handleNotaryError)) } } diff --git a/core/src/main/kotlin/net/corda/core/flows/FinalityFlow.kt b/core/src/main/kotlin/net/corda/core/flows/FinalityFlow.kt index 2c738549bd..6cdbc2b832 100644 --- a/core/src/main/kotlin/net/corda/core/flows/FinalityFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/FinalityFlow.kt @@ -55,14 +55,13 @@ class FinalityFlow private constructor(val transaction: SignedTransaction, override val progressTracker: ProgressTracker, private val sessions: Collection, private val newApi: Boolean, - private val statesToRecord: StatesToRecord = ONLY_RELEVANT, - private val propagateDoubleSpendErrorToPeers: Boolean? = null) : FlowLogic() { + private val statesToRecord: StatesToRecord = ONLY_RELEVANT) : FlowLogic() { @CordaInternal - data class ExtraConstructorArgs(val oldParticipants: Collection, val sessions: Collection, val newApi: Boolean, val statesToRecord: StatesToRecord, val propagateDoubleSpendErrorToPeers: Boolean?) + data class ExtraConstructorArgs(val oldParticipants: Collection, val sessions: Collection, val newApi: Boolean, val statesToRecord: StatesToRecord) @CordaInternal - fun getExtraConstructorArgs() = ExtraConstructorArgs(oldParticipants, sessions, newApi, statesToRecord, propagateDoubleSpendErrorToPeers) + fun getExtraConstructorArgs() = ExtraConstructorArgs(oldParticipants, sessions, newApi, statesToRecord) @Deprecated(DEPRECATION_MSG) constructor(transaction: SignedTransaction, extraRecipients: Set, progressTracker: ProgressTracker) : this( @@ -91,15 +90,13 @@ class FinalityFlow private constructor(val transaction: SignedTransaction, * @param transaction What to commit. * @param sessions A collection of [FlowSession]s for each non-local participant of the transaction. Sessions to non-participants can * also be provided. - * @param propagateDoubleSpendErrorToPeers Whether to catch and propagate Double Spend exception to peers. */ @JvmOverloads constructor( transaction: SignedTransaction, sessions: Collection, - progressTracker: ProgressTracker = tracker(), - propagateDoubleSpendErrorToPeers: Boolean? = null - ) : this(transaction, emptyList(), progressTracker, sessions, true, propagateDoubleSpendErrorToPeers = propagateDoubleSpendErrorToPeers) + progressTracker: ProgressTracker = tracker() + ) : this(transaction, emptyList(), progressTracker, sessions, true) /** * Notarise the given transaction and broadcast it to all the participants. @@ -108,16 +105,14 @@ class FinalityFlow private constructor(val transaction: SignedTransaction, * @param sessions A collection of [FlowSession]s for each non-local participant of the transaction. Sessions to non-participants can * also be provided. * @param statesToRecord Which states to commit to the vault. - * @param propagateDoubleSpendErrorToPeers Whether to catch and propagate Double Spend exception to peers. */ @JvmOverloads constructor( transaction: SignedTransaction, sessions: Collection, statesToRecord: StatesToRecord, - progressTracker: ProgressTracker = tracker(), - propagateDoubleSpendErrorToPeers: Boolean? = null - ) : this(transaction, emptyList(), progressTracker, sessions, true, statesToRecord, propagateDoubleSpendErrorToPeers = propagateDoubleSpendErrorToPeers) + progressTracker: ProgressTracker = tracker() + ) : this(transaction, emptyList(), progressTracker, sessions, true, statesToRecord) /** * Notarise the given transaction and broadcast it to all the participants. @@ -154,13 +149,13 @@ class FinalityFlow private constructor(val transaction: SignedTransaction, @Suppress("ClassNaming") object BROADCASTING_POST_NOTARISATION : ProgressTracker.Step("Broadcasting notary signature") @Suppress("ClassNaming") - object BROADCASTING_DOUBLE_SPEND_ERROR : ProgressTracker.Step("Broadcasting notary double spend error") + object BROADCASTING_NOTARY_ERROR : ProgressTracker.Step("Broadcasting notary error") @Suppress("ClassNaming") object FINALISING_TRANSACTION : ProgressTracker.Step("Finalising transaction locally") object BROADCASTING : ProgressTracker.Step("Broadcasting notarised transaction to other participants") @JvmStatic - fun tracker() = ProgressTracker(RECORD_UNNOTARISED, BROADCASTING_PRE_NOTARISATION, NOTARISING, BROADCASTING_POST_NOTARISATION, BROADCASTING_DOUBLE_SPEND_ERROR, FINALISING_TRANSACTION, BROADCASTING) + fun tracker() = ProgressTracker(RECORD_UNNOTARISED, BROADCASTING_PRE_NOTARISATION, NOTARISING, BROADCASTING_POST_NOTARISATION, BROADCASTING_NOTARY_ERROR, FINALISING_TRANSACTION, BROADCASTING) } @Suspendable @@ -235,12 +230,10 @@ class FinalityFlow private constructor(val transaction: SignedTransaction, return stxn } catch (e: NotaryException) { - if (e.error is NotaryError.Conflict && useTwoPhaseFinality) { - (serviceHub as ServiceHubCoreInternal).removeUnnotarisedTransaction(e.error.txId) - val overridePropagateDoubleSpendErrorToPeers = propagateDoubleSpendErrorToPeers ?: - (serviceHub.cordappProvider.getAppContext().cordapp.targetPlatformVersion >= PlatformVersionSwitches.TWO_PHASE_FINALITY) - if (overridePropagateDoubleSpendErrorToPeers && newPlatformSessions.isNotEmpty()) { - broadcastDoubleSpendError(newPlatformSessions, e) + if (useTwoPhaseFinality) { + (serviceHub as ServiceHubCoreInternal).removeUnnotarisedTransaction(transaction.id) + if (newPlatformSessions.isNotEmpty()) { + broadcastNotaryError(newPlatformSessions, e) } else sleep(Duration.ZERO) // force checkpoint to persist db update. } throw e @@ -298,17 +291,17 @@ class FinalityFlow private constructor(val transaction: SignedTransaction, } @Suspendable - private fun broadcastDoubleSpendError(sessions: Collection, error: NotaryException) { - progressTracker.currentStep = BROADCASTING_DOUBLE_SPEND_ERROR + private fun broadcastNotaryError(sessions: Collection, error: NotaryException) { + progressTracker.currentStep = BROADCASTING_NOTARY_ERROR serviceHub.telemetryServiceInternal.span("${this::class.java.name}#broadcastDoubleSpendError", flowLogic = this) { - logger.info("Broadcasting notary double spend error.") + logger.info("Broadcasting notary error.") sessions.forEach { session -> try { - logger.debug { "Sending notary double spend error to party $session." } + logger.debug { "Sending notary error to party $session." } session.send(Try.Failure>(error)) } catch (e: UnexpectedFlowEndException) { throw UnexpectedFlowEndException( - "${session.counterparty} has finished prematurely and we're trying to send them a notary double spend error. " + + "${session.counterparty} has finished prematurely and we're trying to send them a notary error. " + "Did they forget to call ReceiveFinalityFlow? (${e.message})", e.cause, e.originalErrorId @@ -457,10 +450,12 @@ object NotarySigCheck { * @param expectedTxId Expected ID of the transaction that's about to be received. This is typically retrieved from * [SignTransactionFlow]. Setting it to null disables the expected transaction ID check. * @param statesToRecord Which states to commit to the vault. Defaults to [StatesToRecord.ONLY_RELEVANT]. + * @param handlePropagatedNotaryError Whether to catch and propagate Double Spend exception to peers. */ class ReceiveFinalityFlow @JvmOverloads constructor(private val otherSideSession: FlowSession, private val expectedTxId: SecureHash? = null, - private val statesToRecord: StatesToRecord = ONLY_RELEVANT) : FlowLogic() { + private val statesToRecord: StatesToRecord = ONLY_RELEVANT, + private val handlePropagatedNotaryError: Boolean? = null) : FlowLogic() { @Suppress("ComplexMethod") @Suspendable override fun call(): SignedTransaction { @@ -483,21 +478,18 @@ class ReceiveFinalityFlow @JvmOverloads constructor(private val otherSideSession (serviceHub as ServiceHubCoreInternal).finalizeTransactionWithExtraSignatures(stx, notarySignatures, statesToRecord) logger.info("Peer finalised transaction with notary signature.") } - } catch(throwable: NotaryException) { - if(throwable.error is NotaryError.Conflict) { - logger.info("Peer received double spend error.") + } catch (e: NotaryException) { + logger.info("Peer received notary error.") + val overrideHandlePropagatedNotaryError = handlePropagatedNotaryError ?: + (serviceHub.cordappProvider.getAppContext().cordapp.targetPlatformVersion >= PlatformVersionSwitches.TWO_PHASE_FINALITY) + if (overrideHandlePropagatedNotaryError) { (serviceHub as ServiceHubCoreInternal).removeUnnotarisedTransaction(stx.id) sleep(Duration.ZERO) // force checkpoint to persist db update. + throw e + } + else { + otherSideSession.receive() // simulate unexpected flow end } - throw throwable - } catch (e: UnexpectedFlowEndException) { - (serviceHub as ServiceHubCoreInternal).removeUnnotarisedTransaction(stx.id) - sleep(Duration.ZERO) // force checkpoint to persist db update. - throw UnexpectedFlowEndException( - "${otherSideSession.counterparty} has finished prematurely whilst awaiting transaction notary signature.", - e.cause, - e.originalErrorId - ) } } else { serviceHub.telemetryServiceInternal.span("${this::class.java.name}#recordTransactions", flowLogic = this) { diff --git a/node/src/integration-test/kotlin/net/corda/node/services/statemachine/FlowHospitalTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/statemachine/FlowHospitalTest.kt index b90f23437e..67a8f8144b 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/statemachine/FlowHospitalTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/statemachine/FlowHospitalTest.kt @@ -249,6 +249,7 @@ class FlowHospitalTest { it.startFlow(::SpendStateAndCatchDoubleSpendFlow, nodeBHandle.nodeInfo.singleIdentity(), ref).returnValue.getOrThrow(20.seconds) } waitForAllFlowsToComplete(nodeAHandle) + waitForAllFlowsToComplete(nodeBHandle) } // 1 is the notary failing to notarise and propagating the error // 2 is the receiving flow failing due to the unexpected session end error @@ -321,6 +322,8 @@ class FlowHospitalTest { it.startFlow(::SpendStateAndCatchDoubleSpendOldFinalityFlow, nodeBHandle.nodeInfo.singleIdentity(), ref).returnValue.getOrThrow(20.seconds) it.startFlow(::SpendStateAndCatchDoubleSpendOldFinalityFlow, nodeBHandle.nodeInfo.singleIdentity(), ref).returnValue.getOrThrow(20.seconds) } + waitForAllFlowsToComplete(nodeAHandle) + waitForAllFlowsToComplete(nodeBHandle) } // 1 is the notary failing to notarise and propagating the error // 2 is the receiving flow failing due to the unexpected session end error @@ -378,6 +381,7 @@ class FlowHospitalTest { it.startFlow(::SpendStateAndCatchDoubleSpendFlow, nodeBHandle.nodeInfo.singleIdentity(), ref, true).returnValue.getOrThrow(20.seconds) } waitForAllFlowsToComplete(nodeAHandle) + waitForAllFlowsToComplete(nodeBHandle) } // 1 is the notary failing to notarise and propagating the error assertEquals(1, dischargedCounter) From f4917e08e11d366b3f49bc3002432d2ceb8423b3 Mon Sep 17 00:00:00 2001 From: Rick Parker Date: Wed, 26 Apr 2023 12:34:57 +0100 Subject: [PATCH 021/126] ENT-9837 Use a cache to avoid resolving contract class name for a contract state repeatedly. (#7348) --- core-deterministic/build.gradle | 1 + .../core/internal/ContractStateClassCache.kt | 16 ++++++++++++++ .../corda/core/internal/ConstraintsUtils.kt | 22 +++++++++++-------- .../core/internal/ContractStateClassCache.kt | 18 +++++++++++++++ 4 files changed, 48 insertions(+), 9 deletions(-) create mode 100644 core-deterministic/src/main/kotlin/net/corda/core/internal/ContractStateClassCache.kt create mode 100644 core/src/main/kotlin/net/corda/core/internal/ContractStateClassCache.kt diff --git a/core-deterministic/build.gradle b/core-deterministic/build.gradle index 6ffa0df5a5..e44d17c9d9 100644 --- a/core-deterministic/build.gradle +++ b/core-deterministic/build.gradle @@ -82,6 +82,7 @@ def patchCore = tasks.register('patchCore', Zip) { exclude 'net/corda/core/serialization/internal/CheckpointSerializationFactory*.class' exclude 'net/corda/core/internal/rules/*.class' exclude 'net/corda/core/internal/utilities/PrivateInterner*.class' + exclude 'net/corda/core/internal/ContractStateClassCache*.class' } reproducibleFileOrder = true diff --git a/core-deterministic/src/main/kotlin/net/corda/core/internal/ContractStateClassCache.kt b/core-deterministic/src/main/kotlin/net/corda/core/internal/ContractStateClassCache.kt new file mode 100644 index 0000000000..f437cc0938 --- /dev/null +++ b/core-deterministic/src/main/kotlin/net/corda/core/internal/ContractStateClassCache.kt @@ -0,0 +1,16 @@ +package net.corda.core.internal + +import net.corda.core.contracts.ContractState + +@Suppress("unused") +object ContractStateClassCache { + @Suppress("UNUSED_PARAMETER") + fun contractClassName(key: Class): String? { + return null + } + + @Suppress("UNUSED_PARAMETER") + fun cacheContractClassName(key: Class, contractClassName: String?): String? { + return contractClassName + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/internal/ConstraintsUtils.kt b/core/src/main/kotlin/net/corda/core/internal/ConstraintsUtils.kt index 2cdb80fad1..f558024078 100644 --- a/core/src/main/kotlin/net/corda/core/internal/ConstraintsUtils.kt +++ b/core/src/main/kotlin/net/corda/core/internal/ConstraintsUtils.kt @@ -28,15 +28,19 @@ val Attachment.contractVersion: Version get() = if (this is ContractAttachment) * one and it inherits from [Contract]. */ val ContractState.requiredContractClassName: String? get() { - val annotation = javaClass.getAnnotation(BelongsToContract::class.java) - if (annotation != null) { - return annotation.value.java.typeName.removePrefix(DJVM_SANDBOX_PREFIX) - } - val enclosingClass = javaClass.enclosingClass ?: return null - return if (Contract::class.java.isAssignableFrom(enclosingClass)) { - enclosingClass.typeName.removePrefix(DJVM_SANDBOX_PREFIX) - } else { - null + return ContractStateClassCache.contractClassName(this.javaClass) ?: let { + val annotation = javaClass.getAnnotation(BelongsToContract::class.java) + val className = if (annotation != null) { + annotation.value.java.typeName.removePrefix(DJVM_SANDBOX_PREFIX) + } else { + val enclosingClass = javaClass.enclosingClass ?: return null + if (Contract::class.java.isAssignableFrom(enclosingClass)) { + enclosingClass.typeName.removePrefix(DJVM_SANDBOX_PREFIX) + } else { + null + } + } + ContractStateClassCache.cacheContractClassName(this.javaClass, className) } } diff --git a/core/src/main/kotlin/net/corda/core/internal/ContractStateClassCache.kt b/core/src/main/kotlin/net/corda/core/internal/ContractStateClassCache.kt new file mode 100644 index 0000000000..73fb3a847d --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/internal/ContractStateClassCache.kt @@ -0,0 +1,18 @@ +package net.corda.core.internal + +import com.google.common.collect.MapMaker +import net.corda.core.contracts.ContractState + +object ContractStateClassCache { + private val classToString = MapMaker().weakKeys().makeMap, String>() + + fun contractClassName(key: Class): String? { + return classToString[key] + } + + fun cacheContractClassName(key: Class, contractClassName: String?): String? { + if (contractClassName == null) return null + classToString.putIfAbsent(key, contractClassName) + return contractClassName + } +} \ No newline at end of file From 9ba39199800f5351f101f9413fd1fe6aebdc15fb Mon Sep 17 00:00:00 2001 From: Rick Parker Date: Wed, 26 Apr 2023 17:49:52 +0100 Subject: [PATCH 022/126] ENT-9583 Public key caching of encoded form (OS) (#7332) --- core-deterministic/build.gradle | 1 + .../core/crypto/internal/PublicKeyCache.kt | 21 ++++++++ .../net/corda/core/crypto/CompositeKey.kt | 11 +++- .../kotlin/net/corda/core/crypto/Crypto.kt | 22 +++++--- .../net/corda/core/crypto/SecureHash.kt | 13 +++-- .../core/crypto/internal/PublicKeyCache.kt | 54 +++++++++++++++++++ .../net/corda/core/internal/InternalUtils.kt | 2 +- .../corda/core/internal/X509EdDSAEngine.kt | 11 +++- .../corda/core/node/services/VaultService.kt | 2 +- .../net/corda/core/utilities/ByteArrays.kt | 18 ++++--- .../net/corda/core/utilities/EncodingUtils.kt | 4 +- .../nodeapi/internal/crypto/X509Utilities.kt | 37 ++++++++++--- .../internal/serialization/kryo/Kryo.kt | 16 ++++-- .../identity/PersistentIdentityService.kt | 6 ++- .../keys/BasicHSMKeyManagementService.kt | 21 ++++++-- .../persistence/PublicKeyToTextConverter.kt | 2 +- .../sandbox/net/corda/core/crypto/Crypto.kt | 11 ++-- .../amqp/custom/PublicKeySerializer.kt | 10 +++- 18 files changed, 215 insertions(+), 47 deletions(-) create mode 100644 core-deterministic/src/main/kotlin/net/corda/core/crypto/internal/PublicKeyCache.kt create mode 100644 core/src/main/kotlin/net/corda/core/crypto/internal/PublicKeyCache.kt diff --git a/core-deterministic/build.gradle b/core-deterministic/build.gradle index e44d17c9d9..e322d57401 100644 --- a/core-deterministic/build.gradle +++ b/core-deterministic/build.gradle @@ -82,6 +82,7 @@ def patchCore = tasks.register('patchCore', Zip) { exclude 'net/corda/core/serialization/internal/CheckpointSerializationFactory*.class' exclude 'net/corda/core/internal/rules/*.class' exclude 'net/corda/core/internal/utilities/PrivateInterner*.class' + exclude 'net/corda/core/crypto/internal/PublicKeyCache*.class' exclude 'net/corda/core/internal/ContractStateClassCache*.class' } diff --git a/core-deterministic/src/main/kotlin/net/corda/core/crypto/internal/PublicKeyCache.kt b/core-deterministic/src/main/kotlin/net/corda/core/crypto/internal/PublicKeyCache.kt new file mode 100644 index 0000000000..b4b7d440d7 --- /dev/null +++ b/core-deterministic/src/main/kotlin/net/corda/core/crypto/internal/PublicKeyCache.kt @@ -0,0 +1,21 @@ +package net.corda.core.crypto.internal + +import net.corda.core.utilities.ByteSequence +import java.security.PublicKey + +@Suppress("unused") +object PublicKeyCache { + @Suppress("UNUSED_PARAMETER") + fun bytesForCachedPublicKey(key: PublicKey): ByteSequence? { + return null + } + + @Suppress("UNUSED_PARAMETER") + fun publicKeyForCachedBytes(bytes: ByteSequence): PublicKey? { + return null + } + + fun cachePublicKey(key: PublicKey): PublicKey { + return key + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/crypto/CompositeKey.kt b/core/src/main/kotlin/net/corda/core/crypto/CompositeKey.kt index be744a6b80..eb79aa9e57 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/CompositeKey.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/CompositeKey.kt @@ -4,7 +4,14 @@ import net.corda.core.KeepForDJVM import net.corda.core.serialization.CordaSerializable import net.corda.core.utilities.exactAdd import net.corda.core.utilities.sequence -import org.bouncycastle.asn1.* +import org.bouncycastle.asn1.ASN1EncodableVector +import org.bouncycastle.asn1.ASN1Encoding +import org.bouncycastle.asn1.ASN1Integer +import org.bouncycastle.asn1.ASN1Object +import org.bouncycastle.asn1.ASN1Primitive +import org.bouncycastle.asn1.ASN1Sequence +import org.bouncycastle.asn1.DERBitString +import org.bouncycastle.asn1.DERSequence import org.bouncycastle.asn1.x509.AlgorithmIdentifier import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo import java.security.PublicKey @@ -162,7 +169,7 @@ class CompositeKey private constructor(val threshold: Int, children: List() - private fun internPublicKey(key: PublicKey): PublicKey = interner.intern(key) + private fun internPublicKey(key: PublicKey): PublicKey = PublicKeyCache.cachePublicKey(interner.intern(key)) + private fun convertIfBCEdDSAPublicKey(key: PublicKey): PublicKey { return internPublicKey(when (key) { diff --git a/core/src/main/kotlin/net/corda/core/crypto/SecureHash.kt b/core/src/main/kotlin/net/corda/core/crypto/SecureHash.kt index 0e9dbb6d20..5c7e943c8c 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/SecureHash.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/SecureHash.kt @@ -13,7 +13,6 @@ import net.corda.core.serialization.CordaSerializable import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.parseAsHex import net.corda.core.utilities.toHexString -import java.nio.ByteBuffer import java.security.MessageDigest import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentMap @@ -39,9 +38,10 @@ sealed class SecureHash(bytes: ByteArray) : OpaqueBytes(bytes) { return true } - // This is an efficient hashCode, because there is no point in performing a hash calculation on a cryptographic hash. - // It just takes the first 4 bytes and transforms them into an Int. - override fun hashCode() = ByteBuffer.wrap(bytes).int + override fun hashCode(): Int { + // Hash code not overridden on purpose (super class impl will do), but don't delete or have to deal with detekt and API checker. + return super.hashCode() + } /** * Convert the hash value to an uppercase hexadecimal [String]. @@ -62,7 +62,10 @@ sealed class SecureHash(bytes: ByteArray) : OpaqueBytes(bytes) { } } - override fun hashCode() = ByteBuffer.wrap(bytes).int + override fun hashCode(): Int { + // Hash code not overridden on purpose (super class impl will do), but don't delete or have to deal with detekt and API checker. + return super.hashCode() + } override fun toString(): String { return "$algorithm$DELIMITER${toHexString()}" diff --git a/core/src/main/kotlin/net/corda/core/crypto/internal/PublicKeyCache.kt b/core/src/main/kotlin/net/corda/core/crypto/internal/PublicKeyCache.kt new file mode 100644 index 0000000000..bbd322531d --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/crypto/internal/PublicKeyCache.kt @@ -0,0 +1,54 @@ +package net.corda.core.crypto.internal + +import net.corda.core.utilities.ByteSequence +import java.lang.ref.ReferenceQueue +import java.lang.ref.WeakReference +import java.security.PublicKey +import java.util.concurrent.ConcurrentHashMap + +object PublicKeyCache { + private val collectedWeakPubKeys = ReferenceQueue() + + private class WeakPubKey(key: PublicKey, val bytes: ByteSequence? = null) : WeakReference(key, collectedWeakPubKeys) { + private val hashCode = key.hashCode() + + override fun hashCode(): Int = hashCode + override fun equals(other: Any?): Boolean { + if(this === other) return true + if(other !is WeakPubKey) return false + if(this.hashCode != other.hashCode) return false + val thisGet = this.get() + val otherGet = other.get() + if(thisGet == null || otherGet == null) return false + return thisGet == otherGet + } + } + + private val pubKeyToBytes = ConcurrentHashMap() + private val bytesToPubKey = ConcurrentHashMap() + + private fun reapCollectedWeakPubKeys() { + while(true) { + val weakPubKey = (collectedWeakPubKeys.poll() as? WeakPubKey) ?: break + pubKeyToBytes.remove(weakPubKey) + bytesToPubKey.remove(weakPubKey.bytes!!) + } + } + + fun bytesForCachedPublicKey(key: PublicKey): ByteSequence? { + val weakPubKey = WeakPubKey(key) + return pubKeyToBytes[weakPubKey] + } + + fun publicKeyForCachedBytes(bytes: ByteSequence): PublicKey? { + return bytesToPubKey[bytes]?.get() + } + + fun cachePublicKey(key: PublicKey): PublicKey { + reapCollectedWeakPubKeys() + val weakPubKey = WeakPubKey(key, ByteSequence.of(key.encoded)) + pubKeyToBytes.putIfAbsent(weakPubKey, weakPubKey.bytes!!) + bytesToPubKey.putIfAbsent(weakPubKey.bytes, weakPubKey) + return key + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt b/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt index fb22ca085b..274582ce53 100644 --- a/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt +++ b/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt @@ -555,7 +555,7 @@ fun SerializedBytes.sign(keyPair: KeyPair): SignedData = SignedD fun ByteBuffer.copyBytes(): ByteArray = ByteArray(remaining()).also { get(it) } -val PublicKey.hash: SecureHash get() = encoded.sha256() +val PublicKey.hash: SecureHash get() = Crypto.encodePublicKey(this).sha256() /** * Extension method for providing a sumBy method that processes and returns a Long diff --git a/core/src/main/kotlin/net/corda/core/internal/X509EdDSAEngine.kt b/core/src/main/kotlin/net/corda/core/internal/X509EdDSAEngine.kt index 5c0d6ebe80..1900f23eb1 100644 --- a/core/src/main/kotlin/net/corda/core/internal/X509EdDSAEngine.kt +++ b/core/src/main/kotlin/net/corda/core/internal/X509EdDSAEngine.kt @@ -1,9 +1,16 @@ package net.corda.core.internal import net.corda.core.DeleteForDJVM +import net.corda.core.crypto.Crypto import net.i2p.crypto.eddsa.EdDSAEngine import net.i2p.crypto.eddsa.EdDSAPublicKey -import java.security.* +import java.security.AlgorithmParameters +import java.security.InvalidKeyException +import java.security.MessageDigest +import java.security.PrivateKey +import java.security.PublicKey +import java.security.SecureRandom +import java.security.Signature import java.security.spec.AlgorithmParameterSpec import java.security.spec.X509EncodedKeySpec @@ -30,7 +37,7 @@ class X509EdDSAEngine : Signature { override fun engineInitVerify(publicKey: PublicKey) { val parsedKey = try { - publicKey as? EdDSAPublicKey ?: EdDSAPublicKey(X509EncodedKeySpec(publicKey.encoded)) + publicKey as? EdDSAPublicKey ?: EdDSAPublicKey(X509EncodedKeySpec(Crypto.encodePublicKey(publicKey))) } catch (e: Exception) { throw (InvalidKeyException(e.message)) } diff --git a/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt b/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt index 36a3598bb7..5993d60587 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt @@ -166,7 +166,7 @@ class Vault(val states: Iterable>) { fun data(): ByteArray? { return when (type()) { Type.HASH -> (constraint as HashAttachmentConstraint).attachmentId.bytes - Type.SIGNATURE -> (constraint as SignatureAttachmentConstraint).key.encoded + Type.SIGNATURE -> Crypto.encodePublicKey((constraint as SignatureAttachmentConstraint).key) else -> null } } diff --git a/core/src/main/kotlin/net/corda/core/utilities/ByteArrays.kt b/core/src/main/kotlin/net/corda/core/utilities/ByteArrays.kt index 4d4083b0ec..01050545e7 100644 --- a/core/src/main/kotlin/net/corda/core/utilities/ByteArrays.kt +++ b/core/src/main/kotlin/net/corda/core/utilities/ByteArrays.kt @@ -112,6 +112,7 @@ sealed class ByteSequence(private val _bytes: ByteArray, val offset: Int, val si if (this === other) return true if (other !is ByteSequence) return false if (this.size != other.size) return false + if (this.hashCode() != other.hashCode()) return false return subArraysEqual(this._bytes, this.offset, this.size, other._bytes, other.offset) } @@ -125,13 +126,18 @@ sealed class ByteSequence(private val _bytes: ByteArray, val offset: Int, val si return true } + private var _hashCode: Int = 0; + override fun hashCode(): Int { - val thisBytes = _bytes - var result = 1 - for (index in offset until (offset + size)) { - result = 31 * result + thisBytes[index] - } - return result + return if (_hashCode == 0) { + val thisBytes = _bytes + var result = 1 + for (index in offset until (offset + size)) { + result = 31 * result + thisBytes[index] + } + _hashCode = result + result + } else _hashCode } override fun toString(): String = "[${copyBytes().toHexString()}]" diff --git a/core/src/main/kotlin/net/corda/core/utilities/EncodingUtils.kt b/core/src/main/kotlin/net/corda/core/utilities/EncodingUtils.kt index dc47a1c22f..2d0014827e 100644 --- a/core/src/main/kotlin/net/corda/core/utilities/EncodingUtils.kt +++ b/core/src/main/kotlin/net/corda/core/utilities/EncodingUtils.kt @@ -9,7 +9,7 @@ import net.corda.core.crypto.Crypto import net.corda.core.internal.hash import java.nio.charset.Charset import java.security.PublicKey -import java.util.* +import java.util.Base64 // This file includes useful encoding methods and extension functions for the most common encoding/decoding operations. @@ -81,7 +81,7 @@ fun String.hexToBase64(): String = hexToByteArray().toBase64() fun parsePublicKeyBase58(base58String: String): PublicKey = Crypto.decodePublicKey(base58String.base58ToByteArray()) /** Return the Base58 representation of the serialised public key. */ -fun PublicKey.toBase58String(): String = this.encoded.toBase58() +fun PublicKey.toBase58String(): String = Crypto.encodePublicKey(this).toBase58() /** Return the bytes of the SHA-256 output for this public key. */ fun PublicKey.toSHA256Bytes(): ByteArray = this.hash.bytes diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/X509Utilities.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/X509Utilities.kt index ff115ec78c..60d9136f30 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/X509Utilities.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/X509Utilities.kt @@ -3,14 +3,33 @@ package net.corda.nodeapi.internal.crypto import net.corda.core.CordaOID import net.corda.core.crypto.Crypto import net.corda.core.crypto.newSecureRandom -import net.corda.core.internal.* +import net.corda.core.internal.CertRole +import net.corda.core.internal.SignedDataWithCert +import net.corda.core.internal.reader +import net.corda.core.internal.signWithCert +import net.corda.core.internal.uncheckedCast +import net.corda.core.internal.validate +import net.corda.core.internal.writer import net.corda.core.utilities.days import net.corda.core.utilities.millis -import org.bouncycastle.asn1.* +import org.bouncycastle.asn1.ASN1EncodableVector +import org.bouncycastle.asn1.ASN1ObjectIdentifier +import org.bouncycastle.asn1.ASN1Sequence +import org.bouncycastle.asn1.DERSequence +import org.bouncycastle.asn1.DERUTF8String import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.asn1.x500.style.BCStyle -import org.bouncycastle.asn1.x509.* +import org.bouncycastle.asn1.x509.BasicConstraints +import org.bouncycastle.asn1.x509.CRLDistPoint +import org.bouncycastle.asn1.x509.DistributionPoint +import org.bouncycastle.asn1.x509.DistributionPointName import org.bouncycastle.asn1.x509.Extension +import org.bouncycastle.asn1.x509.GeneralName +import org.bouncycastle.asn1.x509.GeneralNames +import org.bouncycastle.asn1.x509.KeyPurposeId +import org.bouncycastle.asn1.x509.KeyUsage +import org.bouncycastle.asn1.x509.NameConstraints +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo import org.bouncycastle.cert.X509CertificateHolder import org.bouncycastle.cert.X509v3CertificateBuilder import org.bouncycastle.cert.bc.BcX509ExtensionUtils @@ -28,12 +47,18 @@ import java.nio.file.Path import java.security.KeyPair import java.security.PublicKey import java.security.SignatureException -import java.security.cert.* +import java.security.cert.CertPath import java.security.cert.Certificate +import java.security.cert.CertificateException +import java.security.cert.CertificateFactory +import java.security.cert.TrustAnchor +import java.security.cert.X509CRL +import java.security.cert.X509Certificate import java.time.Duration import java.time.Instant import java.time.temporal.ChronoUnit -import java.util.* +import java.util.ArrayList +import java.util.Date import javax.security.auth.x500.X500Principal import kotlin.experimental.and import kotlin.experimental.or @@ -189,7 +214,7 @@ object X509Utilities { crlIssuer: X500Name? = null): X509v3CertificateBuilder { val serial = generateCertificateSerialNumber() val keyPurposes = DERSequence(ASN1EncodableVector().apply { certificateType.purposes.forEach { add(it) } }) - val subjectPublicKeyInfo = SubjectPublicKeyInfo.getInstance(ASN1Sequence.getInstance(subjectPublicKey.encoded)) + val subjectPublicKeyInfo = SubjectPublicKeyInfo.getInstance(ASN1Sequence.getInstance(Crypto.encodePublicKey(subjectPublicKey))) val role = certificateType.role val builder = JcaX509v3CertificateBuilder(issuer, serial, validityWindow.first, validityWindow.second, subject, subjectPublicKey) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/kryo/Kryo.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/kryo/Kryo.kt index b594954ef5..6d5b31d08e 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/kryo/Kryo.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/kryo/Kryo.kt @@ -1,6 +1,10 @@ package net.corda.nodeapi.internal.serialization.kryo -import com.esotericsoftware.kryo.* +import com.esotericsoftware.kryo.ClassResolver +import com.esotericsoftware.kryo.Kryo +import com.esotericsoftware.kryo.KryoException +import com.esotericsoftware.kryo.Registration +import com.esotericsoftware.kryo.Serializer import com.esotericsoftware.kryo.factories.ReflectionSerializerFactory import com.esotericsoftware.kryo.io.Input import com.esotericsoftware.kryo.io.Output @@ -17,7 +21,13 @@ import net.corda.core.internal.LazyMappedList import net.corda.core.internal.uncheckedCast import net.corda.core.serialization.SerializeAsTokenContext import net.corda.core.serialization.SerializedBytes -import net.corda.core.transactions.* +import net.corda.core.transactions.ComponentGroup +import net.corda.core.transactions.ContractUpgradeFilteredTransaction +import net.corda.core.transactions.ContractUpgradeWireTransaction +import net.corda.core.transactions.CoreTransaction +import net.corda.core.transactions.NotaryChangeWireTransaction +import net.corda.core.transactions.SignedTransaction +import net.corda.core.transactions.WireTransaction import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.SgxSupport import net.corda.serialization.internal.serializationContextKey @@ -302,7 +312,7 @@ object PrivateKeySerializer : Serializer() { object PublicKeySerializer : Serializer() { override fun write(kryo: Kryo, output: Output, obj: PublicKey) { // TODO: Instead of encoding to the default X509 format, we could have a custom per key type (space-efficient) serialiser. - output.writeBytesWithLength(obj.encoded) + output.writeBytesWithLength(Crypto.encodePublicKey(obj)) } override fun read(kryo: Kryo, input: Input, type: Class): PublicKey { diff --git a/node/src/main/kotlin/net/corda/node/services/identity/PersistentIdentityService.kt b/node/src/main/kotlin/net/corda/node/services/identity/PersistentIdentityService.kt index 2e10525141..80e6d4050c 100644 --- a/node/src/main/kotlin/net/corda/node/services/identity/PersistentIdentityService.kt +++ b/node/src/main/kotlin/net/corda/node/services/identity/PersistentIdentityService.kt @@ -47,7 +47,9 @@ import java.security.cert.CertificateNotYetValidException import java.security.cert.CollectionCertStoreParameters import java.security.cert.TrustAnchor import java.security.cert.X509Certificate -import java.util.* +import java.util.HashSet +import java.util.Optional +import java.util.UUID import java.util.concurrent.ExecutorService import java.util.concurrent.Executors import java.util.stream.Stream @@ -136,7 +138,7 @@ class PersistentIdentityService(cacheFactory: NamedCacheFactory) : SingletonSeri ) }, toPersistentEntity = { key: String, value: PublicKey -> - PersistentHashToPublicKey(key, value.encoded) + PersistentHashToPublicKey(key, Crypto.encodePublicKey(value)) }, persistentEntityClass = PersistentHashToPublicKey::class.java) } diff --git a/node/src/main/kotlin/net/corda/node/services/keys/BasicHSMKeyManagementService.kt b/node/src/main/kotlin/net/corda/node/services/keys/BasicHSMKeyManagementService.kt index 1655a517a3..cfadb10d4e 100644 --- a/node/src/main/kotlin/net/corda/node/services/keys/BasicHSMKeyManagementService.kt +++ b/node/src/main/kotlin/net/corda/node/services/keys/BasicHSMKeyManagementService.kt @@ -1,6 +1,14 @@ package net.corda.node.services.keys -import net.corda.core.crypto.* +import net.corda.core.crypto.Crypto +import net.corda.core.crypto.DigitalSignature +import net.corda.core.crypto.SignableData +import net.corda.core.crypto.SignatureScheme +import net.corda.core.crypto.TransactionSignature +import net.corda.core.crypto.generateKeyPair +import net.corda.core.crypto.keys +import net.corda.core.crypto.sign +import net.corda.core.crypto.toStringShort import net.corda.core.internal.NamedCacheFactory import net.corda.core.internal.telemetry.TelemetryServiceImpl import net.corda.core.serialization.SingletonSerializeAsToken @@ -16,9 +24,12 @@ import org.bouncycastle.operator.ContentSigner import java.security.KeyPair import java.security.PrivateKey import java.security.PublicKey -import java.util.* -import javax.persistence.* -import kotlin.collections.LinkedHashSet +import java.util.UUID +import javax.persistence.Column +import javax.persistence.Entity +import javax.persistence.Id +import javax.persistence.Lob +import javax.persistence.Table /** * A persistent implementation of [KeyManagementServiceInternal] to support CryptoService for initial keys and @@ -52,7 +63,7 @@ class BasicHSMKeyManagementService( var privateKey: ByteArray = EMPTY_BYTE_ARRAY ) { constructor(publicKey: PublicKey, privateKey: PrivateKey) - : this(publicKey.toStringShort(), publicKey.encoded, privateKey.encoded) + : this(publicKey.toStringShort(), Crypto.encodePublicKey(publicKey), privateKey.encoded) } private companion object { diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/PublicKeyToTextConverter.kt b/node/src/main/kotlin/net/corda/node/services/persistence/PublicKeyToTextConverter.kt index 4ee5a15e57..a8e8ef132b 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/PublicKeyToTextConverter.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/PublicKeyToTextConverter.kt @@ -13,6 +13,6 @@ import javax.persistence.Converter */ @Converter(autoApply = true) class PublicKeyToTextConverter : AttributeConverter { - override fun convertToDatabaseColumn(key: PublicKey?): String? = key?.encoded?.toHex() + override fun convertToDatabaseColumn(key: PublicKey?): String? = key?.let { Crypto.encodePublicKey(key).toHex() } override fun convertToEntityAttribute(text: String?): PublicKey? = text?.let { Crypto.decodePublicKey(it.hexToByteArray()) } } \ No newline at end of file diff --git a/node/src/main/kotlin/sandbox/net/corda/core/crypto/Crypto.kt b/node/src/main/kotlin/sandbox/net/corda/core/crypto/Crypto.kt index b51a586c93..1045902baa 100644 --- a/node/src/main/kotlin/sandbox/net/corda/core/crypto/Crypto.kt +++ b/node/src/main/kotlin/sandbox/net/corda/core/crypto/Crypto.kt @@ -1,7 +1,6 @@ package sandbox.net.corda.core.crypto -import sandbox.net.corda.core.crypto.DJVM.fromDJVM -import sandbox.net.corda.core.crypto.DJVM.toDJVM +import sandbox.java.lang.Object import sandbox.java.lang.String import sandbox.java.lang.doCatch import sandbox.java.math.BigInteger @@ -10,7 +9,8 @@ import sandbox.java.security.PrivateKey import sandbox.java.security.PublicKey import sandbox.java.util.ArrayList import sandbox.java.util.List -import sandbox.java.lang.Object +import sandbox.net.corda.core.crypto.DJVM.fromDJVM +import sandbox.net.corda.core.crypto.DJVM.toDJVM import sandbox.org.bouncycastle.asn1.x509.AlgorithmIdentifier import sandbox.org.bouncycastle.asn1.x509.SubjectPublicKeyInfo import java.security.GeneralSecurityException @@ -149,6 +149,11 @@ object Crypto : Object() { return decodePublicKey(signatureScheme.schemeCodeName, encodedKey) } + @JvmStatic + fun encodePublicKey(key: java.security.PublicKey): ByteArray { + return key.encoded + } + @JvmStatic fun deriveKeyPair(signatureScheme: SignatureScheme, privateKey: PrivateKey, seed: ByteArray): KeyPair { throw sandbox.java.lang.failApi("Crypto.deriveKeyPair(SignatureScheme, PrivateKey, byte[])") diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/PublicKeySerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/PublicKeySerializer.kt index ee4bceb09b..44ff43ab07 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/PublicKeySerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/PublicKeySerializer.kt @@ -3,7 +3,13 @@ package net.corda.serialization.internal.amqp.custom import net.corda.core.crypto.Crypto import net.corda.core.serialization.DESERIALIZATION_CACHE_PROPERTY import net.corda.core.serialization.SerializationContext -import net.corda.serialization.internal.amqp.* +import net.corda.serialization.internal.amqp.AMQPTypeIdentifiers +import net.corda.serialization.internal.amqp.CustomSerializer +import net.corda.serialization.internal.amqp.DeserializationInput +import net.corda.serialization.internal.amqp.RestrictedType +import net.corda.serialization.internal.amqp.Schema +import net.corda.serialization.internal.amqp.SerializationOutput +import net.corda.serialization.internal.amqp.SerializationSchemas import org.apache.qpid.proton.codec.Data import java.lang.reflect.Type import java.security.PublicKey @@ -28,7 +34,7 @@ object PublicKeySerializer context: SerializationContext ) { // TODO: Instead of encoding to the default X509 format, we could have a custom per key type (space-efficient) serialiser. - output.writeObject(obj.encoded, data, clazz, context) + output.writeObject(Crypto.encodePublicKey(obj), data, clazz, context) } override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput, From c3e39a7052d405e92ef1a846dc99809157d75dce Mon Sep 17 00:00:00 2001 From: Jose Coll Date: Thu, 27 Apr 2023 16:58:17 +0100 Subject: [PATCH 023/126] ENT-9842 Re-factor 2PF to support issuance transactions (no notarisation) with observers. (#7349) Re-factor 2PF to support issuance transactions (no notarisation) with observers. --- core-tests/build.gradle | 2 + .../coretests/flows/FinalityFlowTests.kt | 23 ++- .../net/corda/core/flows/FinalityFlow.kt | 187 +++++++++++------- .../net/corda/core/flows/FlowTransaction.kt | 2 +- .../core/internal/ServiceHubCoreInternal.kt | 16 +- node/build.gradle | 3 + .../flows/FinalityFlowErrorHandlingTest.kt | 101 ++++++++++ .../StateMachineErrorHandlingTest.kt | 7 +- .../services/statemachine/FlowHospitalTest.kt | 1 + .../node/services/api/ServiceHubInternal.kt | 30 ++- .../persistence/DBTransactionStorage.kt | 50 ++--- .../node/messaging/TwoPartyTradeFlowTests.kt | 11 +- .../persistence/DBTransactionStorageTests.kt | 55 ++++-- settings.gradle | 1 + testing/cordapps/cashobservers/build.gradle | 17 ++ .../test/flows/CashIssueWithObserversFlow.kt | 48 +++++ .../node/internal/MockTransactionStorage.kt | 7 +- .../kotlin/net/corda/testing/dsl/TestDSL.kt | 4 +- 18 files changed, 433 insertions(+), 132 deletions(-) create mode 100644 node/src/integration-test-slow/kotlin/net/corda/node/flows/FinalityFlowErrorHandlingTest.kt create mode 100644 testing/cordapps/cashobservers/build.gradle create mode 100644 testing/cordapps/cashobservers/src/main/kotlin/net/corda/finance/test/flows/CashIssueWithObserversFlow.kt diff --git a/core-tests/build.gradle b/core-tests/build.gradle index c5e184e448..ea5f7d4ecf 100644 --- a/core-tests/build.gradle +++ b/core-tests/build.gradle @@ -92,6 +92,8 @@ dependencies { smokeTestCompile project(':smoke-test-utils') smokeTestCompile "org.assertj:assertj-core:${assertj_version}" + // used by FinalityFlowTests + testCompile project(':testing:cordapps:cashobservers') } configurations { diff --git a/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt b/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt index d9857a3350..91c5cd3f15 100644 --- a/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt +++ b/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt @@ -46,6 +46,7 @@ import net.corda.finance.contracts.asset.Cash import net.corda.finance.flows.CashIssueFlow import net.corda.finance.flows.CashPaymentFlow import net.corda.finance.issuedBy +import net.corda.finance.test.flows.CashIssueWithObserversFlow import net.corda.node.services.persistence.DBTransactionStorage import net.corda.testing.contracts.DummyContract import net.corda.testing.core.ALICE_NAME @@ -64,6 +65,7 @@ import net.corda.testing.node.internal.TestCordappInternal import net.corda.testing.node.internal.TestStartedNode import net.corda.testing.node.internal.cordappWithPackages import net.corda.testing.node.internal.enclosedCordapp +import net.corda.testing.node.internal.findCordapp import org.assertj.core.api.Assertions.assertThat import org.junit.After import org.junit.Test @@ -79,6 +81,7 @@ class FinalityFlowTests : WithFinality { } override val mockNet = InternalMockNetwork(cordappsForAllNodes = setOf(FINANCE_CONTRACTS_CORDAPP, FINANCE_WORKFLOWS_CORDAPP, DUMMY_CONTRACTS_CORDAPP, enclosedCordapp(), + findCordapp("net.corda.finance.test.flows"), CustomCordapp(targetPlatformVersion = 3, classes = setOf(FinalityFlow::class.java)))) private val aliceNode = makeNode(ALICE_NAME) @@ -223,7 +226,7 @@ class FinalityFlowTests : WithFinality { assertNull(aliceNode.services.validatedTransactions.getTransactionInternal(stxId)) assertTxnRemovedFromDatabase(aliceNode, stxId) val (_, txnStatus) = bobNode.services.validatedTransactions.getTransactionInternal(stxId) ?: fail() - assertEquals(TransactionStatus.MISSING_NOTARY_SIG, txnStatus) + assertEquals(TransactionStatus.IN_FLIGHT, txnStatus) } } @@ -295,7 +298,7 @@ class FinalityFlowTests : WithFinality { val (_, txnStatusAlice) = aliceNode.services.validatedTransactions.getTransactionInternal(notarisedStxn1.id) ?: fail() assertEquals(TransactionStatus.VERIFIED, txnStatusAlice) val (_, txnStatusBob) = bobNode.services.validatedTransactions.getTransactionInternal(notarisedStxn1.id) ?: fail() - assertEquals(TransactionStatus.MISSING_NOTARY_SIG, txnStatusBob) + assertEquals(TransactionStatus.IN_FLIGHT, txnStatusBob) // now lets attempt a new spend with the new output of the previous transaction val newStateRef = notarisedStxn1.coreTransaction.outRef(1) @@ -309,7 +312,7 @@ class FinalityFlowTests : WithFinality { val (_, txnStatusAlice2) = aliceNode.services.validatedTransactions.getTransactionInternal(notarisedStxn2.id) ?: fail() assertEquals(TransactionStatus.VERIFIED, txnStatusAlice2) val (_, txnStatusBob2) = bobNode.services.validatedTransactions.getTransactionInternal(notarisedStxn2.id) ?: fail() - assertEquals(TransactionStatus.MISSING_NOTARY_SIG, txnStatusBob2) + assertEquals(TransactionStatus.IN_FLIGHT, txnStatusBob2) // Validate attempt at flow finalisation by Bob has no effect on outcome. val finaliseStxn1 = bobNode.startFlowAndRunNetwork(FinaliseSpeedySpendFlow(notarisedStxn1.id, notarisedStxn1.sigs)).resultFuture.getOrThrow() @@ -328,10 +331,22 @@ class FinalityFlowTests : WithFinality { catch (e: UnexpectedFlowEndException) { val stxId = SecureHash.parse(e.message) val (_, txnStatusBob) = bobNode.services.validatedTransactions.getTransactionInternal(stxId) ?: fail() - assertEquals(TransactionStatus.MISSING_NOTARY_SIG, txnStatusBob) + assertEquals(TransactionStatus.IN_FLIGHT, txnStatusBob) } } + @Test(timeout=300_000) + fun `two phase finality flow issuance transaction with observers`() { + val bobNode = createBob(platformVersion = PlatformVersionSwitches.TWO_PHASE_FINALITY) + + val stx = aliceNode.startFlowAndRunNetwork(CashIssueWithObserversFlow( + Amount(1000L, GBP), OpaqueBytes.of(1), notary, + observers = setOf(bobNode.info.singleIdentity()))).resultFuture.getOrThrow().stx + + assertThat(aliceNode.services.validatedTransactions.getTransaction(stx.id)).isNotNull + assertThat(bobNode.services.validatedTransactions.getTransaction(stx.id)).isNotNull + } + @StartableByRPC class IssueFlow(val notary: Party) : FlowLogic>() { diff --git a/core/src/main/kotlin/net/corda/core/flows/FinalityFlow.kt b/core/src/main/kotlin/net/corda/core/flows/FinalityFlow.kt index 6cdbc2b832..57639dcd22 100644 --- a/core/src/main/kotlin/net/corda/core/flows/FinalityFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/FinalityFlow.kt @@ -27,7 +27,7 @@ import java.time.Duration /** * Verifies the given transaction, then sends it to the named notary. If the notary agrees that the transaction * is acceptable then it is from that point onwards committed to the ledger, and will be written through to the - * vault. Additionally it will be distributed to the parties reflected in the participants list of the states. + * vault. Additionally, it will be distributed to the parties reflected in the participants list of the states. * * By default, the initiating flow will commit states that are relevant to the initiating party as indicated by * [StatesToRecord.ONLY_RELEVANT]. Relevance is determined by the union of all participants to states which have been @@ -159,6 +159,7 @@ class FinalityFlow private constructor(val transaction: SignedTransaction, } @Suspendable + @Suppress("ComplexMethod", "NestedBlockDepth") @Throws(NotaryException::class) override fun call(): SignedTransaction { if (!newApi) { @@ -181,12 +182,12 @@ class FinalityFlow private constructor(val transaction: SignedTransaction, val externalTxParticipants = extractExternalParticipants(ledgerTransaction) if (newApi) { - val sessionParties = sessions.map { it.counterparty } - val missingRecipients = externalTxParticipants - sessionParties - oldParticipants + val sessionParties = sessions.map { it.counterparty }.toSet() + val missingRecipients = externalTxParticipants - sessionParties - oldParticipants.toSet() require(missingRecipients.isEmpty()) { "Flow sessions were not provided for the following transaction participants: $missingRecipients" } - sessionParties.intersect(oldParticipants).let { + sessionParties.intersect(oldParticipants.toSet()).let { require(it.isEmpty()) { "The following parties are specified both in flow sessions and in the oldParticipants list: $it" } } } @@ -202,41 +203,48 @@ class FinalityFlow private constructor(val transaction: SignedTransaction, serviceHub.networkMapCache.getNodeByLegalIdentity(it.counterparty)?.platformVersion!! < PlatformVersionSwitches.TWO_PHASE_FINALITY } + val requiresNotarisation = needsNotarySignature(transaction) val useTwoPhaseFinality = serviceHub.myInfo.platformVersion >= PlatformVersionSwitches.TWO_PHASE_FINALITY - if (useTwoPhaseFinality && needsNotarySignature(transaction)) { - recordLocallyAndBroadcast(newPlatformSessions, transaction) - } - - try { - val stxn = notariseOrRecord() - val notarySignatures = stxn.sigs - transaction.sigs.toSet() - if (notarySignatures.isNotEmpty()) { - if (useTwoPhaseFinality && newPlatformSessions.isNotEmpty()) { - broadcastSignaturesAndFinalise(newPlatformSessions, notarySignatures) - } else { - progressTracker.currentStep = FINALISING_TRANSACTION - serviceHub.telemetryServiceInternal.span("${this::class.java.name}#finalizeTransactionWithExtraSignatures", flowLogic = this) { - (serviceHub as ServiceHubCoreInternal).finalizeTransactionWithExtraSignatures(transaction, notarySignatures, statesToRecord) - logger.info("Finalised transaction locally.") + if (useTwoPhaseFinality) { + val stxn = if (requiresNotarisation) { + recordLocallyAndBroadcast(newPlatformSessions, transaction) + try { + val (notarisedTxn, notarySignatures) = notarise() + if (newPlatformSessions.isNotEmpty()) { + broadcastSignaturesAndFinalise(newPlatformSessions, notarySignatures) + } else { + finaliseLocally(notarisedTxn, notarySignatures) } + notarisedTxn + } catch (e: NotaryException) { + (serviceHub as ServiceHubCoreInternal).removeUnnotarisedTransaction(transaction.id) + if (newPlatformSessions.isNotEmpty()) { + broadcastNotaryError(newPlatformSessions, e) + } else sleep(Duration.ZERO) // force checkpoint to persist db update. + throw e } } - - if (!useTwoPhaseFinality || !needsNotarySignature(transaction)) { - broadcastToOtherParticipants(externalTxParticipants, newPlatformSessions + oldPlatformSessions, stxn) - } else if (useTwoPhaseFinality && oldPlatformSessions.isNotEmpty()) { - broadcastToOtherParticipants(externalTxParticipants, oldPlatformSessions, stxn) + else { + if (newPlatformSessions.isNotEmpty()) + finaliseLocallyAndBroadcast(newPlatformSessions, transaction, + FlowTransactionMetadata( + serviceHub.myInfo.legalIdentities.first().name, + statesToRecord, + sessions.map { it.counterparty.name }.toSet())) + else + recordTransactionLocally(transaction) + transaction } + broadcastToOtherParticipants(externalTxParticipants, oldPlatformSessions, stxn) return stxn } - catch (e: NotaryException) { - if (useTwoPhaseFinality) { - (serviceHub as ServiceHubCoreInternal).removeUnnotarisedTransaction(transaction.id) - if (newPlatformSessions.isNotEmpty()) { - broadcastNotaryError(newPlatformSessions, e) - } else sleep(Duration.ZERO) // force checkpoint to persist db update. - } - throw e + else { + val stxn = if (requiresNotarisation) { + notarise().first + } else transaction + recordTransactionLocally(stxn) + broadcastToOtherParticipants(externalTxParticipants, newPlatformSessions + oldPlatformSessions, stxn) + return stxn } } @@ -244,16 +252,30 @@ class FinalityFlow private constructor(val transaction: SignedTransaction, private fun recordLocallyAndBroadcast(sessions: Collection, tx: SignedTransaction) { serviceHub.telemetryServiceInternal.span("${this::class.java.name}#recordLocallyAndBroadcast", flowLogic = this) { recordUnnotarisedTransaction(tx) - logger.info("Recorded transaction without notary signature locally.") - if (sessions.isEmpty()) return progressTracker.currentStep = BROADCASTING_PRE_NOTARISATION + broadcast(sessions, tx) + } + } + + @Suspendable + private fun finaliseLocallyAndBroadcast(sessions: Collection, tx: SignedTransaction, metadata: FlowTransactionMetadata) { + serviceHub.telemetryServiceInternal.span("${this::class.java.name}#finaliseLocallyAndBroadcast", flowLogic = this) { + finaliseLocally(tx, metadata = metadata) + progressTracker.currentStep = BROADCASTING + broadcast(sessions, tx) + } + } + + @Suspendable + private fun broadcast(sessions: Collection, tx: SignedTransaction) { + serviceHub.telemetryServiceInternal.span("${this::class.java.name}#broadcast", flowLogic = this) { sessions.forEach { session -> try { logger.debug { "Sending transaction to party $session." } subFlow(SendTransactionFlow(session, tx)) } catch (e: UnexpectedFlowEndException) { throw UnexpectedFlowEndException( - "${session.counterparty} has finished prematurely and we're trying to send them a transaction without notary signature. " + + "${session.counterparty} has finished prematurely and we're trying to send them a transaction." + "Did they forget to call ReceiveFinalityFlow? (${e.message})", e.cause, e.originalErrorId @@ -282,9 +304,20 @@ class FinalityFlow private constructor(val transaction: SignedTransaction, ) } } - progressTracker.currentStep = FINALISING_TRANSACTION - serviceHub.telemetryServiceInternal.span("${this::class.java.name}#finalizeTransactionWithExtraSignatures", flowLogic = this) { - (serviceHub as ServiceHubCoreInternal).finalizeTransactionWithExtraSignatures(transaction, notarySignatures, statesToRecord) + finaliseLocally(transaction, notarySignatures) + } + } + + @Suspendable + private fun finaliseLocally(stx: SignedTransaction, notarySignatures: List = emptyList(), + metadata: FlowTransactionMetadata? = null) { + progressTracker.currentStep = FINALISING_TRANSACTION + serviceHub.telemetryServiceInternal.span("${this::class.java.name}#finaliseLocally", flowLogic = this) { + if (notarySignatures.isEmpty()) { + (serviceHub as ServiceHubCoreInternal).finalizeTransaction(stx, statesToRecord, metadata!!) + logger.info("Finalised transaction locally.") + } else { + (serviceHub as ServiceHubCoreInternal).finalizeTransactionWithExtraSignatures(stx, notarySignatures, statesToRecord) logger.info("Finalised transaction locally with notary signature.") } } @@ -376,22 +409,17 @@ class FinalityFlow private constructor(val transaction: SignedTransaction, serviceHub.myInfo.legalIdentities.first().name, statesToRecord, sessions.map { it.counterparty.name }.toSet())) + logger.info("Recorded un-notarised transaction locally.") return tx } } @Suspendable - private fun notariseOrRecord(): SignedTransaction { - serviceHub.telemetryServiceInternal.span("${this::class.java.name}#notariseOrRecord", flowLogic = this) { - return if (needsNotarySignature(transaction)) { - progressTracker.currentStep = NOTARISING - val notarySignatures = subFlow(NotaryFlow.Client(transaction, skipVerification = true)) - transaction + notarySignatures - } else { - logger.info("No need to notarise this transaction. Recording locally.") - recordTransactionLocally(transaction) - transaction - } + private fun notarise(): Pair> { + return serviceHub.telemetryServiceInternal.span("${this::class.java.name}#notariseOrRecord", flowLogic = this) { + progressTracker.currentStep = NOTARISING + val notarySignatures = subFlow(NotaryFlow.Client(transaction, skipVerification = true)) + Pair(transaction + notarySignatures, notarySignatures) } } @@ -409,7 +437,7 @@ class FinalityFlow private constructor(val transaction: SignedTransaction, private fun extractExternalParticipants(ltx: LedgerTransaction): Set { val participants = ltx.outputStates.flatMap { it.participants } + ltx.inputStates.flatMap { it.participants } - return groupAbstractPartyByWellKnownParty(serviceHub, participants).keys - serviceHub.myInfo.legalIdentities + return groupAbstractPartyByWellKnownParty(serviceHub, participants).keys - serviceHub.myInfo.legalIdentities.toSet() } private fun verifyTx(): LedgerTransaction { @@ -456,40 +484,49 @@ class ReceiveFinalityFlow @JvmOverloads constructor(private val otherSideSession private val expectedTxId: SecureHash? = null, private val statesToRecord: StatesToRecord = ONLY_RELEVANT, private val handlePropagatedNotaryError: Boolean? = null) : FlowLogic() { - @Suppress("ComplexMethod") + @Suppress("ComplexMethod", "NestedBlockDepth") @Suspendable override fun call(): SignedTransaction { val stx = subFlow(ReceiveTransactionFlow(otherSideSession, checkSufficientSignatures = false, statesToRecord = statesToRecord, deferredAck = true)) + val requiresNotarisation = needsNotarySignature(stx) val fromTwoPhaseFinalityNode = serviceHub.networkMapCache.getNodeByLegalIdentity(otherSideSession.counterparty)?.platformVersion!! >= PlatformVersionSwitches.TWO_PHASE_FINALITY - if (fromTwoPhaseFinalityNode && needsNotarySignature(stx)) { - serviceHub.telemetryServiceInternal.span("${this::class.java.name}#recordUnnotarisedTransaction", flowLogic = this) { - logger.debug { "Peer recording transaction without notary signature." } - (serviceHub as ServiceHubCoreInternal).recordUnnotarisedTransaction(stx, - FlowTransactionMetadata(otherSideSession.counterparty.name, statesToRecord)) - } - otherSideSession.send(FetchDataFlow.Request.End) // Finish fetching data (deferredAck) - logger.info("Peer recorded transaction without notary signature. Waiting to receive notary signature.") - - try { - val notarySignatures = otherSideSession.receive>>().unwrap { it.getOrThrow() } - serviceHub.telemetryServiceInternal.span("${this::class.java.name}#finalizeTransactionWithExtraSignatures", flowLogic = this) { - logger.debug { "Peer received notarised signature." } - (serviceHub as ServiceHubCoreInternal).finalizeTransactionWithExtraSignatures(stx, notarySignatures, statesToRecord) - logger.info("Peer finalised transaction with notary signature.") + if (fromTwoPhaseFinalityNode) { + if (requiresNotarisation) { + serviceHub.telemetryServiceInternal.span("${this::class.java.name}#recordUnnotarisedTransaction", flowLogic = this) { + logger.debug { "Peer recording transaction without notary signature." } + (serviceHub as ServiceHubCoreInternal).recordUnnotarisedTransaction(stx, + FlowTransactionMetadata(otherSideSession.counterparty.name, statesToRecord)) } - } catch (e: NotaryException) { - logger.info("Peer received notary error.") - val overrideHandlePropagatedNotaryError = handlePropagatedNotaryError ?: + otherSideSession.send(FetchDataFlow.Request.End) // Finish fetching data (deferredAck) + logger.info("Peer recorded transaction without notary signature. Waiting to receive notary signature.") + try { + val notarySignatures = otherSideSession.receive>>().unwrap { it.getOrThrow() } + serviceHub.telemetryServiceInternal.span("${this::class.java.name}#finalizeTransactionWithExtraSignatures", flowLogic = this) { + logger.debug { "Peer received notarised signature." } + (serviceHub as ServiceHubCoreInternal).finalizeTransactionWithExtraSignatures(stx, notarySignatures, statesToRecord) + logger.info("Peer finalised transaction with notary signature.") + } + } catch (e: NotaryException) { + logger.info("Peer received notary error.") + val overrideHandlePropagatedNotaryError = handlePropagatedNotaryError ?: (serviceHub.cordappProvider.getAppContext().cordapp.targetPlatformVersion >= PlatformVersionSwitches.TWO_PHASE_FINALITY) - if (overrideHandlePropagatedNotaryError) { - (serviceHub as ServiceHubCoreInternal).removeUnnotarisedTransaction(stx.id) - sleep(Duration.ZERO) // force checkpoint to persist db update. - throw e + if (overrideHandlePropagatedNotaryError) { + (serviceHub as ServiceHubCoreInternal).removeUnnotarisedTransaction(stx.id) + sleep(Duration.ZERO) // force checkpoint to persist db update. + throw e + } + else { + otherSideSession.receive() // simulate unexpected flow end + } } - else { - otherSideSession.receive() // simulate unexpected flow end + } else { + serviceHub.telemetryServiceInternal.span("${this::class.java.name}#finalizeTransaction", flowLogic = this) { + (serviceHub as ServiceHubCoreInternal).finalizeTransaction(stx, statesToRecord, + FlowTransactionMetadata(otherSideSession.counterparty.name, statesToRecord)) + logger.info("Peer recorded transaction with recovery metadata.") } + otherSideSession.send(FetchDataFlow.Request.End) // Finish fetching data (deferredAck) } } else { serviceHub.telemetryServiceInternal.span("${this::class.java.name}#recordTransactions", flowLogic = this) { diff --git a/core/src/main/kotlin/net/corda/core/flows/FlowTransaction.kt b/core/src/main/kotlin/net/corda/core/flows/FlowTransaction.kt index 49ecae6151..f66d6990ea 100644 --- a/core/src/main/kotlin/net/corda/core/flows/FlowTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/flows/FlowTransaction.kt @@ -33,5 +33,5 @@ data class FlowTransactionMetadata( enum class TransactionStatus { UNVERIFIED, VERIFIED, - MISSING_NOTARY_SIG; + IN_FLIGHT; } \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/internal/ServiceHubCoreInternal.kt b/core/src/main/kotlin/net/corda/core/internal/ServiceHubCoreInternal.kt index 36b44ed0e1..5e63caebec 100644 --- a/core/src/main/kotlin/net/corda/core/internal/ServiceHubCoreInternal.kt +++ b/core/src/main/kotlin/net/corda/core/internal/ServiceHubCoreInternal.kt @@ -30,13 +30,14 @@ interface ServiceHubCoreInternal : ServiceHub { val attachmentsClassLoaderCache: AttachmentsClassLoaderCache /** - * Stores [SignedTransaction] and participant signatures without the notary signature in the local transaction storage. - * Optionally add finality flow recovery metadata. + * Stores [SignedTransaction] and participant signatures without the notary signature in the local transaction storage, + * inclusive of flow recovery metadata. * This is expected to be run within a database transaction. * * @param txn The transaction to record. + * @param metadata Finality flow recovery metadata. */ - fun recordUnnotarisedTransaction(txn: SignedTransaction, metadata: FlowTransactionMetadata?= null) + fun recordUnnotarisedTransaction(txn: SignedTransaction, metadata: FlowTransactionMetadata) /** * Removes transaction from data store. @@ -54,6 +55,15 @@ interface ServiceHubCoreInternal : ServiceHub { * @param statesToRecord how the vault should treat the output states of the transaction. */ fun finalizeTransactionWithExtraSignatures(txn: SignedTransaction, sigs: Collection, statesToRecord: StatesToRecord) + + /** + * Records a [SignedTransaction] as VERIFIED with flow recovery metadata. + * + * @param txn The transaction to record. + * @param statesToRecord how the vault should treat the output states of the transaction. + * @param metadata Finality flow recovery metadata. + */ + fun finalizeTransaction(txn: SignedTransaction, statesToRecord: StatesToRecord, metadata: FlowTransactionMetadata) } interface TransactionsResolver { diff --git a/node/build.gradle b/node/build.gradle index 2f2ba885ad..05dc5e58a5 100644 --- a/node/build.gradle +++ b/node/build.gradle @@ -251,6 +251,9 @@ dependencies { integrationTestCompile(project(":testing:cordapps:missingmigration")) testCompile project(':testing:cordapps:dbfailure:dbfworkflows') + + // used by FinalityFlowErrorHandlingTest + slowIntegrationTestCompile project(':testing:cordapps:cashobservers') } tasks.withType(JavaCompile).configureEach { diff --git a/node/src/integration-test-slow/kotlin/net/corda/node/flows/FinalityFlowErrorHandlingTest.kt b/node/src/integration-test-slow/kotlin/net/corda/node/flows/FinalityFlowErrorHandlingTest.kt new file mode 100644 index 0000000000..bb73f7da04 --- /dev/null +++ b/node/src/integration-test-slow/kotlin/net/corda/node/flows/FinalityFlowErrorHandlingTest.kt @@ -0,0 +1,101 @@ +package net.corda.node.flows + +import co.paralleluniverse.fibers.Suspendable +import net.corda.core.CordaRuntimeException +import net.corda.core.crypto.SecureHash +import net.corda.core.flows.FinalityFlow +import net.corda.core.flows.FlowLogic +import net.corda.core.flows.StartableByRPC +import net.corda.core.messaging.startFlow +import net.corda.core.utilities.OpaqueBytes +import net.corda.core.utilities.getOrThrow +import net.corda.core.utilities.seconds +import net.corda.finance.DOLLARS +import net.corda.finance.test.flows.CashIssueWithObserversFlow +import net.corda.node.services.statemachine.StateMachineErrorHandlingTest +import net.corda.testing.core.ALICE_NAME +import net.corda.testing.core.CHARLIE_NAME +import net.corda.testing.core.DUMMY_NOTARY_NAME +import net.corda.testing.core.singleIdentity +import net.corda.testing.flows.waitForAllFlowsToComplete +import net.corda.testing.node.NotarySpec +import net.corda.testing.node.internal.FINANCE_CORDAPPS +import net.corda.testing.node.internal.enclosedCordapp +import org.junit.Test +import kotlin.test.assertEquals +import kotlin.test.fail + +class FinalityFlowErrorHandlingTest : StateMachineErrorHandlingTest() { + + /** + * Throws an exception after recording an issuance transaction but before broadcasting the transaction to observer sessions. + * + */ + @Test(timeout = 300_000) + fun `error after recording an issuance transaction inside of FinalityFlow generates recovery metadata`() { + startDriver(notarySpec = NotarySpec(DUMMY_NOTARY_NAME, validating = false), + extraCordappPackagesToScan = listOf("net.corda.node.flows", "net.corda.finance.test.flows")) { + val (charlie, alice, port) = createNodeAndBytemanNode(CHARLIE_NAME, ALICE_NAME, FINANCE_CORDAPPS + enclosedCordapp()) + + val rules = """ + RULE Set flag when entering receive finality flow + CLASS ${FinalityFlow::class.java.name} + METHOD call + AT ENTRY + IF !flagged("finality_flag") + DO flag("finality_flag"); traceln("Setting finality flag") + ENDRULE + + RULE Throw exception when recording transaction + CLASS ${FinalityFlow::class.java.name} + METHOD finaliseLocallyAndBroadcast + AT EXIT + IF flagged("finality_flag") + DO traceln("Throwing exception"); + throw new java.lang.RuntimeException("die dammit die") + ENDRULE + """.trimIndent() + + submitBytemanRules(rules, port) + + try { + alice.rpc.startFlow( + ::CashIssueWithObserversFlow, + 500.DOLLARS, + OpaqueBytes.of(0x01), + defaultNotaryIdentity, + setOf(charlie.nodeInfo.singleIdentity()) + ).returnValue.getOrThrow(30.seconds) + fail() + } + catch (e: CordaRuntimeException) { + waitForAllFlowsToComplete(alice) + val txId = alice.rpc.stateMachineRecordedTransactionMappingSnapshot().single().transactionId + + alice.rpc.startFlow(::GetFlowTransaction, txId).returnValue.getOrThrow().apply { + assertEquals("V", this.first) // "V" -> VERIFIED + assertEquals(ALICE_NAME.toString(), this.second) // initiator + assertEquals(CHARLIE_NAME.toString(), this.third) // peers + } + } + } + } +} + +// Internal use for testing only!! +@StartableByRPC +class GetFlowTransaction(private val txId: SecureHash) : FlowLogic>() { + @Suspendable + override fun call(): Triple { + return serviceHub.jdbcSession().prepareStatement("select * from node_transactions where tx_id = ?") + .apply { setString(1, txId.toString()) } + .use { ps -> + ps.executeQuery().use { rs -> + rs.next() + Triple(rs.getString(4), // TransactionStatus + rs.getString(7), // initiator + rs.getString(8)) // participants + } + } + } +} \ No newline at end of file diff --git a/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StateMachineErrorHandlingTest.kt b/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StateMachineErrorHandlingTest.kt index 3e0882e62e..8885b936ee 100644 --- a/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StateMachineErrorHandlingTest.kt +++ b/node/src/integration-test-slow/kotlin/net/corda/node/services/statemachine/StateMachineErrorHandlingTest.kt @@ -49,13 +49,16 @@ abstract class StateMachineErrorHandlingTest { counter = 0 } - internal fun startDriver(notarySpec: NotarySpec = NotarySpec(DUMMY_NOTARY_NAME), dsl: DriverDSL.() -> Unit) { + internal fun startDriver(notarySpec: NotarySpec = NotarySpec(DUMMY_NOTARY_NAME), + extraCordappPackagesToScan: List = emptyList(), + dsl: DriverDSL.() -> Unit) { driver( DriverParameters( notarySpecs = listOf(notarySpec), startNodesInProcess = false, inMemoryDB = false, - systemProperties = mapOf("co.paralleluniverse.fibers.verifyInstrumentation" to "true") + systemProperties = mapOf("co.paralleluniverse.fibers.verifyInstrumentation" to "true"), + extraCordappPackagesToScan = extraCordappPackagesToScan ) ) { dsl() diff --git a/node/src/integration-test/kotlin/net/corda/node/services/statemachine/FlowHospitalTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/statemachine/FlowHospitalTest.kt index 67a8f8144b..f14f60cc5b 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/statemachine/FlowHospitalTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/statemachine/FlowHospitalTest.kt @@ -354,6 +354,7 @@ class FlowHospitalTest { it.startFlow(::CreateTransactionButDontFinalizeFlow, nodeBHandle.nodeInfo.singleIdentity(), ref3).returnValue.getOrThrow(20.seconds) } waitForAllFlowsToComplete(nodeAHandle) + waitForAllFlowsToComplete(nodeBHandle) } assertEquals(0, dischargedCounter) assertEquals(1, observationCounter) diff --git a/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt b/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt index cb917e0b51..60df01f809 100644 --- a/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt +++ b/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt @@ -160,7 +160,6 @@ interface ServiceHubInternal : ServiceHubCoreInternal { vaultService: VaultServiceInternal, database: CordaPersistence) { database.transaction { - require(sigs.isNotEmpty()) { "No signatures passed in for recording" } recordTransactions(statesToRecord, listOf(txn), validatedTransactions, stateMachineRecordedTransactionMapping, vaultService, database) { validatedTransactions.finalizeTransactionWithExtraSignatures(it, sigs) } @@ -227,6 +226,7 @@ interface ServiceHubInternal : ServiceHubCoreInternal { override fun finalizeTransactionWithExtraSignatures(txn: SignedTransaction, sigs: Collection, statesToRecord: StatesToRecord) { requireSupportedHashType(txn) + require(sigs.isNotEmpty()) { "No signatures passed in for recording" } if (txn.coreTransaction is WireTransaction) (txn + sigs).verifyRequiredSignatures() finalizeTransactionWithExtraSignatures( @@ -240,7 +240,18 @@ interface ServiceHubInternal : ServiceHubCoreInternal { ) } - override fun recordUnnotarisedTransaction(txn: SignedTransaction, metadata: FlowTransactionMetadata?) { + override fun finalizeTransaction(txn: SignedTransaction, statesToRecord: StatesToRecord, metadata: FlowTransactionMetadata) { + requireSupportedHashType(txn) + if (txn.coreTransaction is WireTransaction) + txn.verifyRequiredSignatures() + database.transaction { + recordTransactions(statesToRecord, listOf(txn), validatedTransactions, stateMachineRecordedTransactionMapping, vaultService, database) { + validatedTransactions.finalizeTransaction(txn, metadata) + } + } + } + + override fun recordUnnotarisedTransaction(txn: SignedTransaction, metadata: FlowTransactionMetadata) { if (txn.coreTransaction is WireTransaction) { txn.notary?.let { notary -> txn.verifySignaturesExcept(notary.owningKey) @@ -344,13 +355,13 @@ interface WritableTransactionStorage : TransactionStorage { fun addTransaction(transaction: SignedTransaction): Boolean /** - * Add an un-notarised transaction to the store with a status of *MISSING_TRANSACTION_SIG*. - * Optionally add finality flow recovery metadata. + * Add an un-notarised transaction to the store with a status of *MISSING_TRANSACTION_SIG* and inclusive of flow recovery metadata. + * * @param transaction The transaction to be recorded. * @param metadata Finality flow recovery metadata. * @return true if the transaction was recorded as a *new* transaction, false if the transaction already exists. */ - fun addUnnotarisedTransaction(transaction: SignedTransaction, metadata: FlowTransactionMetadata? = null): Boolean + fun addUnnotarisedTransaction(transaction: SignedTransaction, metadata: FlowTransactionMetadata): Boolean /** * Removes an un-notarised transaction (with a status of *MISSING_TRANSACTION_SIG*) from the data store. @@ -358,6 +369,15 @@ interface WritableTransactionStorage : TransactionStorage { */ fun removeUnnotarisedTransaction(id: SecureHash): Boolean + /** + * Add a finalised transaction to the store with flow recovery metadata. + * + * @param transaction The transaction to be recorded. + * @param metadata Finality flow recovery metadata. + * @return true if the transaction was recorded as a *new* transaction, false if the transaction already exists. + */ + fun finalizeTransaction(transaction: SignedTransaction, metadata: FlowTransactionMetadata): Boolean + /** * Update a previously un-notarised transaction including associated notary signatures. * @param transaction The notarised transaction to be finalized. diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt index 7773fdcd22..dd69c26fc3 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt @@ -103,13 +103,13 @@ class DBTransactionStorage(private val database: CordaPersistence, cacheFactory: enum class TransactionStatus { UNVERIFIED, VERIFIED, - MISSING_NOTARY_SIG; + IN_FLIGHT; fun toDatabaseValue(): String { return when (this) { UNVERIFIED -> "U" VERIFIED -> "V" - MISSING_NOTARY_SIG -> "M" + IN_FLIGHT -> "F" } } @@ -121,7 +121,7 @@ class DBTransactionStorage(private val database: CordaPersistence, cacheFactory: return when(this) { UNVERIFIED -> net.corda.core.flows.TransactionStatus.UNVERIFIED VERIFIED -> net.corda.core.flows.TransactionStatus.VERIFIED - MISSING_NOTARY_SIG -> net.corda.core.flows.TransactionStatus.MISSING_NOTARY_SIG + IN_FLIGHT -> net.corda.core.flows.TransactionStatus.IN_FLIGHT } } @@ -130,7 +130,7 @@ class DBTransactionStorage(private val database: CordaPersistence, cacheFactory: return when (databaseValue) { "V" -> VERIFIED "U" -> UNVERIFIED - "M" -> MISSING_NOTARY_SIG + "F" -> IN_FLIGHT else -> throw UnexpectedStatusValueException(databaseValue) } } @@ -241,7 +241,7 @@ class DBTransactionStorage(private val database: CordaPersistence, cacheFactory: criteriaUpdate.set(updateRoot.get(DBTransaction::status.name), TransactionStatus.VERIFIED) criteriaUpdate.where(criteriaBuilder.and( criteriaBuilder.equal(updateRoot.get(DBTransaction::txId.name), txId.toString()), - criteriaBuilder.and(updateRoot.get(DBTransaction::status.name).`in`(setOf(TransactionStatus.UNVERIFIED, TransactionStatus.MISSING_NOTARY_SIG)) + criteriaBuilder.and(updateRoot.get(DBTransaction::status.name).`in`(setOf(TransactionStatus.UNVERIFIED, TransactionStatus.IN_FLIGHT)) ))) criteriaUpdate.set(updateRoot.get(DBTransaction::timestamp.name), clock.instant()) val update = session.createQuery(criteriaUpdate) @@ -254,20 +254,15 @@ class DBTransactionStorage(private val database: CordaPersistence, cacheFactory: updateTransaction(transaction.id) } - override fun addUnnotarisedTransaction(transaction: SignedTransaction, metadata: FlowTransactionMetadata?) = - database.transaction { - txStorage.locked { - val cacheValue = TxCacheValue(transaction, status = TransactionStatus.MISSING_NOTARY_SIG, metadata = metadata) - val added = addWithDuplicatesAllowed(transaction.id, cacheValue) - if (added) { - logger.info ("Transaction ${transaction.id} recorded as un-notarised.") - } else { - logger.info("Transaction ${transaction.id} (un-notarised) already exists so no need to record.") - } - added - } + override fun addUnnotarisedTransaction(transaction: SignedTransaction, metadata: FlowTransactionMetadata) = + addTransaction(transaction, metadata, TransactionStatus.IN_FLIGHT) { + false } + override fun finalizeTransaction(transaction: SignedTransaction, metadata: FlowTransactionMetadata) = + addTransaction(transaction, metadata) { + false + } override fun removeUnnotarisedTransaction(id: SecureHash): Boolean { return database.transaction { val session = currentDBSession() @@ -276,7 +271,7 @@ class DBTransactionStorage(private val database: CordaPersistence, cacheFactory: val root = delete.from(DBTransaction::class.java) delete.where(criteriaBuilder.and( criteriaBuilder.equal(root.get(DBTransaction::txId.name), id.toString()), - criteriaBuilder.equal(root.get(DBTransaction::status.name), TransactionStatus.MISSING_NOTARY_SIG) + criteriaBuilder.equal(root.get(DBTransaction::status.name), TransactionStatus.IN_FLIGHT) )) if (session.createQuery(delete).executeUpdate() != 0) { txStorage.locked { @@ -294,16 +289,21 @@ class DBTransactionStorage(private val database: CordaPersistence, cacheFactory: finalizeTransactionWithExtraSignatures(transaction.id, signatures) } - private fun addTransaction(transaction: SignedTransaction, updateFn: (SecureHash) -> Boolean): Boolean { + private fun addTransaction(transaction: SignedTransaction, + metadata: FlowTransactionMetadata? = null, + status: TransactionStatus = TransactionStatus.VERIFIED, + updateFn: (SecureHash) -> Boolean): Boolean { return database.transaction { txStorage.locked { - val cachedValue = TxCacheValue(transaction, TransactionStatus.VERIFIED) + val cachedValue = TxCacheValue(transaction, status, metadata) val addedOrUpdated = addOrUpdate(transaction.id, cachedValue) { k, _ -> updateFn(k) } if (addedOrUpdated) { - logger.debug { "Transaction ${transaction.id} has been recorded as verified" } - onNewTx(transaction) + logger.debug { "Transaction ${transaction.id} has been recorded as $status" } + if (status.isVerified()) + onNewTx(transaction) + true } else { - logger.debug { "Transaction ${transaction.id} is already recorded as verified, so no need to re-record" } + logger.debug { "Transaction ${transaction.id} is already recorded as $status, so no need to re-record" } false } } @@ -320,7 +320,7 @@ class DBTransactionStorage(private val database: CordaPersistence, cacheFactory: criteriaUpdate.set(updateRoot.get(DBTransaction::status.name), TransactionStatus.VERIFIED) criteriaUpdate.where(criteriaBuilder.and( criteriaBuilder.equal(updateRoot.get(DBTransaction::txId.name), txId.toString()), - criteriaBuilder.equal(updateRoot.get(DBTransaction::status.name), TransactionStatus.MISSING_NOTARY_SIG) + criteriaBuilder.equal(updateRoot.get(DBTransaction::status.name), TransactionStatus.IN_FLIGHT) )) criteriaUpdate.set(updateRoot.get(DBTransaction::timestamp.name), clock.instant()) val update = session.createQuery(criteriaUpdate) @@ -360,7 +360,7 @@ class DBTransactionStorage(private val database: CordaPersistence, cacheFactory: txStorage.locked { val cacheValue = TxCacheValue(transaction, status = TransactionStatus.UNVERIFIED) val added = addWithDuplicatesAllowed(transaction.id, cacheValue) { k, v, existingEntry -> - if (existingEntry.status == TransactionStatus.MISSING_NOTARY_SIG) { + if (existingEntry.status == TransactionStatus.IN_FLIGHT) { session.merge(toPersistentEntity(k, v)) true } else false diff --git a/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt b/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt index 7e3ed4f2f9..d1436c94da 100644 --- a/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt +++ b/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt @@ -801,10 +801,10 @@ class TwoPartyTradeFlowTests(private val anonymous: Boolean) { return true } - override fun addUnnotarisedTransaction(transaction: SignedTransaction, metadata: FlowTransactionMetadata?): Boolean { + override fun addUnnotarisedTransaction(transaction: SignedTransaction, metadata: FlowTransactionMetadata): Boolean { database.transaction { records.add(TxRecord.Add(transaction)) - delegate.addUnnotarisedTransaction(transaction) + delegate.addUnnotarisedTransaction(transaction, metadata) } return true } @@ -815,6 +815,13 @@ class TwoPartyTradeFlowTests(private val anonymous: Boolean) { } } + override fun finalizeTransaction(transaction: SignedTransaction, metadata: FlowTransactionMetadata): Boolean { + database.transaction { + delegate.finalizeTransaction(transaction, metadata) + } + return true + } + override fun finalizeTransactionWithExtraSignatures(transaction: SignedTransaction, signatures: Collection) : Boolean { database.transaction { delegate.finalizeTransactionWithExtraSignatures(transaction, signatures) diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageTests.kt b/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageTests.kt index 16889e886e..cfc9c3ecf1 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageTests.kt @@ -19,7 +19,7 @@ import net.corda.core.transactions.WireTransaction import net.corda.node.CordaClock import net.corda.node.MutableClock import net.corda.node.SimpleClock -import net.corda.node.services.persistence.DBTransactionStorage.TransactionStatus.MISSING_NOTARY_SIG +import net.corda.node.services.persistence.DBTransactionStorage.TransactionStatus.IN_FLIGHT import net.corda.node.services.persistence.DBTransactionStorage.TransactionStatus.UNVERIFIED import net.corda.node.services.persistence.DBTransactionStorage.TransactionStatus.VERIFIED import net.corda.node.services.transactions.PersistentUniquenessProvider @@ -113,8 +113,8 @@ class DBTransactionStorageTests { val transactionClock = TransactionClock(now) newTransactionStorage(clock = transactionClock) val transaction = newTransaction() - transactionStorage.addUnnotarisedTransaction(transaction) - assertEquals(MISSING_NOTARY_SIG, readTransactionFromDB(transaction.id).status) + transactionStorage.addUnnotarisedTransaction(transaction, FlowTransactionMetadata(ALICE.party.name)) + assertEquals(IN_FLIGHT, readTransactionFromDB(transaction.id).status) } @Test(timeout = 300_000) @@ -125,7 +125,7 @@ class DBTransactionStorageTests { val transaction = newTransaction() transactionStorage.addUnnotarisedTransaction(transaction, FlowTransactionMetadata(ALICE.party.name, StatesToRecord.ALL_VISIBLE, setOf(BOB_PARTY.name))) val txn = readTransactionFromDB(transaction.id) - assertEquals(MISSING_NOTARY_SIG, txn.status) + assertEquals(IN_FLIGHT, txn.status) assertEquals(StatesToRecord.ALL_VISIBLE, txn.statesToRecord) assertEquals(ALICE_NAME.toString(), txn.initiator) assertEquals(listOf(BOB_NAME.toString()), txn.participants) @@ -144,15 +144,46 @@ class DBTransactionStorageTests { } } + @Test(timeout = 300_000) + fun `finalize transaction after recording transaction as un-notarised`() { + val now = Instant.ofEpochSecond(333444555L) + val transactionClock = TransactionClock(now) + newTransactionStorage(clock = transactionClock) + val transaction = newTransaction(notarySig = false) + transactionStorage.addUnnotarisedTransaction(transaction, FlowTransactionMetadata(ALICE.party.name)) + assertNull(transactionStorage.getTransaction(transaction.id)) + assertEquals(IN_FLIGHT, readTransactionFromDB(transaction.id).status) + transactionStorage.finalizeTransactionWithExtraSignatures(transaction, emptyList()) + readTransactionFromDB(transaction.id).let { + assertSignatures(it.transaction, it.signatures, transaction.sigs) + assertEquals(VERIFIED, it.status) + } + } + + @Test(timeout = 300_000) + fun `finalize transaction with recovery metadata`() { + val now = Instant.ofEpochSecond(333444555L) + val transactionClock = TransactionClock(now) + newTransactionStorage(clock = transactionClock) + val transaction = newTransaction(notarySig = false) + transactionStorage.finalizeTransaction(transaction, + FlowTransactionMetadata(ALICE_NAME)) + readTransactionFromDB(transaction.id).let { + assertEquals(VERIFIED, it.status) + assertEquals(ALICE_NAME.toString(), it.initiator) + assertEquals(StatesToRecord.ONLY_RELEVANT, it.statesToRecord) + } + } + @Test(timeout = 300_000) fun `finalize transaction with extra signatures after recording transaction as un-notarised`() { val now = Instant.ofEpochSecond(333444555L) val transactionClock = TransactionClock(now) newTransactionStorage(clock = transactionClock) val transaction = newTransaction(notarySig = false) - transactionStorage.addUnnotarisedTransaction(transaction) + transactionStorage.addUnnotarisedTransaction(transaction, FlowTransactionMetadata(ALICE.party.name)) assertNull(transactionStorage.getTransaction(transaction.id)) - assertEquals(MISSING_NOTARY_SIG, readTransactionFromDB(transaction.id).status) + assertEquals(IN_FLIGHT, readTransactionFromDB(transaction.id).status) val notarySig = notarySig(transaction.id) transactionStorage.finalizeTransactionWithExtraSignatures(transaction, listOf(notarySig)) readTransactionFromDB(transaction.id).let { @@ -167,9 +198,9 @@ class DBTransactionStorageTests { val transactionClock = TransactionClock(now) newTransactionStorage(clock = transactionClock) val transaction = newTransaction(notarySig = false) - transactionStorage.addUnnotarisedTransaction(transaction) + transactionStorage.addUnnotarisedTransaction(transaction, FlowTransactionMetadata(ALICE.party.name)) assertNull(transactionStorage.getTransaction(transaction.id)) - assertEquals(MISSING_NOTARY_SIG, readTransactionFromDB(transaction.id).status) + assertEquals(IN_FLIGHT, readTransactionFromDB(transaction.id).status) assertEquals(true, transactionStorage.removeUnnotarisedTransaction(transaction.id)) assertFailsWith { readTransactionFromDB(transaction.id).status } @@ -201,8 +232,8 @@ class DBTransactionStorageTests { val transactionWithoutNotarySig = newTransaction(notarySig = false) // txn recorded as un-notarised (simulate ReceiverFinalityFlow in initial flow) - transactionStorage.addUnnotarisedTransaction(transactionWithoutNotarySig) - assertEquals(MISSING_NOTARY_SIG, readTransactionFromDB(transactionWithoutNotarySig.id).status) + transactionStorage.addUnnotarisedTransaction(transactionWithoutNotarySig, FlowTransactionMetadata(ALICE.party.name)) + assertEquals(IN_FLIGHT, readTransactionFromDB(transactionWithoutNotarySig.id).status) // txn then recorded as unverified (simulate ResolveTransactionFlow in follow-up flow) val notarySig = notarySig(transactionWithoutNotarySig.id) @@ -232,8 +263,8 @@ class DBTransactionStorageTests { val transactionWithoutNotarySigs = newTransaction(notarySig = false) // txn recorded as un-notarised (simulate ReceiverFinalityFlow in initial flow) - transactionStorage.addUnnotarisedTransaction(transactionWithoutNotarySigs) - assertEquals(MISSING_NOTARY_SIG, readTransactionFromDB(transactionWithoutNotarySigs.id).status) + transactionStorage.addUnnotarisedTransaction(transactionWithoutNotarySigs, FlowTransactionMetadata(ALICE.party.name)) + assertEquals(IN_FLIGHT, readTransactionFromDB(transactionWithoutNotarySigs.id).status) // txn then recorded as unverified (simulate ResolveTransactionFlow in follow-up flow) val notarySig = notarySig(transactionWithoutNotarySigs.id) diff --git a/settings.gradle b/settings.gradle index 395b3a40b6..9c1a33ad3e 100644 --- a/settings.gradle +++ b/settings.gradle @@ -102,6 +102,7 @@ include 'testing:cordapps:dbfailure:dbfcontracts' include 'testing:cordapps:dbfailure:dbfworkflows' include 'testing:cordapps:missingmigration' include 'testing:cordapps:sleeping' +include 'testing:cordapps:cashobservers' // Common libraries - start include 'common-validation' diff --git a/testing/cordapps/cashobservers/build.gradle b/testing/cordapps/cashobservers/build.gradle new file mode 100644 index 0000000000..e8f0d47d5f --- /dev/null +++ b/testing/cordapps/cashobservers/build.gradle @@ -0,0 +1,17 @@ +apply plugin: 'kotlin' +//apply plugin: 'net.corda.plugins.cordapp' +//apply plugin: 'net.corda.plugins.quasar-utils' + +dependencies { + compile project(":core") + compile project(':finance:workflows') +} + +jar { + baseName "testing-cashobservers-cordapp" + manifest { + // This JAR is part of Corda's testing framework. + // Driver will not include it as part of an out-of-process node. + attributes('Corda-Testing': true) + } +} \ No newline at end of file diff --git a/testing/cordapps/cashobservers/src/main/kotlin/net/corda/finance/test/flows/CashIssueWithObserversFlow.kt b/testing/cordapps/cashobservers/src/main/kotlin/net/corda/finance/test/flows/CashIssueWithObserversFlow.kt new file mode 100644 index 0000000000..9829f0e4c5 --- /dev/null +++ b/testing/cordapps/cashobservers/src/main/kotlin/net/corda/finance/test/flows/CashIssueWithObserversFlow.kt @@ -0,0 +1,48 @@ +package net.corda.finance.test.flows + +import co.paralleluniverse.fibers.Suspendable +import net.corda.core.contracts.Amount +import net.corda.core.flows.FlowLogic +import net.corda.core.flows.FlowSession +import net.corda.core.flows.InitiatedBy +import net.corda.core.flows.InitiatingFlow +import net.corda.core.flows.ReceiveFinalityFlow +import net.corda.core.flows.StartableByRPC +import net.corda.core.identity.Party +import net.corda.core.transactions.TransactionBuilder +import net.corda.core.utilities.OpaqueBytes +import net.corda.finance.contracts.asset.Cash +import net.corda.finance.flows.AbstractCashFlow +import net.corda.finance.issuedBy +import java.util.Currency + +@StartableByRPC +@InitiatingFlow +class CashIssueWithObserversFlow(private val amount: Amount, + private val issuerBankPartyRef: OpaqueBytes, + private val notary: Party, + private val observers: Set) : AbstractCashFlow(tracker()) { + @Suspendable + override fun call(): Result { + progressTracker.currentStep = Companion.GENERATING_TX + val builder = TransactionBuilder(notary) + val issuer = ourIdentity.ref(issuerBankPartyRef) + val signers = Cash().generateIssue(builder, amount.issuedBy(issuer), ourIdentity, notary) + progressTracker.currentStep = Companion.SIGNING_TX + val tx = serviceHub.signInitialTransaction(builder, signers) + progressTracker.currentStep = Companion.FINALISING_TX + val observerSessions = observers.map { initiateFlow(it) } + val notarised = finaliseTx(tx, observerSessions, "Unable to notarise issue") + return Result(notarised, ourIdentity) + } +} + +@InitiatedBy(CashIssueWithObserversFlow::class) +class CashIssueReceiverFlowWithObservers(private val otherSide: FlowSession) : FlowLogic() { + @Suspendable + override fun call() { + if (!serviceHub.myInfo.isLegalIdentity(otherSide.counterparty)) { + subFlow(ReceiveFinalityFlow(otherSide)) + } + } +} \ No newline at end of file diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/MockTransactionStorage.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/MockTransactionStorage.kt index e0beb1ec00..00593c35ee 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/MockTransactionStorage.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/MockTransactionStorage.kt @@ -55,14 +55,17 @@ open class MockTransactionStorage : WritableTransactionStorage, SingletonSeriali } } - override fun addUnnotarisedTransaction(transaction: SignedTransaction, metadata: FlowTransactionMetadata?): Boolean { - return txns.putIfAbsent(transaction.id, TxHolder(transaction, status = TransactionStatus.MISSING_NOTARY_SIG)) == null + override fun addUnnotarisedTransaction(transaction: SignedTransaction, metadata: FlowTransactionMetadata): Boolean { + return txns.putIfAbsent(transaction.id, TxHolder(transaction, status = TransactionStatus.IN_FLIGHT)) == null } override fun removeUnnotarisedTransaction(id: SecureHash): Boolean { return txns.remove(id) != null } + override fun finalizeTransaction(transaction: SignedTransaction, metadata: FlowTransactionMetadata) = + addTransaction(transaction) + override fun finalizeTransactionWithExtraSignatures(transaction: SignedTransaction, signatures: Collection): Boolean { val current = txns.replace(transaction.id, TxHolder(transaction, status = TransactionStatus.VERIFIED)) return if (current != null) { diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TestDSL.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TestDSL.kt index 0a63b813b1..103e954b5d 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TestDSL.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TestDSL.kt @@ -139,11 +139,13 @@ data class TestTransactionDSLInterpreter private constructor( override val attachmentsClassLoaderCache: AttachmentsClassLoaderCache = AttachmentsClassLoaderCacheImpl(TestingNamedCacheFactory()) - override fun recordUnnotarisedTransaction(txn: SignedTransaction, metadata: FlowTransactionMetadata?) {} + override fun recordUnnotarisedTransaction(txn: SignedTransaction, metadata: FlowTransactionMetadata) {} override fun removeUnnotarisedTransaction(id: SecureHash) {} override fun finalizeTransactionWithExtraSignatures(txn: SignedTransaction, sigs: Collection, statesToRecord: StatesToRecord) {} + + override fun finalizeTransaction(txn: SignedTransaction, statesToRecord: StatesToRecord, metadata: FlowTransactionMetadata) {} } private fun copy(): TestTransactionDSLInterpreter = From a731996844a89c4d3d0a1dcddd05262af2f07642 Mon Sep 17 00:00:00 2001 From: Adel El-Beik Date: Tue, 2 May 2023 11:24:39 +0100 Subject: [PATCH 024/126] ENT-9883: Updated CODEOWNERS file. --- .github/CODEOWNERS | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 3b47f359c9..8a8dff99e1 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -2,10 +2,10 @@ *.md @corda/technical-writers # By default anything under core or node-api is the Kernel team -core @corda/kernel -node-api @corda/kernel -node/src/main/kotlin/net/corda/node/internal @corda/kernel -node/src/main/kotlin/net/corda/node/services @corda/kernel +core @rick-r3 +node-api @rick-r3 +node/src/main/kotlin/net/corda/node/internal @rick-r3 +node/src/main/kotlin/net/corda/node/services @rick-r3 # Determinstic components core-deterministic @chrisr3 @@ -20,20 +20,20 @@ tools/demobench @chrisr3 # General Corda code -core/src/main/kotlin/net/corda/core/flows @dimosr +core/src/main/kotlin/net/corda/core/flows @rick-r3 core/src/main/kotlin/net/corda/core/internal/notary @corda/notaries -node/src/integration-test/kotlin/net/corda/node/persistence @blsemo -node/src/integration-test/kotlin/net/corda/node/services/persistence @blsemo -node/src/main/kotlin/net/corda/node/services/messaging @dimosr -node/src/main/kotlin/net/corda/node/services/persistence @blsemo -node/src/main/kotlin/net/corda/node/services/statemachine @lankydan +node/src/integration-test/kotlin/net/corda/node/persistence @chriscochrane +node/src/integration-test/kotlin/net/corda/node/services/persistence @chriscochrane +node/src/main/kotlin/net/corda/node/services/messaging @rick-r3 +node/src/main/kotlin/net/corda/node/services/persistence @rick-r3 +node/src/main/kotlin/net/corda/node/services/statemachine @rick-r3 node/src/main/kotlin/net/corda/notary @corda/notaries -node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence @blsemo +node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence @rick-r3 -common/logging/src/main/kotlin/net/corda/common/logging/errorReporting @JamesHR3 -common/logging/src/test/kotlin/net/corda/commmon/logging/errorReporting @JamesHR3 +common/logging/src/main/kotlin/net/corda/common/logging/errorReporting @chriscochrane +common/logging/src/test/kotlin/net/corda/commmon/logging/errorReporting @chriscochrane # Single file ownerships go at the end, as they are most specific and take precedence over other ownerships From 9ebcfd3176ff74815065bb9bae6f595324254361 Mon Sep 17 00:00:00 2001 From: Shams Asari Date: Mon, 22 May 2023 10:00:03 +0100 Subject: [PATCH 025/126] Merge fix --- .../kotlin/net/corda/nodeapi/internal/crypto/X509Utilities.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/X509Utilities.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/X509Utilities.kt index 0908e5322b..33036f95ff 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/X509Utilities.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/X509Utilities.kt @@ -23,6 +23,7 @@ import org.bouncycastle.asn1.DERSequence import org.bouncycastle.asn1.DERUTF8String import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.asn1.x500.style.BCStyle +import org.bouncycastle.asn1.x509.AuthorityKeyIdentifier import org.bouncycastle.asn1.x509.BasicConstraints import org.bouncycastle.asn1.x509.CRLDistPoint import org.bouncycastle.asn1.x509.DistributionPoint @@ -33,6 +34,7 @@ import org.bouncycastle.asn1.x509.GeneralNames import org.bouncycastle.asn1.x509.KeyPurposeId import org.bouncycastle.asn1.x509.KeyUsage import org.bouncycastle.asn1.x509.NameConstraints +import org.bouncycastle.asn1.x509.SubjectKeyIdentifier import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo import org.bouncycastle.cert.X509CertificateHolder import org.bouncycastle.cert.X509v3CertificateBuilder From 2e29e36e01ffa84272a81e69f133415012903e08 Mon Sep 17 00:00:00 2001 From: Jose Coll Date: Wed, 24 May 2023 09:42:09 +0100 Subject: [PATCH 026/126] ENT-9923 Ledger Recovery: split out recovery metadata into own database schema. (#7364) --- .../coretests/flows/FinalityFlowTests.kt | 31 +- .../net/corda/core/flows/FinalityFlow.kt | 12 +- .../net/corda/core/flows/FlowTransaction.kt | 38 +- .../core/internal/ServiceHubCoreInternal.kt | 6 +- .../flows/FinalityFlowErrorHandlingTest.kt | 22 +- ...inisticContractWithCustomSerializerTest.kt | 4 +- .../network/PersistentNetworkMapCacheTest.kt | 2 +- .../network/PersistentPartyInfoCacheTest.kt | 95 +++++ .../net/corda/node/internal/AbstractNode.kt | 15 +- .../node/services/api/ServiceHubInternal.kt | 16 +- .../network/PersistentPartyInfoCache.kt | 80 +++++ .../persistence/DBTransactionStorage.kt | 93 +---- .../DBTransactionStorageLedgerRecovery.kt | 330 ++++++++++++++++++ .../node/services/schema/NodeSchemaService.kt | 6 +- .../corda/node/utilities/NodeNamedCache.kt | 4 + .../migration/node-core.changelog-master.xml | 1 + .../migration/node-core.changelog-v25.xml | 112 ++++++ .../node/messaging/TwoPartyTradeFlowTests.kt | 10 +- ...DBTransactionStorageLedgerRecoveryTests.kt | 305 ++++++++++++++++ .../persistence/DBTransactionStorageTests.kt | 47 +-- .../node/internal/MockTransactionStorage.kt | 6 +- .../kotlin/net/corda/testing/dsl/TestDSL.kt | 6 +- 22 files changed, 1077 insertions(+), 164 deletions(-) create mode 100644 node/src/integration-test/kotlin/net/corda/node/services/network/PersistentPartyInfoCacheTest.kt create mode 100644 node/src/main/kotlin/net/corda/node/services/network/PersistentPartyInfoCache.kt create mode 100644 node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecovery.kt create mode 100644 node/src/main/resources/migration/node-core.changelog-v25.xml create mode 100644 node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecoveryTests.kt diff --git a/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt b/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt index 91c5cd3f15..77a7731d39 100644 --- a/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt +++ b/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt @@ -14,7 +14,6 @@ import net.corda.core.flows.FinalityFlow import net.corda.core.flows.FlowException import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowSession -import net.corda.core.flows.FlowTransactionMetadata import net.corda.core.flows.InitiatedBy import net.corda.core.flows.InitiatingFlow import net.corda.core.flows.NotaryError @@ -24,6 +23,7 @@ import net.corda.core.flows.ReceiveFinalityFlow import net.corda.core.flows.ReceiveTransactionFlow import net.corda.core.flows.SendTransactionFlow import net.corda.core.flows.StartableByRPC +import net.corda.core.flows.TransactionMetadata import net.corda.core.flows.TransactionStatus import net.corda.core.flows.UnexpectedFlowEndException import net.corda.core.identity.Party @@ -48,6 +48,9 @@ import net.corda.finance.flows.CashPaymentFlow import net.corda.finance.issuedBy import net.corda.finance.test.flows.CashIssueWithObserversFlow import net.corda.node.services.persistence.DBTransactionStorage +import net.corda.node.services.persistence.DBTransactionStorageLedgerRecovery +import net.corda.node.services.persistence.DistributionRecord +import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.testing.contracts.DummyContract import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.BOB_NAME @@ -61,6 +64,7 @@ import net.corda.testing.node.internal.FINANCE_WORKFLOWS_CORDAPP import net.corda.testing.node.internal.InternalMockNetwork import net.corda.testing.node.internal.InternalMockNodeParameters import net.corda.testing.node.internal.MOCK_VERSION_INFO +import net.corda.testing.node.internal.MockCryptoService import net.corda.testing.node.internal.TestCordappInternal import net.corda.testing.node.internal.TestStartedNode import net.corda.testing.node.internal.cordappWithPackages @@ -345,6 +349,29 @@ class FinalityFlowTests : WithFinality { assertThat(aliceNode.services.validatedTransactions.getTransaction(stx.id)).isNotNull assertThat(bobNode.services.validatedTransactions.getTransaction(stx.id)).isNotNull + + assertThat(getSenderRecoveryData(stx.id, aliceNode.database)).isNotNull + assertThat(getReceiverRecoveryData(stx.id, bobNode.database)).isNotNull + } + + private fun getSenderRecoveryData(id: SecureHash, database: CordaPersistence): DistributionRecord? { + val fromDb = database.transaction { + session.createQuery( + "from ${DBTransactionStorageLedgerRecovery.DBSenderDistributionRecord::class.java.name} where tx_id = :transactionId", + DBTransactionStorageLedgerRecovery.DBSenderDistributionRecord::class.java + ).setParameter("transactionId", id.toString()).resultList.map { it } + } + return fromDb.singleOrNull()?.toSenderDistributionRecord() + } + + private fun getReceiverRecoveryData(id: SecureHash, database: CordaPersistence): DistributionRecord? { + val fromDb = database.transaction { + session.createQuery( + "from ${DBTransactionStorageLedgerRecovery.DBReceiverDistributionRecord::class.java.name} where tx_id = :transactionId", + DBTransactionStorageLedgerRecovery.DBReceiverDistributionRecord::class.java + ).setParameter("transactionId", id.toString()).resultList.map { it } + } + return fromDb.singleOrNull()?.toReceiverDistributionRecord(MockCryptoService(emptyMap())) } @StartableByRPC @@ -423,7 +450,7 @@ class FinalityFlowTests : WithFinality { require(NotarySigCheck.needsNotarySignature(stx)) logger.info("Peer recording transaction without notary signature.") (serviceHub as ServiceHubCoreInternal).recordUnnotarisedTransaction(stx, - FlowTransactionMetadata(otherSideSession.counterparty.name, StatesToRecord.ONLY_RELEVANT)) + TransactionMetadata(otherSideSession.counterparty.name, StatesToRecord.ONLY_RELEVANT)) otherSideSession.send(FetchDataFlow.Request.End) // Finish fetching data (overrideAutoAck) logger.info("Peer recorded transaction without notary signature.") diff --git a/core/src/main/kotlin/net/corda/core/flows/FinalityFlow.kt b/core/src/main/kotlin/net/corda/core/flows/FinalityFlow.kt index 57639dcd22..9219edccd4 100644 --- a/core/src/main/kotlin/net/corda/core/flows/FinalityFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/FinalityFlow.kt @@ -227,7 +227,7 @@ class FinalityFlow private constructor(val transaction: SignedTransaction, else { if (newPlatformSessions.isNotEmpty()) finaliseLocallyAndBroadcast(newPlatformSessions, transaction, - FlowTransactionMetadata( + TransactionMetadata( serviceHub.myInfo.legalIdentities.first().name, statesToRecord, sessions.map { it.counterparty.name }.toSet())) @@ -258,7 +258,7 @@ class FinalityFlow private constructor(val transaction: SignedTransaction, } @Suspendable - private fun finaliseLocallyAndBroadcast(sessions: Collection, tx: SignedTransaction, metadata: FlowTransactionMetadata) { + private fun finaliseLocallyAndBroadcast(sessions: Collection, tx: SignedTransaction, metadata: TransactionMetadata) { serviceHub.telemetryServiceInternal.span("${this::class.java.name}#finaliseLocallyAndBroadcast", flowLogic = this) { finaliseLocally(tx, metadata = metadata) progressTracker.currentStep = BROADCASTING @@ -310,7 +310,7 @@ class FinalityFlow private constructor(val transaction: SignedTransaction, @Suspendable private fun finaliseLocally(stx: SignedTransaction, notarySignatures: List = emptyList(), - metadata: FlowTransactionMetadata? = null) { + metadata: TransactionMetadata? = null) { progressTracker.currentStep = FINALISING_TRANSACTION serviceHub.telemetryServiceInternal.span("${this::class.java.name}#finaliseLocally", flowLogic = this) { if (notarySignatures.isEmpty()) { @@ -405,7 +405,7 @@ class FinalityFlow private constructor(val transaction: SignedTransaction, progressTracker.currentStep = RECORD_UNNOTARISED serviceHub.telemetryServiceInternal.span("${this::class.java.name}#recordUnnotarisedTransaction", flowLogic = this) { (serviceHub as ServiceHubCoreInternal).recordUnnotarisedTransaction(tx, - FlowTransactionMetadata( + TransactionMetadata( serviceHub.myInfo.legalIdentities.first().name, statesToRecord, sessions.map { it.counterparty.name }.toSet())) @@ -496,7 +496,7 @@ class ReceiveFinalityFlow @JvmOverloads constructor(private val otherSideSession serviceHub.telemetryServiceInternal.span("${this::class.java.name}#recordUnnotarisedTransaction", flowLogic = this) { logger.debug { "Peer recording transaction without notary signature." } (serviceHub as ServiceHubCoreInternal).recordUnnotarisedTransaction(stx, - FlowTransactionMetadata(otherSideSession.counterparty.name, statesToRecord)) + TransactionMetadata(otherSideSession.counterparty.name, statesToRecord)) } otherSideSession.send(FetchDataFlow.Request.End) // Finish fetching data (deferredAck) logger.info("Peer recorded transaction without notary signature. Waiting to receive notary signature.") @@ -523,7 +523,7 @@ class ReceiveFinalityFlow @JvmOverloads constructor(private val otherSideSession } else { serviceHub.telemetryServiceInternal.span("${this::class.java.name}#finalizeTransaction", flowLogic = this) { (serviceHub as ServiceHubCoreInternal).finalizeTransaction(stx, statesToRecord, - FlowTransactionMetadata(otherSideSession.counterparty.name, statesToRecord)) + TransactionMetadata(otherSideSession.counterparty.name, statesToRecord)) logger.info("Peer recorded transaction with recovery metadata.") } otherSideSession.send(FetchDataFlow.Request.End) // Finish fetching data (deferredAck) diff --git a/core/src/main/kotlin/net/corda/core/flows/FlowTransaction.kt b/core/src/main/kotlin/net/corda/core/flows/FlowTransaction.kt index f66d6990ea..8f0c4a901a 100644 --- a/core/src/main/kotlin/net/corda/core/flows/FlowTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/flows/FlowTransaction.kt @@ -10,20 +10,19 @@ import java.time.Instant */ @CordaSerializable -data class FlowTransaction( +data class FlowTransactionInfo( val stateMachineRunId: StateMachineRunId, val txId: String, val status: TransactionStatus, - val signatures: ByteArray?, val timestamp: Instant, - val metadata: FlowTransactionMetadata?) { - + val metadata: TransactionMetadata? +) { fun isInitiator(myCordaX500Name: CordaX500Name) = - this.metadata?.initiator == myCordaX500Name + this.metadata?.initiator == myCordaX500Name } @CordaSerializable -data class FlowTransactionMetadata( +data class TransactionMetadata( val initiator: CordaX500Name, val statesToRecord: StatesToRecord? = StatesToRecord.ONLY_RELEVANT, val peers: Set? = null @@ -34,4 +33,31 @@ enum class TransactionStatus { UNVERIFIED, VERIFIED, IN_FLIGHT; +} + +@CordaSerializable +data class RecoveryTimeWindow(val fromTime: Instant, val untilTime: Instant = Instant.now()) { + + init { + if (untilTime < fromTime) { + throw IllegalArgumentException("$fromTime must be before $untilTime") + } + } + + companion object { + @JvmStatic + fun between(fromTime: Instant, untilTime: Instant): RecoveryTimeWindow { + return RecoveryTimeWindow(fromTime, untilTime) + } + + @JvmStatic + fun fromOnly(fromTime: Instant): RecoveryTimeWindow { + return RecoveryTimeWindow(fromTime = fromTime) + } + + @JvmStatic + fun untilOnly(untilTime: Instant): RecoveryTimeWindow { + return RecoveryTimeWindow(fromTime = Instant.EPOCH, untilTime = untilTime) + } + } } \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/internal/ServiceHubCoreInternal.kt b/core/src/main/kotlin/net/corda/core/internal/ServiceHubCoreInternal.kt index 5e63caebec..264353f932 100644 --- a/core/src/main/kotlin/net/corda/core/internal/ServiceHubCoreInternal.kt +++ b/core/src/main/kotlin/net/corda/core/internal/ServiceHubCoreInternal.kt @@ -4,7 +4,7 @@ import co.paralleluniverse.fibers.Suspendable import net.corda.core.DeleteForDJVM import net.corda.core.crypto.SecureHash import net.corda.core.crypto.TransactionSignature -import net.corda.core.flows.FlowTransactionMetadata +import net.corda.core.flows.TransactionMetadata import net.corda.core.internal.notary.NotaryService import net.corda.core.node.ServiceHub import net.corda.core.node.StatesToRecord @@ -37,7 +37,7 @@ interface ServiceHubCoreInternal : ServiceHub { * @param txn The transaction to record. * @param metadata Finality flow recovery metadata. */ - fun recordUnnotarisedTransaction(txn: SignedTransaction, metadata: FlowTransactionMetadata) + fun recordUnnotarisedTransaction(txn: SignedTransaction, metadata: TransactionMetadata) /** * Removes transaction from data store. @@ -63,7 +63,7 @@ interface ServiceHubCoreInternal : ServiceHub { * @param statesToRecord how the vault should treat the output states of the transaction. * @param metadata Finality flow recovery metadata. */ - fun finalizeTransaction(txn: SignedTransaction, statesToRecord: StatesToRecord, metadata: FlowTransactionMetadata) + fun finalizeTransaction(txn: SignedTransaction, statesToRecord: StatesToRecord, metadata: TransactionMetadata) } interface TransactionsResolver { diff --git a/node/src/integration-test-slow/kotlin/net/corda/node/flows/FinalityFlowErrorHandlingTest.kt b/node/src/integration-test-slow/kotlin/net/corda/node/flows/FinalityFlowErrorHandlingTest.kt index bb73f7da04..cbc4b43c94 100644 --- a/node/src/integration-test-slow/kotlin/net/corda/node/flows/FinalityFlowErrorHandlingTest.kt +++ b/node/src/integration-test-slow/kotlin/net/corda/node/flows/FinalityFlowErrorHandlingTest.kt @@ -74,8 +74,7 @@ class FinalityFlowErrorHandlingTest : StateMachineErrorHandlingTest() { alice.rpc.startFlow(::GetFlowTransaction, txId).returnValue.getOrThrow().apply { assertEquals("V", this.first) // "V" -> VERIFIED - assertEquals(ALICE_NAME.toString(), this.second) // initiator - assertEquals(CHARLIE_NAME.toString(), this.third) // peers + assertEquals(CHARLIE_NAME.hashCode().toLong(), this.second) // peer } } } @@ -84,18 +83,25 @@ class FinalityFlowErrorHandlingTest : StateMachineErrorHandlingTest() { // Internal use for testing only!! @StartableByRPC -class GetFlowTransaction(private val txId: SecureHash) : FlowLogic>() { +class GetFlowTransaction(private val txId: SecureHash) : FlowLogic>() { @Suspendable - override fun call(): Triple { - return serviceHub.jdbcSession().prepareStatement("select * from node_transactions where tx_id = ?") + override fun call(): Pair { + val transactionStatus = serviceHub.jdbcSession().prepareStatement("select * from node_transactions where tx_id = ?") .apply { setString(1, txId.toString()) } .use { ps -> ps.executeQuery().use { rs -> rs.next() - Triple(rs.getString(4), // TransactionStatus - rs.getString(7), // initiator - rs.getString(8)) // participants + rs.getString(4) // TransactionStatus } } + val receiverPartyId = serviceHub.jdbcSession().prepareStatement("select * from node_sender_distribution_records where tx_id = ?") + .apply { setString(1, txId.toString()) } + .use { ps -> + ps.executeQuery().use { rs -> + rs.next() + rs.getLong(2) // receiverPartyId + } + } + return Pair(transactionStatus, receiverPartyId) } } \ No newline at end of file diff --git a/node/src/integration-test/kotlin/net/corda/node/services/DeterministicContractWithCustomSerializerTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/DeterministicContractWithCustomSerializerTest.kt index 447cd2d6a6..7aef99a62f 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/DeterministicContractWithCustomSerializerTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/DeterministicContractWithCustomSerializerTest.kt @@ -20,6 +20,7 @@ import net.corda.testing.node.internal.cordappWithPackages import org.assertj.core.api.Assertions.assertThat import org.junit.BeforeClass import org.junit.ClassRule +import org.junit.Ignore import org.junit.Test import org.junit.jupiter.api.assertDoesNotThrow import org.junit.jupiter.api.assertThrows @@ -60,7 +61,8 @@ class DeterministicContractWithCustomSerializerTest { } @Test(timeout=300_000) - fun `test DJVM can verify using custom serializer`() { + @Ignore("Flaky test in CI: org.opentest4j.AssertionFailedError: Unexpected exception thrown: net.corda.client.rpc.RPCException: Class \"class net.corda.contracts.serialization.custom.Currantsy\" is not on the whitelist or annotated with @CordaSerializable.") + fun `test DJVM can verify using custom serializer`() { driver(parametersFor(djvmSources, listOf(flowCordapp, contractCordapp))) { val alice = startNode(providedName = ALICE_NAME).getOrThrow() val txId = assertDoesNotThrow { diff --git a/node/src/integration-test/kotlin/net/corda/node/services/network/PersistentNetworkMapCacheTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/network/PersistentNetworkMapCacheTest.kt index bf8d2910b6..d469788526 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/network/PersistentNetworkMapCacheTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/network/PersistentNetworkMapCacheTest.kt @@ -24,7 +24,7 @@ import org.junit.Rule import org.junit.Test class PersistentNetworkMapCacheTest { - private companion object { + internal companion object { val ALICE = TestIdentity(ALICE_NAME, 70) val BOB = TestIdentity(BOB_NAME, 80) val CHARLIE = TestIdentity(CHARLIE_NAME, 90) diff --git a/node/src/integration-test/kotlin/net/corda/node/services/network/PersistentPartyInfoCacheTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/network/PersistentPartyInfoCacheTest.kt new file mode 100644 index 0000000000..42931cebc0 --- /dev/null +++ b/node/src/integration-test/kotlin/net/corda/node/services/network/PersistentPartyInfoCacheTest.kt @@ -0,0 +1,95 @@ +package net.corda.node.services.network + +import net.corda.core.node.NodeInfo +import net.corda.core.utilities.NetworkHostAndPort +import net.corda.node.services.identity.InMemoryIdentityService +import net.corda.node.services.network.PersistentNetworkMapCacheTest.Companion.ALICE +import net.corda.node.services.network.PersistentNetworkMapCacheTest.Companion.BOB +import net.corda.node.services.network.PersistentNetworkMapCacheTest.Companion.CHARLIE +import net.corda.nodeapi.internal.DEV_ROOT_CA +import net.corda.nodeapi.internal.persistence.DatabaseConfig +import net.corda.testing.core.SerializationEnvironmentRule +import net.corda.testing.core.TestIdentity +import net.corda.testing.internal.TestingNamedCacheFactory +import net.corda.testing.internal.configureDatabase +import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties +import org.assertj.core.api.Assertions.assertThat +import org.junit.Rule +import org.junit.Test + +class PersistentPartyInfoCacheTest { + + @Rule + @JvmField + val testSerialization = SerializationEnvironmentRule() + + private var portCounter = 1000 + private val database = configureDatabase(makeTestDataSourceProperties(), DatabaseConfig(), { null }, { null }) + private val charlieNetMapCache = PersistentNetworkMapCache(TestingNamedCacheFactory(), database, InMemoryIdentityService(trustRoot = DEV_ROOT_CA.certificate)) + + @Test(timeout=300_000) + fun `get party id from CordaX500Name sourced from NetworkMapCache`() { + charlieNetMapCache.addOrUpdateNodes(listOf( + createNodeInfo(listOf(ALICE)), + createNodeInfo(listOf(BOB)), + createNodeInfo(listOf(CHARLIE)))) + val partyInfoCache = PersistentPartyInfoCache(charlieNetMapCache, TestingNamedCacheFactory(), database) + partyInfoCache.start() + assertThat(partyInfoCache.getPartyIdByCordaX500Name(ALICE.name)).isEqualTo(ALICE.name.hashCode().toLong()) + assertThat(partyInfoCache.getPartyIdByCordaX500Name(BOB.name)).isEqualTo(BOB.name.hashCode().toLong()) + assertThat(partyInfoCache.getPartyIdByCordaX500Name(CHARLIE.name)).isEqualTo(CHARLIE.name.hashCode().toLong()) + } + + @Test(timeout=300_000) + fun `get party id from CordaX500Name sourced from backing database`() { + charlieNetMapCache.addOrUpdateNodes(listOf( + createNodeInfo(listOf(ALICE)), + createNodeInfo(listOf(BOB)), + createNodeInfo(listOf(CHARLIE)))) + PersistentPartyInfoCache(charlieNetMapCache, TestingNamedCacheFactory(), database).start() + // clear network map cache & bootstrap another PersistentInfoCache + charlieNetMapCache.clearNetworkMapCache() + val partyInfoCache = PersistentPartyInfoCache(charlieNetMapCache, TestingNamedCacheFactory(), database) + assertThat(partyInfoCache.getPartyIdByCordaX500Name(ALICE.name)).isEqualTo(ALICE.name.hashCode().toLong()) + assertThat(partyInfoCache.getPartyIdByCordaX500Name(BOB.name)).isEqualTo(BOB.name.hashCode().toLong()) + assertThat(partyInfoCache.getPartyIdByCordaX500Name(CHARLIE.name)).isEqualTo(CHARLIE.name.hashCode().toLong()) + } + + @Test(timeout=300_000) + fun `get party CordaX500Name from id sourced from NetworkMapCache`() { + charlieNetMapCache.addOrUpdateNodes(listOf( + createNodeInfo(listOf(ALICE)), + createNodeInfo(listOf(BOB)), + createNodeInfo(listOf(CHARLIE)))) + val partyInfoCache = PersistentPartyInfoCache(charlieNetMapCache, TestingNamedCacheFactory(), database) + partyInfoCache.start() + assertThat(partyInfoCache.getCordaX500NameByPartyId(ALICE.name.hashCode().toLong())).isEqualTo(ALICE.name) + assertThat(partyInfoCache.getCordaX500NameByPartyId(BOB.name.hashCode().toLong())).isEqualTo(BOB.name) + assertThat(partyInfoCache.getCordaX500NameByPartyId(CHARLIE.name.hashCode().toLong())).isEqualTo(CHARLIE.name) + } + + @Test(timeout=300_000) + fun `get party CordaX500Name from id sourced from backing database`() { + charlieNetMapCache.addOrUpdateNodes(listOf( + createNodeInfo(listOf(ALICE)), + createNodeInfo(listOf(BOB)), + createNodeInfo(listOf(CHARLIE)))) + PersistentPartyInfoCache(charlieNetMapCache, TestingNamedCacheFactory(), database).start() + // clear network map cache & bootstrap another PersistentInfoCache + charlieNetMapCache.clearNetworkMapCache() + val partyInfoCache = PersistentPartyInfoCache(charlieNetMapCache, TestingNamedCacheFactory(), database) + assertThat(partyInfoCache.getCordaX500NameByPartyId(ALICE.name.hashCode().toLong())).isEqualTo(ALICE.name) + assertThat(partyInfoCache.getCordaX500NameByPartyId(BOB.name.hashCode().toLong())).isEqualTo(BOB.name) + assertThat(partyInfoCache.getCordaX500NameByPartyId(CHARLIE.name.hashCode().toLong())).isEqualTo(CHARLIE.name) + } + + private fun createNodeInfo(identities: List, + address: NetworkHostAndPort = NetworkHostAndPort("localhost", portCounter++)): NodeInfo { + return NodeInfo( + addresses = listOf(address), + legalIdentitiesAndCerts = identities.map { it.identity }, + platformVersion = 3, + serial = 1 + ) + } +} diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index 57c6c7906f..eb781dd3f7 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -38,6 +38,7 @@ import net.corda.core.internal.VisibleForTesting import net.corda.core.internal.concurrent.flatMap import net.corda.core.internal.concurrent.map import net.corda.core.internal.concurrent.openFuture +import net.corda.core.internal.concurrent.thenMatch import net.corda.core.internal.div import net.corda.core.internal.messaging.AttachmentTrustInfoRPCOps import net.corda.core.internal.notary.NotaryService @@ -121,13 +122,14 @@ import net.corda.node.services.network.NetworkParameterUpdateListener import net.corda.node.services.network.NetworkParametersHotloader import net.corda.node.services.network.NodeInfoWatcher import net.corda.node.services.network.PersistentNetworkMapCache +import net.corda.node.services.network.PersistentPartyInfoCache import net.corda.node.services.persistence.AbstractPartyDescriptor import net.corda.node.services.persistence.AbstractPartyToX500NameAsStringConverter import net.corda.node.services.persistence.AttachmentStorageInternal import net.corda.node.services.persistence.DBCheckpointPerformanceRecorder import net.corda.node.services.persistence.DBCheckpointStorage import net.corda.node.services.persistence.DBTransactionMappingStorage -import net.corda.node.services.persistence.DBTransactionStorage +import net.corda.node.services.persistence.DBTransactionStorageLedgerRecovery import net.corda.node.services.persistence.NodeAttachmentService import net.corda.node.services.persistence.NodePropertiesPersistentStore import net.corda.node.services.persistence.PublicKeyToOwningIdentityCacheImpl @@ -285,6 +287,9 @@ abstract class AbstractNode(val configuration: NodeConfiguration, } val networkMapCache = PersistentNetworkMapCache(cacheFactory, database, identityService).tokenize() + val partyInfoCache = PersistentPartyInfoCache(networkMapCache, cacheFactory, database) + @Suppress("LeakingThis") + val cryptoService = makeCryptoService() @Suppress("LeakingThis") val transactionStorage = makeTransactionStorage(configuration.transactionCacheSizeBytes).tokenize() val networkMapClient: NetworkMapClient? = configuration.networkServices?.let { NetworkMapClient(it.networkMapURL, versionInfo) } @@ -296,8 +301,6 @@ abstract class AbstractNode(val configuration: NodeConfiguration, ).tokenize() val attachmentTrustCalculator = makeAttachmentTrustCalculator(configuration, database) @Suppress("LeakingThis") - val cryptoService = makeCryptoService() - @Suppress("LeakingThis") val networkParametersStorage = makeNetworkParametersStorage() val cordappProvider = CordappProviderImpl(cordappLoader, CordappConfigFileProvider(configuration.cordappDirectories), attachments).tokenize() val diagnosticsService = NodeDiagnosticsService().tokenize() @@ -694,6 +697,10 @@ abstract class AbstractNode(val configuration: NodeConfiguration, log.warn("Not distributing events as NetworkMap is not ready") } } + nodeReadyFuture.thenMatch({ + partyInfoCache.start() + }, { th -> log.error("Unexpected exception during cache initialisation", th) }) + setNodeStatus(NodeStatus.STARTED) return resultingNodeInfo } @@ -1077,7 +1084,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, } protected open fun makeTransactionStorage(transactionCacheSizeBytes: Long): WritableTransactionStorage { - return DBTransactionStorage(database, cacheFactory, platformClock) + return DBTransactionStorageLedgerRecovery(database, cacheFactory, platformClock, cryptoService, partyInfoCache) } protected open fun makeNetworkParametersStorage(): NetworkParametersStorage { diff --git a/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt b/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt index 60df01f809..4d2e1e7b44 100644 --- a/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt +++ b/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt @@ -5,7 +5,7 @@ import net.corda.core.context.InvocationContext import net.corda.core.crypto.SecureHash import net.corda.core.crypto.TransactionSignature import net.corda.core.flows.FlowLogic -import net.corda.core.flows.FlowTransactionMetadata +import net.corda.core.flows.TransactionMetadata import net.corda.core.flows.StateMachineRunId import net.corda.core.flows.TransactionStatus import net.corda.core.internal.FlowStateMachineHandle @@ -240,25 +240,27 @@ interface ServiceHubInternal : ServiceHubCoreInternal { ) } - override fun finalizeTransaction(txn: SignedTransaction, statesToRecord: StatesToRecord, metadata: FlowTransactionMetadata) { + override fun finalizeTransaction(txn: SignedTransaction, statesToRecord: StatesToRecord, metadata: TransactionMetadata) { requireSupportedHashType(txn) if (txn.coreTransaction is WireTransaction) txn.verifyRequiredSignatures() database.transaction { recordTransactions(statesToRecord, listOf(txn), validatedTransactions, stateMachineRecordedTransactionMapping, vaultService, database) { - validatedTransactions.finalizeTransaction(txn, metadata) + val isInitiator = metadata.initiator == myInfo.legalIdentities.first().name + validatedTransactions.finalizeTransaction(txn, metadata, isInitiator) } } } - override fun recordUnnotarisedTransaction(txn: SignedTransaction, metadata: FlowTransactionMetadata) { + override fun recordUnnotarisedTransaction(txn: SignedTransaction, metadata: TransactionMetadata) { if (txn.coreTransaction is WireTransaction) { txn.notary?.let { notary -> txn.verifySignaturesExcept(notary.owningKey) } ?: txn.verifyRequiredSignatures() } database.transaction { - validatedTransactions.addUnnotarisedTransaction(txn, metadata) + val isInitiator = metadata.initiator == myInfo.legalIdentities.first().name + validatedTransactions.addUnnotarisedTransaction(txn, metadata, isInitiator) } } @@ -361,7 +363,7 @@ interface WritableTransactionStorage : TransactionStorage { * @param metadata Finality flow recovery metadata. * @return true if the transaction was recorded as a *new* transaction, false if the transaction already exists. */ - fun addUnnotarisedTransaction(transaction: SignedTransaction, metadata: FlowTransactionMetadata): Boolean + fun addUnnotarisedTransaction(transaction: SignedTransaction, metadata: TransactionMetadata, isInitiator: Boolean): Boolean /** * Removes an un-notarised transaction (with a status of *MISSING_TRANSACTION_SIG*) from the data store. @@ -376,7 +378,7 @@ interface WritableTransactionStorage : TransactionStorage { * @param metadata Finality flow recovery metadata. * @return true if the transaction was recorded as a *new* transaction, false if the transaction already exists. */ - fun finalizeTransaction(transaction: SignedTransaction, metadata: FlowTransactionMetadata): Boolean + fun finalizeTransaction(transaction: SignedTransaction, metadata: TransactionMetadata, isInitiator: Boolean): Boolean /** * Update a previously un-notarised transaction including associated notary signatures. diff --git a/node/src/main/kotlin/net/corda/node/services/network/PersistentPartyInfoCache.kt b/node/src/main/kotlin/net/corda/node/services/network/PersistentPartyInfoCache.kt new file mode 100644 index 0000000000..bb3c9aed9f --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/services/network/PersistentPartyInfoCache.kt @@ -0,0 +1,80 @@ +package net.corda.node.services.network + +import net.corda.core.identity.CordaX500Name +import net.corda.core.internal.NamedCacheFactory +import net.corda.core.node.services.NetworkMapCache +import net.corda.node.services.persistence.DBTransactionStorageLedgerRecovery +import net.corda.node.utilities.NonInvalidatingCache +import net.corda.nodeapi.internal.persistence.CordaPersistence +import org.hibernate.Session +import rx.Observable + +class PersistentPartyInfoCache(private val networkMapCache: PersistentNetworkMapCache, + cacheFactory: NamedCacheFactory, + private val database: CordaPersistence) { + + // probably better off using a BiMap here: https://www.baeldung.com/guava-bimap + private val cordaX500NameToPartyIdCache = NonInvalidatingCache( + cacheFactory = cacheFactory, + name = "RecoveryPartyInfoCache_byCordaX500Name") { key -> + database.transaction { queryByCordaX500Name(session, key) } + } + + private val partyIdToCordaX500NameCache = NonInvalidatingCache( + cacheFactory = cacheFactory, + name = "RecoveryPartyInfoCache_byPartyId") { key -> + database.transaction { queryByPartyId(session, key) } + } + + private lateinit var trackNetworkMapUpdates: Observable + + fun start() { + val (snapshot, updates) = networkMapCache.track() + snapshot.map { entry -> + entry.legalIdentities.map { party -> + add(party.name.hashCode().toLong(), party.name) + } + } + trackNetworkMapUpdates = updates + trackNetworkMapUpdates.cache().forEach { nodeInfo -> + nodeInfo.node.legalIdentities.map { party -> + add(party.name.hashCode().toLong(), party.name) + } + } + } + + fun getPartyIdByCordaX500Name(name: CordaX500Name): Long = cordaX500NameToPartyIdCache[name] ?: throw IllegalStateException("Missing cache entry for $name") + + fun getCordaX500NameByPartyId(partyId: Long): CordaX500Name = partyIdToCordaX500NameCache[partyId] ?: throw IllegalStateException("Missing cache entry for $partyId") + + private fun add(partyHashCode: Long, partyName: CordaX500Name) { + partyIdToCordaX500NameCache.cache.put(partyHashCode, partyName) + cordaX500NameToPartyIdCache.cache.put(partyName, partyHashCode) + updateInfoDB(partyHashCode, partyName) + } + + private fun updateInfoDB(partyHashCode: Long, partyName: CordaX500Name) { + database.transaction { + if (queryByPartyId(session, partyHashCode) == null) { + println("PartyInfo: $partyHashCode -> $partyName") + session.save(DBTransactionStorageLedgerRecovery.DBRecoveryPartyInfo(partyHashCode, partyName.toString())) + } + } + } + + private fun queryByCordaX500Name(session: Session, key: CordaX500Name): Long? { + val query = session.createQuery( + "FROM ${DBTransactionStorageLedgerRecovery.DBRecoveryPartyInfo::class.java.name} WHERE partyName = :partyName", + DBTransactionStorageLedgerRecovery.DBRecoveryPartyInfo::class.java) + query.setParameter("partyName", key.toString()) + return query.resultList.singleOrNull()?.partyId + } + + private fun queryByPartyId(session: Session, key: Long): CordaX500Name? { + val query = session.createQuery( + "FROM ${DBTransactionStorageLedgerRecovery.DBRecoveryPartyInfo::class.java.name} WHERE partyId = :partyId", + DBTransactionStorageLedgerRecovery.DBRecoveryPartyInfo::class.java) + query.setParameter("partyId", key) + return query.resultList.singleOrNull()?.partyName?.let { CordaX500Name.parse(it) } + } +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt index dd69c26fc3..b78e2adb35 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt @@ -3,15 +3,13 @@ package net.corda.node.services.persistence import net.corda.core.concurrent.CordaFuture import net.corda.core.crypto.SecureHash import net.corda.core.crypto.TransactionSignature -import net.corda.core.flows.FlowTransactionMetadata -import net.corda.core.identity.CordaX500Name +import net.corda.core.flows.TransactionMetadata import net.corda.core.internal.NamedCacheFactory import net.corda.core.internal.ThreadBox import net.corda.core.internal.VisibleForTesting import net.corda.core.internal.bufferUntilSubscribed import net.corda.core.internal.concurrent.doneFuture import net.corda.core.messaging.DataFeed -import net.corda.core.node.StatesToRecord import net.corda.core.serialization.SerializationContext import net.corda.core.serialization.SerializationDefaults import net.corda.core.serialization.SerializedBytes @@ -52,8 +50,8 @@ import javax.persistence.Table import kotlin.streams.toList @Suppress("TooManyFunctions") -class DBTransactionStorage(private val database: CordaPersistence, cacheFactory: NamedCacheFactory, - private val clock: CordaClock) : WritableTransactionStorage, SingletonSerializeAsToken() { +open class DBTransactionStorage(private val database: CordaPersistence, cacheFactory: NamedCacheFactory, + private val clock: CordaClock) : WritableTransactionStorage, SingletonSerializeAsToken() { @Suppress("MagicNumber") // database column width @Entity @@ -78,26 +76,7 @@ class DBTransactionStorage(private val database: CordaPersistence, cacheFactory: val timestamp: Instant, @Column(name = "signatures") - val signatures: ByteArray?, - - /** - * Flow finality metadata used for recovery - * TODO: create association table solely for Flow metadata and recovery purposes. - * See https://r3-cev.atlassian.net/browse/ENT-9521 - */ - - /** X500Name of flow initiator **/ - @Column(name = "initiator") - val initiator: String? = null, - - /** X500Name of flow participant parties **/ - @Column(name = "participants") - @Convert(converter = StringListConverter::class) - val participants: List? = null, - - /** states to record: NONE, ALL_VISIBLE, ONLY_RELEVANT */ - @Column(name = "states_to_record") - val statesToRecord: StatesToRecord? = null + val signatures: ByteArray? ) enum class TransactionStatus { @@ -150,21 +129,6 @@ class DBTransactionStorage(private val database: CordaPersistence, cacheFactory: } } - @Converter - class StringListConverter : AttributeConverter?, String?> { - override fun convertToDatabaseColumn(stringList: List?): String? { - return stringList?.let { if (it.isEmpty()) null else it.joinToString(SPLIT_CHAR) } - } - - override fun convertToEntityAttribute(string: String?): List? { - return string?.split(SPLIT_CHAR) - } - - companion object { - private const val SPLIT_CHAR = ";" - } - } - internal companion object { const val TRANSACTION_ALREADY_IN_PROGRESS_WARNING = "trackTransaction is called with an already existing, open DB transaction. As a result, there might be transactions missing from the returned data feed, because of race conditions." @@ -187,7 +151,7 @@ class DBTransactionStorage(private val database: CordaPersistence, cacheFactory: private fun createTransactionsMap(cacheFactory: NamedCacheFactory, clock: CordaClock) : AppendOnlyPersistentMapBase { - return WeightBasedAppendOnlyPersistentMap( + return WeightBasedAppendOnlyPersistentMap( cacheFactory = cacheFactory, name = "DBTransactionStorage_transactions", toPersistentEntityKey = SecureHash::toString, @@ -195,14 +159,7 @@ class DBTransactionStorage(private val database: CordaPersistence, cacheFactory: SecureHash.create(dbTxn.txId) to TxCacheValue( dbTxn.transaction.deserialize(context = contextToUse()), dbTxn.status, - dbTxn.signatures?.deserialize(context = contextToUse()), - dbTxn.initiator?.let { initiator -> - FlowTransactionMetadata( - CordaX500Name.parse(initiator), - dbTxn.statesToRecord!!, - dbTxn.participants?.let { it.map { CordaX500Name.parse(it) }.toSet() } - ) - } + dbTxn.signatures?.deserialize(context = contextToUse()) ) }, toPersistentEntity = { key: SecureHash, value: TxCacheValue -> @@ -212,10 +169,7 @@ class DBTransactionStorage(private val database: CordaPersistence, cacheFactory: transaction = value.toSignedTx().serialize(context = contextToUse().withEncoding(SNAPPY)).bytes, status = value.status, timestamp = clock.instant(), - signatures = value.sigs.serialize(context = contextToUse().withEncoding(SNAPPY)).bytes, - statesToRecord = value.metadata?.statesToRecord, - initiator = value.metadata?.initiator?.toString(), - participants = value.metadata?.peers?.map { it.toString() } + signatures = value.sigs.serialize(context = contextToUse().withEncoding(SNAPPY)).bytes ) }, persistentEntityClass = DBTransaction::class.java, @@ -254,18 +208,18 @@ class DBTransactionStorage(private val database: CordaPersistence, cacheFactory: updateTransaction(transaction.id) } - override fun addUnnotarisedTransaction(transaction: SignedTransaction, metadata: FlowTransactionMetadata) = - addTransaction(transaction, metadata, TransactionStatus.IN_FLIGHT) { + override fun addUnnotarisedTransaction(transaction: SignedTransaction, metadata: TransactionMetadata, isInitiator: Boolean) = + addTransaction(transaction, TransactionStatus.IN_FLIGHT) { false } - override fun finalizeTransaction(transaction: SignedTransaction, metadata: FlowTransactionMetadata) = - addTransaction(transaction, metadata) { + override fun finalizeTransaction(transaction: SignedTransaction, metadata: TransactionMetadata, isInitiator: Boolean) = + addTransaction(transaction) { false } + override fun removeUnnotarisedTransaction(id: SecureHash): Boolean { return database.transaction { - val session = currentDBSession() val criteriaBuilder = session.criteriaBuilder val delete = criteriaBuilder.createCriteriaDelete(DBTransaction::class.java) val root = delete.from(DBTransaction::class.java) @@ -289,13 +243,12 @@ class DBTransactionStorage(private val database: CordaPersistence, cacheFactory: finalizeTransactionWithExtraSignatures(transaction.id, signatures) } - private fun addTransaction(transaction: SignedTransaction, - metadata: FlowTransactionMetadata? = null, + protected fun addTransaction(transaction: SignedTransaction, status: TransactionStatus = TransactionStatus.VERIFIED, updateFn: (SecureHash) -> Boolean): Boolean { return database.transaction { txStorage.locked { - val cachedValue = TxCacheValue(transaction, status, metadata) + val cachedValue = TxCacheValue(transaction, status) val addedOrUpdated = addOrUpdate(transaction.id, cachedValue) { k, _ -> updateFn(k) } if (addedOrUpdated) { logger.debug { "Transaction ${transaction.id} has been recorded as $status" } @@ -436,29 +389,21 @@ class DBTransactionStorage(private val database: CordaPersistence, cacheFactory: } // Cache value type to just store the immutable bits of a signed transaction plus conversion helpers - private class TxCacheValue( + internal class TxCacheValue( val txBits: SerializedBytes, val sigs: List, - val status: TransactionStatus, - // flow metadata recorded for recovery - val metadata: FlowTransactionMetadata? = null + val status: TransactionStatus ) { constructor(stx: SignedTransaction, status: TransactionStatus) : this( stx.txBits, Collections.unmodifiableList(stx.sigs), status ) - constructor(stx: SignedTransaction, status: TransactionStatus, metadata: FlowTransactionMetadata?) : this( - stx.txBits, - Collections.unmodifiableList(stx.sigs), - status, - metadata - ) - constructor(stx: SignedTransaction, status: TransactionStatus, sigs: List?, metadata: FlowTransactionMetadata?) : this( + + constructor(stx: SignedTransaction, status: TransactionStatus, sigs: List?) : this( stx.txBits, if (sigs == null) Collections.unmodifiableList(stx.sigs) else Collections.unmodifiableList(stx.sigs + sigs).distinct(), - status, - metadata + status ) fun toSignedTx() = SignedTransaction(txBits, sigs) } diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecovery.kt b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecovery.kt new file mode 100644 index 0000000000..f7b65472ac --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecovery.kt @@ -0,0 +1,330 @@ +package net.corda.node.services.persistence + +import net.corda.core.crypto.SecureHash +import net.corda.core.flows.RecoveryTimeWindow +import net.corda.core.flows.TransactionMetadata +import net.corda.core.identity.CordaX500Name +import net.corda.core.internal.NamedCacheFactory +import net.corda.core.node.StatesToRecord +import net.corda.core.node.services.vault.Sort +import net.corda.core.serialization.CordaSerializable +import net.corda.core.serialization.deserialize +import net.corda.core.serialization.serialize +import net.corda.core.transactions.SignedTransaction +import net.corda.node.CordaClock +import net.corda.node.services.network.PersistentPartyInfoCache +import net.corda.nodeapi.internal.cryptoservice.CryptoService +import net.corda.nodeapi.internal.persistence.CordaPersistence +import net.corda.nodeapi.internal.persistence.NODE_DATABASE_PREFIX +import net.corda.serialization.internal.CordaSerializationEncoding +import org.hibernate.annotations.Immutable +import java.io.Serializable +import java.time.Instant +import java.util.concurrent.atomic.AtomicLong +import javax.persistence.Column +import javax.persistence.Embeddable +import javax.persistence.EmbeddedId +import javax.persistence.Entity +import javax.persistence.Id +import javax.persistence.Lob +import javax.persistence.Table +import javax.persistence.criteria.Predicate +import kotlin.streams.toList + +class DBTransactionStorageLedgerRecovery(private val database: CordaPersistence, cacheFactory: NamedCacheFactory, + val clock: CordaClock, + private val cryptoService: CryptoService, + private val partyInfoCache: PersistentPartyInfoCache) : DBTransactionStorage(database, cacheFactory, clock) { + @Embeddable + @Immutable + data class PersistentKey( + @Column(name = "sequence_number", nullable = false) + var sequenceNumber: Long, + + @Column(name = "timestamp", nullable = false) + var timestamp: Instant + ) : Serializable { + constructor(key: Key) : this(key.sequenceNumber, key.timestamp) + } + + @Entity + @Table(name = "${NODE_DATABASE_PREFIX}sender_distribution_records") + data class DBSenderDistributionRecord( + @EmbeddedId + var compositeKey: PersistentKey, + + @Column(name = "tx_id", length = 144, nullable = false) + var txId: String, + + /** PartyId of flow peer **/ + @Column(name = "receiver_party_id", nullable = false) + val receiverPartyId: Long, + + /** states to record: NONE, ALL_VISIBLE, ONLY_RELEVANT */ + @Column(name = "states_to_record", nullable = false) + var statesToRecord: StatesToRecord + + ) { + fun toSenderDistributionRecord() = + SenderDistributionRecord( + SecureHash.parse(this.txId), + this.receiverPartyId, + this.statesToRecord, + this.compositeKey.timestamp + ) + } + + @Entity + @Table(name = "${NODE_DATABASE_PREFIX}receiver_distribution_records") + data class DBReceiverDistributionRecord( + @EmbeddedId + var compositeKey: PersistentKey, + + @Column(name = "tx_id", length = 144, nullable = false) + var txId: String, + + /** PartyId of flow initiator **/ + @Column(name = "sender_party_id", nullable = true) + val senderPartyId: Long, + + /** Encrypted information for use by Sender (eg. partyId's of flow peers) **/ + @Lob + @Column(name = "distribution_list", nullable = false) + val distributionList: ByteArray, + + /** states to record: NONE, ALL_VISIBLE, ONLY_RELEVANT */ + @Column(name = "receiver_states_to_record", nullable = false) + val receiverStatesToRecord: StatesToRecord, + + /** states to record: NONE, ALL_VISIBLE, ONLY_RELEVANT */ + @Column(name = "sender_states_to_record", nullable = false) + val senderStatesToRecord: StatesToRecord +) { + constructor(key: Key, txId: SecureHash, initiatorPartyId: Long, peerPartyIds: Set, statesToRecord: StatesToRecord, cryptoService: CryptoService) : + this(PersistentKey(key), + txId = txId.toString(), + senderPartyId = initiatorPartyId, + distributionList = cryptoService.encrypt(peerPartyIds.serialize(context = contextToUse().withEncoding(CordaSerializationEncoding.SNAPPY)).bytes), + receiverStatesToRecord = statesToRecord, + senderStatesToRecord = StatesToRecord.NONE // to be set in follow-up PR. + ) + + fun toReceiverDistributionRecord(cryptoService: CryptoService) = + ReceiverDistributionRecord( + SecureHash.parse(this.txId), + this.senderPartyId, + cryptoService.decrypt(this.distributionList).deserialize(context = contextToUse()), + this.receiverStatesToRecord, + this.compositeKey.timestamp + ) + } + + @Entity + @Table(name = "${NODE_DATABASE_PREFIX}recovery_party_info") + data class DBRecoveryPartyInfo( + @Id + /** CordaX500Name hashCode() **/ + @Column(name = "party_id", nullable = false) + var partyId: Long, + + /** CordaX500Name of party **/ + @Column(name = "party_name", nullable = false) + val partyName: String + ) + + class Key( + val timestamp: Instant, + val sequenceNumber: Long = nextSequenceNumber.andIncrement + ) { + companion object { + private val nextSequenceNumber = AtomicLong() + } + } + + override fun addUnnotarisedTransaction(transaction: SignedTransaction, metadata: TransactionMetadata, isInitiator: Boolean): Boolean { + return addTransaction(transaction, TransactionStatus.IN_FLIGHT) { + addTransactionRecoveryMetadata(transaction.id, metadata, isInitiator, clock) + } + } + + override fun finalizeTransaction(transaction: SignedTransaction, metadata: TransactionMetadata, isInitiator: Boolean) = + addTransaction(transaction) { + addTransactionRecoveryMetadata(transaction.id, metadata, isInitiator, clock) + } + + override fun removeUnnotarisedTransaction(id: SecureHash): Boolean { + return database.transaction { + super.removeUnnotarisedTransaction(id) + val criteriaBuilder = session.criteriaBuilder + val deleteSenderDistributionRecords = criteriaBuilder.createCriteriaDelete(DBSenderDistributionRecord::class.java) + val root = deleteSenderDistributionRecords.from(DBSenderDistributionRecord::class.java) + deleteSenderDistributionRecords.where(criteriaBuilder.equal(root.get(DBSenderDistributionRecord::txId.name), id.toString())) + val deletedSenderDistributionRecords = session.createQuery(deleteSenderDistributionRecords).executeUpdate() != 0 + val deleteReceiverDistributionRecords = criteriaBuilder.createCriteriaDelete(DBReceiverDistributionRecord::class.java) + val rootReceiverDistributionRecord = deleteReceiverDistributionRecords.from(DBReceiverDistributionRecord::class.java) + deleteReceiverDistributionRecords.where(criteriaBuilder.equal(rootReceiverDistributionRecord.get(DBReceiverDistributionRecord::txId.name), id.toString())) + val deletedReceiverDistributionRecords = session.createQuery(deleteReceiverDistributionRecords).executeUpdate() != 0 + deletedSenderDistributionRecords || deletedReceiverDistributionRecords + } + } + + fun queryDistributionRecords(timeWindow: RecoveryTimeWindow, + recordType: DistributionRecordType = DistributionRecordType.ALL, + excludingTxnIds: Set? = null, + orderByTimestamp: Sort.Direction? = null + ): List { + return when(recordType) { + DistributionRecordType.SENDER -> + querySenderDistributionRecords(timeWindow, excludingTxnIds = excludingTxnIds, orderByTimestamp = orderByTimestamp) + DistributionRecordType.RECEIVER -> + queryReceiverDistributionRecords(timeWindow, excludingTxnIds = excludingTxnIds, orderByTimestamp = orderByTimestamp) + DistributionRecordType.ALL -> + querySenderDistributionRecords(timeWindow, excludingTxnIds = excludingTxnIds, orderByTimestamp = orderByTimestamp).plus( + queryReceiverDistributionRecords(timeWindow, excludingTxnIds = excludingTxnIds, orderByTimestamp = orderByTimestamp) + ) + } + } + + @Suppress("SpreadOperator") + fun querySenderDistributionRecords(timeWindow: RecoveryTimeWindow, + peers: Set = emptySet(), + excludingTxnIds: Set? = null, + orderByTimestamp: Sort.Direction? = null + ): List { + return database.transaction { + val criteriaBuilder = session.criteriaBuilder + val criteriaQuery = criteriaBuilder.createQuery(DBSenderDistributionRecord::class.java) + val txnMetadata = criteriaQuery.from(DBSenderDistributionRecord::class.java) + val predicates = mutableListOf() + val compositeKey = txnMetadata.get("compositeKey") + predicates.add(criteriaBuilder.greaterThanOrEqualTo(compositeKey.get(PersistentKey::timestamp.name), timeWindow.fromTime)) + predicates.add(criteriaBuilder.and(criteriaBuilder.lessThanOrEqualTo(compositeKey.get(PersistentKey::timestamp.name), timeWindow.untilTime))) + excludingTxnIds?.let { excludingTxnIds -> + predicates.add(criteriaBuilder.and(criteriaBuilder.notEqual(txnMetadata.get(DBSenderDistributionRecord::txId.name), + excludingTxnIds.map { it.toString() }))) + } + if (peers.isNotEmpty()) { + val peerPartyIds = peers.map { partyInfoCache.getPartyIdByCordaX500Name(it) } + predicates.add(criteriaBuilder.and(txnMetadata.get(DBSenderDistributionRecord::receiverPartyId.name).`in`(peerPartyIds))) + } + criteriaQuery.where(*predicates.toTypedArray()) + // optionally order by timestamp + orderByTimestamp?.let { + val orderCriteria = + when (orderByTimestamp) { + // when adding column position of 'group by' shift in case columns were removed + Sort.Direction.ASC -> criteriaBuilder.asc(compositeKey.get(PersistentKey::timestamp.name)) + Sort.Direction.DESC -> criteriaBuilder.desc(compositeKey.get(PersistentKey::timestamp.name)) + } + criteriaQuery.orderBy(orderCriteria) + } + val results = session.createQuery(criteriaQuery).stream() + results.map { it.toSenderDistributionRecord() }.toList() + } + } + + @Suppress("SpreadOperator") + fun queryReceiverDistributionRecords(timeWindow: RecoveryTimeWindow, + initiators: Set = emptySet(), + excludingTxnIds: Set? = null, + orderByTimestamp: Sort.Direction? = null + ): List { + return database.transaction { + val criteriaBuilder = session.criteriaBuilder + val criteriaQuery = criteriaBuilder.createQuery(DBReceiverDistributionRecord::class.java) + val txnMetadata = criteriaQuery.from(DBReceiverDistributionRecord::class.java) + val predicates = mutableListOf() + val compositeKey = txnMetadata.get("compositeKey") + predicates.add(criteriaBuilder.greaterThanOrEqualTo(compositeKey.get(PersistentKey::timestamp.name), timeWindow.fromTime)) + predicates.add(criteriaBuilder.and(criteriaBuilder.lessThanOrEqualTo(compositeKey.get(PersistentKey::timestamp.name), timeWindow.untilTime))) + excludingTxnIds?.let { excludingTxnIds -> + predicates.add(criteriaBuilder.and(criteriaBuilder.notEqual(txnMetadata.get(DBReceiverDistributionRecord::txId.name), + excludingTxnIds.map { it.toString() }))) + } + if (initiators.isNotEmpty()) { + val initiatorPartyIds = initiators.map { partyInfoCache.getPartyIdByCordaX500Name(it) } + predicates.add(criteriaBuilder.and(txnMetadata.get(DBReceiverDistributionRecord::senderPartyId.name).`in`(initiatorPartyIds))) + } + criteriaQuery.where(*predicates.toTypedArray()) + // optionally order by timestamp + orderByTimestamp?.let { + val orderCriteria = + when (orderByTimestamp) { + // when adding column position of 'group by' shift in case columns were removed + Sort.Direction.ASC -> criteriaBuilder.asc(compositeKey.get(PersistentKey::timestamp.name)) + Sort.Direction.DESC -> criteriaBuilder.desc(compositeKey.get(PersistentKey::timestamp.name)) + } + criteriaQuery.orderBy(orderCriteria) + } + val results = session.createQuery(criteriaQuery).stream() + results.map { it.toReceiverDistributionRecord(cryptoService) }.toList() + } + } + + @Suppress("IMPLICIT_CAST_TO_ANY") + private fun addTransactionRecoveryMetadata(txId: SecureHash, metadata: TransactionMetadata, isInitiator: Boolean, clock: CordaClock): Boolean { + database.transaction { + if (isInitiator) { + metadata.peers?.map { peer -> + val senderDistributionRecord = DBSenderDistributionRecord(PersistentKey(Key(clock.instant())), + txId.toString(), + partyInfoCache.getPartyIdByCordaX500Name(peer), + metadata.statesToRecord ?: StatesToRecord.ONLY_RELEVANT) + session.save(senderDistributionRecord) + } + } else { + val receiverDistributionRecord = + DBReceiverDistributionRecord(Key(clock.instant()), + txId, + partyInfoCache.getPartyIdByCordaX500Name(metadata.initiator), + metadata.peers?.map { partyInfoCache.getPartyIdByCordaX500Name(it) }?.toSet() ?: emptySet(), + metadata.statesToRecord ?: StatesToRecord.ONLY_RELEVANT, + cryptoService) + session.save(receiverDistributionRecord) + } + } + return false + } +} + +// TO DO: https://r3-cev.atlassian.net/browse/ENT-9876 +private fun CryptoService.decrypt(bytes: ByteArray): ByteArray { + return bytes +} + +// TO DO: https://r3-cev.atlassian.net/browse/ENT-9876 +private fun CryptoService.encrypt(bytes: ByteArray): ByteArray { + return bytes +} + +@CordaSerializable +open class DistributionRecord( + open val txId: SecureHash, + open val statesToRecord: StatesToRecord, + open val timestamp: Instant +) + +@CordaSerializable +data class SenderDistributionRecord( + override val txId: SecureHash, + val peerPartyId: Long, // CordaX500Name hashCode() + override val statesToRecord: StatesToRecord, + override val timestamp: Instant +) : DistributionRecord(txId, statesToRecord, timestamp) + +@CordaSerializable +data class ReceiverDistributionRecord( + override val txId: SecureHash, + val initiatorPartyId: Long, // CordaX500Name hashCode() + val peerPartyIds: Set, // CordaX500Name hashCode() + override val statesToRecord: StatesToRecord, + override val timestamp: Instant +) : DistributionRecord(txId, statesToRecord, timestamp) + +@CordaSerializable +enum class DistributionRecordType { + SENDER, RECEIVER, ALL +} + + + diff --git a/node/src/main/kotlin/net/corda/node/services/schema/NodeSchemaService.kt b/node/src/main/kotlin/net/corda/node/services/schema/NodeSchemaService.kt index f62c40ee1c..760544758d 100644 --- a/node/src/main/kotlin/net/corda/node/services/schema/NodeSchemaService.kt +++ b/node/src/main/kotlin/net/corda/node/services/schema/NodeSchemaService.kt @@ -16,6 +16,7 @@ import net.corda.node.services.keys.BasicHSMKeyManagementService import net.corda.node.services.messaging.P2PMessageDeduplicator import net.corda.node.services.network.PersistentNetworkMapCache import net.corda.node.services.persistence.DBCheckpointStorage +import net.corda.node.services.persistence.DBTransactionStorageLedgerRecovery import net.corda.node.services.persistence.DBTransactionStorage import net.corda.node.services.persistence.NodeAttachmentService import net.corda.node.services.persistence.PublicKeyHashToExternalId @@ -51,7 +52,10 @@ class NodeSchemaService(private val extraSchemas: Set = emptySet() ContractUpgradeServiceImpl.DBContractUpgrade::class.java, DBNetworkParametersStorage.PersistentNetworkParameters::class.java, PublicKeyHashToExternalId::class.java, - PersistentNetworkMapCache.PersistentPartyToPublicKeyHash::class.java + PersistentNetworkMapCache.PersistentPartyToPublicKeyHash::class.java, + DBTransactionStorageLedgerRecovery.DBSenderDistributionRecord::class.java, + DBTransactionStorageLedgerRecovery.DBReceiverDistributionRecord::class.java, + DBTransactionStorageLedgerRecovery.DBRecoveryPartyInfo::class.java )) { override val migrationResource = "node-core.changelog-master" } diff --git a/node/src/main/kotlin/net/corda/node/utilities/NodeNamedCache.kt b/node/src/main/kotlin/net/corda/node/utilities/NodeNamedCache.kt index 9c2e2eaf93..fa06c1b25b 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/NodeNamedCache.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/NodeNamedCache.kt @@ -64,6 +64,10 @@ open class DefaultNamedCacheFactory protected constructor(private val metricRegi name == "PublicKeyToOwningIdentityCache_cache" -> caffeine.maximumSize(defaultCacheSize) name == "NodeAttachmentTrustCalculator_trustedKeysCache" -> caffeine.maximumSize(defaultCacheSize) name == "AttachmentsClassLoader_cache" -> caffeine.maximumSize(defaultAttachmentsClassLoaderCacheSize) + name == "RecoveryPartyInfoCache_byCordaX500Name" -> caffeine.maximumSize(defaultCacheSize) + name == "RecoveryPartyInfoCache_byPartyId" -> caffeine.maximumSize(defaultCacheSize) + name == "DBTransactionRecovery_senderDistributionRecords" -> caffeine.maximumSize(defaultCacheSize) + name == "DBTransactionRecovery_receiverDistributionRecords" -> caffeine.maximumSize(defaultCacheSize) else -> throw IllegalArgumentException("Unexpected cache name $name. Did you add a new cache?") } } diff --git a/node/src/main/resources/migration/node-core.changelog-master.xml b/node/src/main/resources/migration/node-core.changelog-master.xml index a7949838ce..0ebf26bdc1 100644 --- a/node/src/main/resources/migration/node-core.changelog-master.xml +++ b/node/src/main/resources/migration/node-core.changelog-master.xml @@ -30,6 +30,7 @@ + diff --git a/node/src/main/resources/migration/node-core.changelog-v25.xml b/node/src/main/resources/migration/node-core.changelog-v25.xml new file mode 100644 index 0000000000..d28037153f --- /dev/null +++ b/node/src/main/resources/migration/node-core.changelog-v25.xml @@ -0,0 +1,112 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt b/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt index d1436c94da..6f87a4f525 100644 --- a/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt +++ b/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt @@ -17,7 +17,7 @@ import net.corda.core.crypto.SignatureMetadata import net.corda.core.crypto.TransactionSignature import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowSession -import net.corda.core.flows.FlowTransactionMetadata +import net.corda.core.flows.TransactionMetadata import net.corda.core.flows.InitiatedBy import net.corda.core.flows.InitiatingFlow import net.corda.core.flows.StateMachineRunId @@ -801,10 +801,10 @@ class TwoPartyTradeFlowTests(private val anonymous: Boolean) { return true } - override fun addUnnotarisedTransaction(transaction: SignedTransaction, metadata: FlowTransactionMetadata): Boolean { + override fun addUnnotarisedTransaction(transaction: SignedTransaction, metadata: TransactionMetadata, isInitiator: Boolean): Boolean { database.transaction { records.add(TxRecord.Add(transaction)) - delegate.addUnnotarisedTransaction(transaction, metadata) + delegate.addUnnotarisedTransaction(transaction, metadata, isInitiator) } return true } @@ -815,9 +815,9 @@ class TwoPartyTradeFlowTests(private val anonymous: Boolean) { } } - override fun finalizeTransaction(transaction: SignedTransaction, metadata: FlowTransactionMetadata): Boolean { + override fun finalizeTransaction(transaction: SignedTransaction, metadata: TransactionMetadata, isInitiator: Boolean): Boolean { database.transaction { - delegate.finalizeTransaction(transaction, metadata) + delegate.finalizeTransaction(transaction, metadata, isInitiator) } return true } diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecoveryTests.kt b/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecoveryTests.kt new file mode 100644 index 0000000000..4e17a0b6f9 --- /dev/null +++ b/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecoveryTests.kt @@ -0,0 +1,305 @@ +package net.corda.node.services.persistence + +import net.corda.core.contracts.StateRef +import net.corda.core.crypto.Crypto +import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.SignableData +import net.corda.core.crypto.SignatureMetadata +import net.corda.core.crypto.sign +import net.corda.core.flows.TransactionMetadata +import net.corda.core.flows.RecoveryTimeWindow +import net.corda.core.node.NodeInfo +import net.corda.core.node.StatesToRecord +import net.corda.core.transactions.SignedTransaction +import net.corda.core.transactions.WireTransaction +import net.corda.core.utilities.NetworkHostAndPort +import net.corda.node.CordaClock +import net.corda.node.SimpleClock +import net.corda.node.services.identity.InMemoryIdentityService +import net.corda.node.services.network.PersistentNetworkMapCache +import net.corda.node.services.network.PersistentPartyInfoCache +import net.corda.node.services.persistence.DBTransactionStorage.TransactionStatus.IN_FLIGHT +import net.corda.node.services.persistence.DBTransactionStorage.TransactionStatus.VERIFIED +import net.corda.nodeapi.internal.DEV_ROOT_CA +import net.corda.nodeapi.internal.cryptoservice.CryptoService +import net.corda.nodeapi.internal.persistence.CordaPersistence +import net.corda.nodeapi.internal.persistence.DatabaseConfig +import net.corda.testing.core.ALICE_NAME +import net.corda.testing.core.BOB_NAME +import net.corda.testing.core.CHARLIE_NAME +import net.corda.testing.core.DUMMY_NOTARY_NAME +import net.corda.testing.core.SerializationEnvironmentRule +import net.corda.testing.core.TestIdentity +import net.corda.testing.core.dummyCommand +import net.corda.testing.internal.TestingNamedCacheFactory +import net.corda.testing.internal.configureDatabase +import net.corda.testing.internal.createWireTransaction +import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties +import net.corda.testing.node.internal.MockCryptoService +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import java.security.KeyPair +import java.time.Clock +import java.time.Instant.now +import java.time.temporal.ChronoUnit +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertNull + +class DBTransactionStorageLedgerRecoveryTests { + private companion object { + val ALICE = TestIdentity(ALICE_NAME, 70) + val BOB = TestIdentity(BOB_NAME, 80) + val CHARLIE = TestIdentity(CHARLIE_NAME, 90) + val DUMMY_NOTARY = TestIdentity(DUMMY_NOTARY_NAME, 20) + } + + @Rule + @JvmField + val testSerialization = SerializationEnvironmentRule(inheritable = true) + + private lateinit var database: CordaPersistence + private lateinit var transactionRecovery: DBTransactionStorageLedgerRecovery + private lateinit var partyInfoCache: PersistentPartyInfoCache + + @Before + fun setUp() { + val dataSourceProps = makeTestDataSourceProperties() + database = configureDatabase(dataSourceProps, DatabaseConfig(), { null }, { null }) + newTransactionRecovery() + } + + @After + fun cleanUp() { + database.close() + } + + @Test(timeout = 300_000) + fun `query local ledger for transactions with recovery peers within time window`() { + val beforeFirstTxn = now() + transactionRecovery.addUnnotarisedTransaction(newTransaction(), TransactionMetadata(ALICE_NAME, StatesToRecord.ALL_VISIBLE, setOf(BOB_NAME)), true) + val timeWindow = RecoveryTimeWindow(fromTime = beforeFirstTxn, + untilTime = beforeFirstTxn.plus(1, ChronoUnit.MINUTES)) + val results = transactionRecovery.querySenderDistributionRecords(timeWindow) + assertEquals(1, results.size) + + val afterFirstTxn = now() + transactionRecovery.addUnnotarisedTransaction(newTransaction(), TransactionMetadata(ALICE_NAME, StatesToRecord.ONLY_RELEVANT, setOf(CHARLIE_NAME)), true) + assertEquals(2, transactionRecovery.querySenderDistributionRecords(timeWindow).size) + assertEquals(1, transactionRecovery.querySenderDistributionRecords(RecoveryTimeWindow(fromTime = afterFirstTxn)).size) + } + + @Test(timeout = 300_000) + fun `query local ledger for transactions within timeWindow and excluding remoteTransactionIds`() { + val transaction1 = newTransaction() + transactionRecovery.addUnnotarisedTransaction(transaction1, TransactionMetadata(ALICE_NAME, StatesToRecord.ALL_VISIBLE, setOf(BOB_NAME)), true) + val transaction2 = newTransaction() + transactionRecovery.addUnnotarisedTransaction(transaction2, TransactionMetadata(ALICE_NAME, StatesToRecord.ALL_VISIBLE, setOf(BOB_NAME)), true) + val timeWindow = RecoveryTimeWindow(fromTime = now().minus(1, ChronoUnit.DAYS)) + val results = transactionRecovery.querySenderDistributionRecords(timeWindow, excludingTxnIds = setOf(transaction1.id)) + assertEquals(1, results.size) + } + + @Test(timeout = 300_000) + fun `query local ledger by distribution record type`() { + val transaction1 = newTransaction() + // sender txn + transactionRecovery.addUnnotarisedTransaction(transaction1, TransactionMetadata(ALICE_NAME, StatesToRecord.ALL_VISIBLE, setOf(BOB_NAME)), true) + val transaction2 = newTransaction() + // receiver txn + transactionRecovery.addUnnotarisedTransaction(transaction2, TransactionMetadata(BOB_NAME, StatesToRecord.ALL_VISIBLE, setOf(ALICE_NAME)), false) + val timeWindow = RecoveryTimeWindow(fromTime = now().minus(1, ChronoUnit.DAYS)) + transactionRecovery.queryDistributionRecords(timeWindow, recordType = DistributionRecordType.SENDER).let { + assertEquals(1, it.size) + assertEquals((it[0] as SenderDistributionRecord).peerPartyId, BOB_NAME.hashCode().toLong()) + } + transactionRecovery.queryDistributionRecords(timeWindow, recordType = DistributionRecordType.RECEIVER).let { + assertEquals(1, it.size) + assertEquals((it[0] as ReceiverDistributionRecord).initiatorPartyId, BOB_NAME.hashCode().toLong()) + } + val resultsAll = transactionRecovery.queryDistributionRecords(timeWindow, recordType = DistributionRecordType.ALL) + assertEquals(2, resultsAll.size) + } + + @Test(timeout = 300_000) + fun `query for sender distribution records by peers`() { + transactionRecovery.addUnnotarisedTransaction(newTransaction(), TransactionMetadata(ALICE_NAME, StatesToRecord.ALL_VISIBLE, setOf(BOB_NAME)), true) + transactionRecovery.addUnnotarisedTransaction(newTransaction(), TransactionMetadata(ALICE_NAME, StatesToRecord.ONLY_RELEVANT, setOf(CHARLIE_NAME)), true) + transactionRecovery.addUnnotarisedTransaction(newTransaction(), TransactionMetadata(ALICE_NAME, StatesToRecord.ONLY_RELEVANT, setOf(BOB_NAME, CHARLIE_NAME)), true) + transactionRecovery.addUnnotarisedTransaction(newTransaction(), TransactionMetadata(BOB_NAME, StatesToRecord.ONLY_RELEVANT, setOf(ALICE_NAME)), true) + transactionRecovery.addUnnotarisedTransaction(newTransaction(), TransactionMetadata(CHARLIE_NAME, StatesToRecord.ONLY_RELEVANT), true) + assertEquals(5, readSenderDistributionRecordFromDB().size) + + val timeWindow = RecoveryTimeWindow(fromTime = now().minus(1, ChronoUnit.DAYS)) + transactionRecovery.querySenderDistributionRecords(timeWindow, peers = setOf(BOB_NAME)).let { + assertEquals(2, it.size) + assertEquals(it[0].statesToRecord, StatesToRecord.ALL_VISIBLE) + assertEquals(it[1].statesToRecord, StatesToRecord.ONLY_RELEVANT) + } + assertEquals(1, transactionRecovery.querySenderDistributionRecords(timeWindow, peers = setOf(ALICE_NAME)).size) + assertEquals(2, transactionRecovery.querySenderDistributionRecords(timeWindow, peers = setOf(CHARLIE_NAME)).size) + } + + @Test(timeout = 300_000) + fun `query for receiver distribution records by initiator`() { + transactionRecovery.addUnnotarisedTransaction(newTransaction(), TransactionMetadata(ALICE_NAME, StatesToRecord.ALL_VISIBLE, setOf(BOB_NAME, CHARLIE_NAME)), false) + transactionRecovery.addUnnotarisedTransaction(newTransaction(), TransactionMetadata(ALICE_NAME, StatesToRecord.ONLY_RELEVANT, setOf(BOB_NAME)), false) + transactionRecovery.addUnnotarisedTransaction(newTransaction(), TransactionMetadata(ALICE_NAME, StatesToRecord.NONE, setOf(CHARLIE_NAME)), false) + transactionRecovery.addUnnotarisedTransaction(newTransaction(), TransactionMetadata(BOB_NAME, StatesToRecord.ALL_VISIBLE, setOf(ALICE_NAME)), false) + transactionRecovery.addUnnotarisedTransaction(newTransaction(), TransactionMetadata(CHARLIE_NAME, StatesToRecord.ONLY_RELEVANT), false) + + val timeWindow = RecoveryTimeWindow(fromTime = now().minus(1, ChronoUnit.DAYS)) + transactionRecovery.queryReceiverDistributionRecords(timeWindow, initiators = setOf(ALICE_NAME)).let { + assertEquals(3, it.size) + assertEquals(it[0].statesToRecord, StatesToRecord.ALL_VISIBLE) + assertEquals(it[1].statesToRecord, StatesToRecord.ONLY_RELEVANT) + assertEquals(it[2].statesToRecord, StatesToRecord.NONE) + } + assertEquals(1, transactionRecovery.queryReceiverDistributionRecords(timeWindow, initiators = setOf(BOB_NAME)).size) + assertEquals(1, transactionRecovery.queryReceiverDistributionRecords(timeWindow, initiators = setOf(CHARLIE_NAME)).size) + assertEquals(2, transactionRecovery.queryReceiverDistributionRecords(timeWindow, initiators = setOf(BOB_NAME, CHARLIE_NAME)).size) + } + + @Test(timeout = 300_000) + fun `create un-notarised transaction with flow metadata and validate status in db`() { + val senderTransaction = newTransaction() + transactionRecovery.addUnnotarisedTransaction(senderTransaction, TransactionMetadata(ALICE_NAME, StatesToRecord.ALL_VISIBLE, setOf(BOB_NAME)), true) + assertEquals(IN_FLIGHT, readTransactionFromDB(senderTransaction.id).status) + readSenderDistributionRecordFromDB(senderTransaction.id).let { + assertEquals(1, it.size) + assertEquals(StatesToRecord.ALL_VISIBLE, it[0].statesToRecord) + assertEquals(BOB_NAME, partyInfoCache.getCordaX500NameByPartyId(it[0].peerPartyId)) + } + + val receiverTransaction = newTransaction() + transactionRecovery.addUnnotarisedTransaction(receiverTransaction, TransactionMetadata(ALICE_NAME, StatesToRecord.ONLY_RELEVANT, setOf(BOB_NAME)), false) + assertEquals(IN_FLIGHT, readTransactionFromDB(receiverTransaction.id).status) + readReceiverDistributionRecordFromDB(receiverTransaction.id).let { + assertEquals(StatesToRecord.ONLY_RELEVANT, it.statesToRecord) + assertEquals(ALICE_NAME, partyInfoCache.getCordaX500NameByPartyId(it.initiatorPartyId)) + assertEquals(setOf(BOB_NAME), it.peerPartyIds.map { partyInfoCache.getCordaX500NameByPartyId(it) }.toSet() ) + } + } + + @Test(timeout = 300_000) + fun `finalize transaction with recovery metadata`() { + val transaction = newTransaction(notarySig = false) + transactionRecovery.finalizeTransaction(transaction, + TransactionMetadata(ALICE_NAME), false) + + assertEquals(VERIFIED, readTransactionFromDB(transaction.id).status) + assertEquals(StatesToRecord.ONLY_RELEVANT, readReceiverDistributionRecordFromDB(transaction.id).statesToRecord) + } + + @Test(timeout = 300_000) + fun `remove un-notarised transaction and associated recovery metadata`() { + val senderTransaction = newTransaction(notarySig = false) + transactionRecovery.addUnnotarisedTransaction(senderTransaction, TransactionMetadata(ALICE.name, peers = setOf(BOB.name, CHARLIE_NAME)), true) + assertNull(transactionRecovery.getTransaction(senderTransaction.id)) + assertEquals(IN_FLIGHT, readTransactionFromDB(senderTransaction.id).status) + + assertEquals(true, transactionRecovery.removeUnnotarisedTransaction(senderTransaction.id)) + assertFailsWith { readTransactionFromDB(senderTransaction.id).status } + assertEquals(0, readSenderDistributionRecordFromDB(senderTransaction.id).size) + assertNull(transactionRecovery.getTransactionInternal(senderTransaction.id)) + + val receiverTransaction = newTransaction(notarySig = false) + transactionRecovery.addUnnotarisedTransaction(receiverTransaction, TransactionMetadata(ALICE.name), false) + assertNull(transactionRecovery.getTransaction(receiverTransaction.id)) + assertEquals(IN_FLIGHT, readTransactionFromDB(receiverTransaction.id).status) + + assertEquals(true, transactionRecovery.removeUnnotarisedTransaction(receiverTransaction.id)) + assertFailsWith { readTransactionFromDB(receiverTransaction.id).status } + assertFailsWith { readReceiverDistributionRecordFromDB(receiverTransaction.id) } + assertNull(transactionRecovery.getTransactionInternal(receiverTransaction.id)) + } + + private fun readTransactionFromDB(id: SecureHash): DBTransactionStorage.DBTransaction { + val fromDb = database.transaction { + session.createQuery( + "from ${DBTransactionStorage.DBTransaction::class.java.name} where tx_id = :transactionId", + DBTransactionStorage.DBTransaction::class.java + ).setParameter("transactionId", id.toString()).resultList.map { it } + } + assertEquals(1, fromDb.size) + return fromDb[0] + } + + private fun readSenderDistributionRecordFromDB(id: SecureHash? = null): List { + return database.transaction { + if (id != null) + session.createQuery( + "from ${DBTransactionStorageLedgerRecovery.DBSenderDistributionRecord::class.java.name} where tx_id = :transactionId", + DBTransactionStorageLedgerRecovery.DBSenderDistributionRecord::class.java + ).setParameter("transactionId", id.toString()).resultList.map { it.toSenderDistributionRecord() } + else + session.createQuery( + "from ${DBTransactionStorageLedgerRecovery.DBSenderDistributionRecord::class.java.name}", + DBTransactionStorageLedgerRecovery.DBSenderDistributionRecord::class.java + ).resultList.map { it.toSenderDistributionRecord() } + } + } + + private fun readReceiverDistributionRecordFromDB(id: SecureHash): ReceiverDistributionRecord { + val fromDb = database.transaction { + session.createQuery( + "from ${DBTransactionStorageLedgerRecovery.DBReceiverDistributionRecord::class.java.name} where tx_id = :transactionId", + DBTransactionStorageLedgerRecovery.DBReceiverDistributionRecord::class.java + ).setParameter("transactionId", id.toString()).resultList.map { it } + } + assertEquals(1, fromDb.size) + return fromDb[0].toReceiverDistributionRecord(MockCryptoService(emptyMap())) + } + + private fun newTransactionRecovery(cacheSizeBytesOverride: Long? = null, clock: CordaClock = SimpleClock(Clock.systemUTC()), + cryptoService: CryptoService = MockCryptoService(emptyMap())) { + + val networkMapCache = PersistentNetworkMapCache(TestingNamedCacheFactory(), database, InMemoryIdentityService(trustRoot = DEV_ROOT_CA.certificate)) + val alice = createNodeInfo(listOf(ALICE)) + val bob = createNodeInfo(listOf(BOB)) + val charlie = createNodeInfo(listOf(CHARLIE)) + networkMapCache.addOrUpdateNodes(listOf(alice, bob, charlie)) + partyInfoCache = PersistentPartyInfoCache(networkMapCache, TestingNamedCacheFactory(), database) + partyInfoCache.start() + transactionRecovery = DBTransactionStorageLedgerRecovery(database, TestingNamedCacheFactory(cacheSizeBytesOverride + ?: 1024), clock, cryptoService, partyInfoCache) + } + + private var portCounter = 1000 + private fun createNodeInfo(identities: List, + address: NetworkHostAndPort = NetworkHostAndPort("localhost", portCounter++)): NodeInfo { + return NodeInfo( + addresses = listOf(address), + legalIdentitiesAndCerts = identities.map { it.identity }, + platformVersion = 3, + serial = 1 + ) + } + + private fun newTransaction(notarySig: Boolean = true): SignedTransaction { + val wtx = createWireTransaction( + inputs = listOf(StateRef(SecureHash.randomSHA256(), 0)), + attachments = emptyList(), + outputs = emptyList(), + commands = listOf(dummyCommand(ALICE.publicKey)), + notary = DUMMY_NOTARY.party, + timeWindow = null + ) + return makeSigned(wtx, ALICE.keyPair, notarySig = notarySig) + } + + private fun makeSigned(wtx: WireTransaction, vararg keys: KeyPair, notarySig: Boolean = true): SignedTransaction { + val keySigs = keys.map { it.sign(SignableData(wtx.id, SignatureMetadata(1, Crypto.findSignatureScheme(it.public).schemeNumberID))) } + val sigs = if (notarySig) { + keySigs + notarySig(wtx.id) + } else { + keySigs + } + return SignedTransaction(wtx, sigs) + } + + private fun notarySig(txId: SecureHash) = + DUMMY_NOTARY.keyPair.sign(SignableData(txId, SignatureMetadata(1, Crypto.findSignatureScheme(DUMMY_NOTARY.publicKey).schemeNumberID))) +} diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageTests.kt b/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageTests.kt index cfc9c3ecf1..3a4b8615b3 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageTests.kt @@ -10,8 +10,7 @@ import net.corda.core.crypto.SignableData import net.corda.core.crypto.SignatureMetadata import net.corda.core.crypto.TransactionSignature import net.corda.core.crypto.sign -import net.corda.core.flows.FlowTransactionMetadata -import net.corda.core.node.StatesToRecord +import net.corda.core.flows.TransactionMetadata import net.corda.core.serialization.deserialize import net.corda.core.toFuture import net.corda.core.transactions.SignedTransaction @@ -26,7 +25,6 @@ import net.corda.node.services.transactions.PersistentUniquenessProvider import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.testing.core.ALICE_NAME -import net.corda.testing.core.BOB_NAME import net.corda.testing.core.DUMMY_NOTARY_NAME import net.corda.testing.core.SerializationEnvironmentRule import net.corda.testing.core.TestIdentity @@ -43,7 +41,6 @@ import org.junit.Before import org.junit.Rule import org.junit.Test import rx.plugins.RxJavaHooks -import java.lang.AssertionError import java.security.KeyPair import java.time.Clock import java.time.Instant @@ -58,7 +55,6 @@ import kotlin.test.assertNull class DBTransactionStorageTests { private companion object { val ALICE = TestIdentity(ALICE_NAME, 70) - val BOB_PARTY = TestIdentity(BOB_NAME, 80).party val DUMMY_NOTARY = TestIdentity(DUMMY_NOTARY_NAME, 20) } @@ -113,24 +109,10 @@ class DBTransactionStorageTests { val transactionClock = TransactionClock(now) newTransactionStorage(clock = transactionClock) val transaction = newTransaction() - transactionStorage.addUnnotarisedTransaction(transaction, FlowTransactionMetadata(ALICE.party.name)) + transactionStorage.addUnnotarisedTransaction(transaction, TransactionMetadata(ALICE.party.name), true) assertEquals(IN_FLIGHT, readTransactionFromDB(transaction.id).status) } - @Test(timeout = 300_000) - fun `create un-notarised transaction with flow metadata and validate status in db`() { - val now = Instant.ofEpochSecond(333444555L) - val transactionClock = TransactionClock(now) - newTransactionStorage(clock = transactionClock) - val transaction = newTransaction() - transactionStorage.addUnnotarisedTransaction(transaction, FlowTransactionMetadata(ALICE.party.name, StatesToRecord.ALL_VISIBLE, setOf(BOB_PARTY.name))) - val txn = readTransactionFromDB(transaction.id) - assertEquals(IN_FLIGHT, txn.status) - assertEquals(StatesToRecord.ALL_VISIBLE, txn.statesToRecord) - assertEquals(ALICE_NAME.toString(), txn.initiator) - assertEquals(listOf(BOB_NAME.toString()), txn.participants) - } - @Test(timeout = 300_000) fun `finalize transaction with no prior recording of un-notarised transaction`() { val now = Instant.ofEpochSecond(333444555L) @@ -150,7 +132,7 @@ class DBTransactionStorageTests { val transactionClock = TransactionClock(now) newTransactionStorage(clock = transactionClock) val transaction = newTransaction(notarySig = false) - transactionStorage.addUnnotarisedTransaction(transaction, FlowTransactionMetadata(ALICE.party.name)) + transactionStorage.addUnnotarisedTransaction(transaction, TransactionMetadata(ALICE.party.name), true) assertNull(transactionStorage.getTransaction(transaction.id)) assertEquals(IN_FLIGHT, readTransactionFromDB(transaction.id).status) transactionStorage.finalizeTransactionWithExtraSignatures(transaction, emptyList()) @@ -160,28 +142,13 @@ class DBTransactionStorageTests { } } - @Test(timeout = 300_000) - fun `finalize transaction with recovery metadata`() { - val now = Instant.ofEpochSecond(333444555L) - val transactionClock = TransactionClock(now) - newTransactionStorage(clock = transactionClock) - val transaction = newTransaction(notarySig = false) - transactionStorage.finalizeTransaction(transaction, - FlowTransactionMetadata(ALICE_NAME)) - readTransactionFromDB(transaction.id).let { - assertEquals(VERIFIED, it.status) - assertEquals(ALICE_NAME.toString(), it.initiator) - assertEquals(StatesToRecord.ONLY_RELEVANT, it.statesToRecord) - } - } - @Test(timeout = 300_000) fun `finalize transaction with extra signatures after recording transaction as un-notarised`() { val now = Instant.ofEpochSecond(333444555L) val transactionClock = TransactionClock(now) newTransactionStorage(clock = transactionClock) val transaction = newTransaction(notarySig = false) - transactionStorage.addUnnotarisedTransaction(transaction, FlowTransactionMetadata(ALICE.party.name)) + transactionStorage.addUnnotarisedTransaction(transaction, TransactionMetadata(ALICE.party.name), true) assertNull(transactionStorage.getTransaction(transaction.id)) assertEquals(IN_FLIGHT, readTransactionFromDB(transaction.id).status) val notarySig = notarySig(transaction.id) @@ -198,7 +165,7 @@ class DBTransactionStorageTests { val transactionClock = TransactionClock(now) newTransactionStorage(clock = transactionClock) val transaction = newTransaction(notarySig = false) - transactionStorage.addUnnotarisedTransaction(transaction, FlowTransactionMetadata(ALICE.party.name)) + transactionStorage.addUnnotarisedTransaction(transaction, TransactionMetadata(ALICE.party.name), true) assertNull(transactionStorage.getTransaction(transaction.id)) assertEquals(IN_FLIGHT, readTransactionFromDB(transaction.id).status) @@ -232,7 +199,7 @@ class DBTransactionStorageTests { val transactionWithoutNotarySig = newTransaction(notarySig = false) // txn recorded as un-notarised (simulate ReceiverFinalityFlow in initial flow) - transactionStorage.addUnnotarisedTransaction(transactionWithoutNotarySig, FlowTransactionMetadata(ALICE.party.name)) + transactionStorage.addUnnotarisedTransaction(transactionWithoutNotarySig, TransactionMetadata(ALICE.party.name), false) assertEquals(IN_FLIGHT, readTransactionFromDB(transactionWithoutNotarySig.id).status) // txn then recorded as unverified (simulate ResolveTransactionFlow in follow-up flow) @@ -263,7 +230,7 @@ class DBTransactionStorageTests { val transactionWithoutNotarySigs = newTransaction(notarySig = false) // txn recorded as un-notarised (simulate ReceiverFinalityFlow in initial flow) - transactionStorage.addUnnotarisedTransaction(transactionWithoutNotarySigs, FlowTransactionMetadata(ALICE.party.name)) + transactionStorage.addUnnotarisedTransaction(transactionWithoutNotarySigs, TransactionMetadata(ALICE.party.name), false) assertEquals(IN_FLIGHT, readTransactionFromDB(transactionWithoutNotarySigs.id).status) // txn then recorded as unverified (simulate ResolveTransactionFlow in follow-up flow) diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/MockTransactionStorage.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/MockTransactionStorage.kt index 00593c35ee..156651efd7 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/MockTransactionStorage.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/MockTransactionStorage.kt @@ -9,7 +9,7 @@ import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.toFuture import net.corda.core.transactions.SignedTransaction import net.corda.node.services.api.WritableTransactionStorage -import net.corda.core.flows.FlowTransactionMetadata +import net.corda.core.flows.TransactionMetadata import net.corda.core.flows.TransactionStatus import net.corda.testing.node.MockServices import rx.Observable @@ -55,7 +55,7 @@ open class MockTransactionStorage : WritableTransactionStorage, SingletonSeriali } } - override fun addUnnotarisedTransaction(transaction: SignedTransaction, metadata: FlowTransactionMetadata): Boolean { + override fun addUnnotarisedTransaction(transaction: SignedTransaction, metadata: TransactionMetadata, isInitiator: Boolean): Boolean { return txns.putIfAbsent(transaction.id, TxHolder(transaction, status = TransactionStatus.IN_FLIGHT)) == null } @@ -63,7 +63,7 @@ open class MockTransactionStorage : WritableTransactionStorage, SingletonSeriali return txns.remove(id) != null } - override fun finalizeTransaction(transaction: SignedTransaction, metadata: FlowTransactionMetadata) = + override fun finalizeTransaction(transaction: SignedTransaction, metadata: TransactionMetadata, isInitiator: Boolean) = addTransaction(transaction) override fun finalizeTransactionWithExtraSignatures(transaction: SignedTransaction, signatures: Collection): Boolean { diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TestDSL.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TestDSL.kt index 103e954b5d..5d0f74d25e 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TestDSL.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TestDSL.kt @@ -8,7 +8,7 @@ import net.corda.core.crypto.NullKeys.NULL_SIGNATURE import net.corda.core.crypto.SecureHash import net.corda.core.crypto.TransactionSignature import net.corda.core.flows.FlowException -import net.corda.core.flows.FlowTransactionMetadata +import net.corda.core.flows.TransactionMetadata import net.corda.core.identity.Party import net.corda.core.internal.* import net.corda.core.internal.notary.NotaryService @@ -139,13 +139,13 @@ data class TestTransactionDSLInterpreter private constructor( override val attachmentsClassLoaderCache: AttachmentsClassLoaderCache = AttachmentsClassLoaderCacheImpl(TestingNamedCacheFactory()) - override fun recordUnnotarisedTransaction(txn: SignedTransaction, metadata: FlowTransactionMetadata) {} + override fun recordUnnotarisedTransaction(txn: SignedTransaction, metadata: TransactionMetadata) {} override fun removeUnnotarisedTransaction(id: SecureHash) {} override fun finalizeTransactionWithExtraSignatures(txn: SignedTransaction, sigs: Collection, statesToRecord: StatesToRecord) {} - override fun finalizeTransaction(txn: SignedTransaction, statesToRecord: StatesToRecord, metadata: FlowTransactionMetadata) {} + override fun finalizeTransaction(txn: SignedTransaction, statesToRecord: StatesToRecord, metadata: TransactionMetadata) {} } private fun copy(): TestTransactionDSLInterpreter = From ed08b2c5de6dd028a9d33cc078da71e971b1bd14 Mon Sep 17 00:00:00 2001 From: Shams Asari Date: Thu, 25 May 2023 13:01:42 +0100 Subject: [PATCH 027/126] ENT-9793: Added Page.previousPageAnchor to allow detection of vault changes whilst pages are loaded --- .../corda/core/node/services/VaultService.kt | 72 ++++-- .../net/corda/node/internal/AbstractNode.kt | 2 +- .../internal/NodeServicesForResolution.kt | 15 ++ .../internal/ServicesForResolutionImpl.kt | 23 +- .../node/migration/VaultStateMigration.kt | 5 +- .../PersistentScheduledFlowRepository.kt | 7 +- .../PersistentUniquenessProvider.kt | 9 +- .../node/services/vault/NodeVaultService.kt | 244 +++++++++++------- .../corda/node/services/vault/VaultSchema.kt | 18 ++ .../bftsmart/BFTSmartNotaryService.kt | 10 +- .../corda/notary/jpa/JPAUniquenessProvider.kt | 11 +- .../persistence/HibernateConfigurationTest.kt | 22 +- .../node/services/vault/VaultQueryTests.kt | 92 +++++-- .../vault/VaultSoftLockManagerTest.kt | 4 +- .../net/corda/testing/node/MockServices.kt | 10 +- 15 files changed, 366 insertions(+), 178 deletions(-) create mode 100644 node/src/main/kotlin/net/corda/node/internal/NodeServicesForResolution.kt diff --git a/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt b/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt index 5993d60587..1913e6bf84 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt @@ -1,10 +1,23 @@ +@file:Suppress("LongParameterList") + package net.corda.core.node.services import co.paralleluniverse.fibers.Suspendable import net.corda.core.DeleteForDJVM import net.corda.core.DoNotImplement import net.corda.core.concurrent.CordaFuture -import net.corda.core.contracts.* +import net.corda.core.contracts.AlwaysAcceptAttachmentConstraint +import net.corda.core.contracts.Amount +import net.corda.core.contracts.AttachmentConstraint +import net.corda.core.contracts.ContractState +import net.corda.core.contracts.FungibleAsset +import net.corda.core.contracts.FungibleState +import net.corda.core.contracts.HashAttachmentConstraint +import net.corda.core.contracts.ReferencedStateAndRef +import net.corda.core.contracts.SignatureAttachmentConstraint +import net.corda.core.contracts.StateAndRef +import net.corda.core.contracts.StateRef +import net.corda.core.contracts.WhitelistedByZoneAttachmentConstraint import net.corda.core.crypto.Crypto import net.corda.core.crypto.SecureHash import net.corda.core.flows.FlowException @@ -13,9 +26,16 @@ import net.corda.core.identity.AbstractParty import net.corda.core.internal.MAX_NUMBER_OF_KEYS_IN_SIGNATURE_CONSTRAINT import net.corda.core.internal.concurrent.doneFuture import net.corda.core.messaging.DataFeed -import net.corda.core.node.services.Vault.RelevancyStatus.* +import net.corda.core.node.StatesToRecord +import net.corda.core.node.services.Vault.RelevancyStatus.ALL +import net.corda.core.node.services.Vault.RelevancyStatus.NOT_RELEVANT +import net.corda.core.node.services.Vault.RelevancyStatus.RELEVANT import net.corda.core.node.services.Vault.StateStatus -import net.corda.core.node.services.vault.* +import net.corda.core.node.services.vault.DEFAULT_PAGE_SIZE +import net.corda.core.node.services.vault.MAX_PAGE_SIZE +import net.corda.core.node.services.vault.PageSpecification +import net.corda.core.node.services.vault.QueryCriteria +import net.corda.core.node.services.vault.Sort import net.corda.core.serialization.CordaSerializable import net.corda.core.toFuture import net.corda.core.transactions.LedgerTransaction @@ -196,16 +216,28 @@ class Vault(val states: Iterable>) { * otherwise it defaults to -1. * 4) Status types used in this query: [StateStatus.UNCONSUMED], [StateStatus.CONSUMED], [StateStatus.ALL]. * 5) Other results as a [List] of any type (eg. aggregate function results with/without group by). + * 6) A [StateRef] pointing to the last state of the previous page. Use this to detect if the database has changed whilst loading pages + * by checking it matches your last loaded state. * - * Note: currently otherResults are used only for Aggregate Functions (in which case, the states and statesMetadata - * results will be empty). + * Note: currently [otherResults] is used only for aggregate functions (in which case, [states] and [statesMetadata] will be empty). */ @CordaSerializable - data class Page(val states: List>, - val statesMetadata: List, - val totalStatesAvailable: Long, - val stateTypes: StateStatus, - val otherResults: List) + data class Page @JvmOverloads constructor( + val states: List>, + val statesMetadata: List, + val totalStatesAvailable: Long, + val stateTypes: StateStatus, + val otherResults: List, + val previousPageAnchor: StateRef? = null + ) { + fun copy(states: List> = this.states, + statesMetadata: List = this.statesMetadata, + totalStatesAvailable: Long = this.totalStatesAvailable, + stateTypes: StateStatus = this.stateTypes, + otherResults: List = this.otherResults): Page { + return Page(states, statesMetadata, totalStatesAvailable, stateTypes, otherResults, null) + } + } @CordaSerializable data class StateMetadata @JvmOverloads constructor( @@ -213,11 +245,11 @@ class Vault(val states: Iterable>) { val contractStateClassName: String, val recordedTime: Instant, val consumedTime: Instant?, - val status: Vault.StateStatus, + val status: StateStatus, val notary: AbstractParty?, val lockId: String?, val lockUpdateTime: Instant?, - val relevancyStatus: Vault.RelevancyStatus? = null, + val relevancyStatus: RelevancyStatus? = null, val constraintInfo: ConstraintInfo? = null ) { fun copy( @@ -225,7 +257,7 @@ class Vault(val states: Iterable>) { contractStateClassName: String = this.contractStateClassName, recordedTime: Instant = this.recordedTime, consumedTime: Instant? = this.consumedTime, - status: Vault.StateStatus = this.status, + status: StateStatus = this.status, notary: AbstractParty? = this.notary, lockId: String? = this.lockId, lockUpdateTime: Instant? = this.lockUpdateTime @@ -237,11 +269,11 @@ class Vault(val states: Iterable>) { contractStateClassName: String = this.contractStateClassName, recordedTime: Instant = this.recordedTime, consumedTime: Instant? = this.consumedTime, - status: Vault.StateStatus = this.status, + status: StateStatus = this.status, notary: AbstractParty? = this.notary, lockId: String? = this.lockId, lockUpdateTime: Instant? = this.lockUpdateTime, - relevancyStatus: Vault.RelevancyStatus? + relevancyStatus: RelevancyStatus? ): StateMetadata { return StateMetadata(ref, contractStateClassName, recordedTime, consumedTime, status, notary, lockId, lockUpdateTime, relevancyStatus, ConstraintInfo(AlwaysAcceptAttachmentConstraint)) } @@ -249,9 +281,9 @@ class Vault(val states: Iterable>) { companion object { @Deprecated("No longer used. The vault does not emit empty updates") - val NoUpdate = Update(emptySet(), emptySet(), type = Vault.UpdateType.GENERAL, references = emptySet()) + val NoUpdate = Update(emptySet(), emptySet(), type = UpdateType.GENERAL, references = emptySet()) @Deprecated("No longer used. The vault does not emit empty updates") - val NoNotaryUpdate = Vault.Update(emptySet(), emptySet(), type = Vault.UpdateType.NOTARY_CHANGE, references = emptySet()) + val NoNotaryUpdate = Update(emptySet(), emptySet(), type = UpdateType.NOTARY_CHANGE, references = emptySet()) } } @@ -302,7 +334,7 @@ interface VaultService { fun whenConsumed(ref: StateRef): CordaFuture> { val query = QueryCriteria.VaultQueryCriteria( stateRefs = listOf(ref), - status = Vault.StateStatus.CONSUMED + status = StateStatus.CONSUMED ) val result = trackBy(query) val snapshot = result.snapshot.states @@ -358,8 +390,8 @@ interface VaultService { /** * Helper function to determine spendable states and soft locking them. * Currently performance will be worse than for the hand optimised version in - * [Cash.unconsumedCashStatesForSpending]. However, this is fully generic and can operate with custom [FungibleState] - * and [FungibleAsset] states. + * [net.corda.finance.workflows.asset.selection.AbstractCashSelection.unconsumedCashStatesForSpending]. However, this is fully generic + * and can operate with custom [FungibleState] and [FungibleAsset] states. * @param lockId The [FlowLogic.runId]'s [UUID] of the current flow used to soft lock the states. * @param eligibleStatesQuery A custom query object that selects down to the appropriate subset of all states of the * [contractStateType]. e.g. by selecting on account, issuer, etc. The query is internally augmented with the diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index eb781dd3f7..5fbbde1296 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -1181,7 +1181,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, networkParameters: NetworkParameters) protected open fun makeVaultService(keyManagementService: KeyManagementService, - services: ServicesForResolution, + services: NodeServicesForResolution, database: CordaPersistence, cordappLoader: CordappLoader): VaultServiceInternal { return NodeVaultService(platformClock, keyManagementService, services, database, schemaService, cordappLoader.appClassLoader) diff --git a/node/src/main/kotlin/net/corda/node/internal/NodeServicesForResolution.kt b/node/src/main/kotlin/net/corda/node/internal/NodeServicesForResolution.kt new file mode 100644 index 0000000000..5baa528297 --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/internal/NodeServicesForResolution.kt @@ -0,0 +1,15 @@ +package net.corda.node.internal + +import net.corda.core.contracts.ContractState +import net.corda.core.contracts.StateAndRef +import net.corda.core.contracts.StateRef +import net.corda.core.contracts.TransactionResolutionException +import net.corda.core.node.ServicesForResolution +import java.util.LinkedHashSet + +interface NodeServicesForResolution : ServicesForResolution { + @Throws(TransactionResolutionException::class) + override fun loadStates(stateRefs: Set): Set> = loadStates(stateRefs, LinkedHashSet()) + + fun >> loadStates(input: Iterable, output: C): C +} diff --git a/node/src/main/kotlin/net/corda/node/internal/ServicesForResolutionImpl.kt b/node/src/main/kotlin/net/corda/node/internal/ServicesForResolutionImpl.kt index f5836c0cc5..ffea11f536 100644 --- a/node/src/main/kotlin/net/corda/node/internal/ServicesForResolutionImpl.kt +++ b/node/src/main/kotlin/net/corda/node/internal/ServicesForResolutionImpl.kt @@ -1,10 +1,17 @@ package net.corda.node.internal -import net.corda.core.contracts.* +import net.corda.core.contracts.Attachment +import net.corda.core.contracts.AttachmentResolutionException +import net.corda.core.contracts.ContractAttachment +import net.corda.core.contracts.ContractState +import net.corda.core.contracts.StateAndRef +import net.corda.core.contracts.StateRef +import net.corda.core.contracts.TransactionResolutionException +import net.corda.core.contracts.TransactionState import net.corda.core.cordapp.CordappProvider import net.corda.core.internal.SerializedStateAndRef +import net.corda.core.internal.uncheckedCast import net.corda.core.node.NetworkParameters -import net.corda.core.node.ServicesForResolution import net.corda.core.node.services.AttachmentStorage import net.corda.core.node.services.IdentityService import net.corda.core.node.services.NetworkParametersService @@ -20,7 +27,7 @@ data class ServicesForResolutionImpl( override val cordappProvider: CordappProvider, override val networkParametersService: NetworkParametersService, private val validatedTransactions: TransactionStorage -) : ServicesForResolution { +) : NodeServicesForResolution { override val networkParameters: NetworkParameters get() = networkParametersService.lookup(networkParametersService.currentHash) ?: throw IllegalArgumentException("No current parameters in network parameters storage") @@ -30,13 +37,13 @@ data class ServicesForResolutionImpl( return stx.resolveBaseTransaction(this).outputs[stateRef.index] } - @Throws(TransactionResolutionException::class) - override fun loadStates(stateRefs: Set): Set> { - return stateRefs.groupBy { it.txhash }.flatMap { + override fun >> loadStates(input: Iterable, output: C): C { + input.groupBy { it.txhash }.forEach { val stx = validatedTransactions.getTransaction(it.key) ?: throw TransactionResolutionException(it.key) val baseTx = stx.resolveBaseTransaction(this) - it.value.map { ref -> StateAndRef(baseTx.outputs[ref.index], ref) } - }.toSet() + it.value.mapTo(output) { ref -> StateAndRef(uncheckedCast(baseTx.outputs[ref.index]), ref) } + } + return output } @Throws(TransactionResolutionException::class, AttachmentResolutionException::class) diff --git a/node/src/main/kotlin/net/corda/node/migration/VaultStateMigration.kt b/node/src/main/kotlin/net/corda/node/migration/VaultStateMigration.kt index dad25cf69f..28d8dc3a89 100644 --- a/node/src/main/kotlin/net/corda/node/migration/VaultStateMigration.kt +++ b/node/src/main/kotlin/net/corda/node/migration/VaultStateMigration.kt @@ -2,7 +2,6 @@ package net.corda.node.migration import liquibase.database.Database import net.corda.core.contracts.* -import net.corda.core.crypto.SecureHash import net.corda.core.identity.CordaX500Name import net.corda.core.node.services.Vault import net.corda.core.schemas.MappedSchema @@ -19,6 +18,7 @@ import net.corda.node.services.persistence.DBTransactionStorage import net.corda.node.services.persistence.NodeAttachmentService import net.corda.node.services.vault.NodeVaultService import net.corda.node.services.vault.VaultSchemaV1 +import net.corda.node.services.vault.toStateRef import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.DatabaseTransaction import net.corda.nodeapi.internal.persistence.SchemaMigration @@ -62,8 +62,7 @@ class VaultStateMigration : CordaMigration() { private fun getStateAndRef(persistentState: VaultSchemaV1.VaultStates): StateAndRef { val persistentStateRef = persistentState.stateRef ?: throw VaultStateMigrationException("Persistent state ref missing from state") - val txHash = SecureHash.create(persistentStateRef.txId) - val stateRef = StateRef(txHash, persistentStateRef.index) + val stateRef = persistentStateRef.toStateRef() val state = try { servicesForResolution.loadState(stateRef) } catch (e: Exception) { diff --git a/node/src/main/kotlin/net/corda/node/services/events/PersistentScheduledFlowRepository.kt b/node/src/main/kotlin/net/corda/node/services/events/PersistentScheduledFlowRepository.kt index 2208eef88f..f62db2eee4 100644 --- a/node/src/main/kotlin/net/corda/node/services/events/PersistentScheduledFlowRepository.kt +++ b/node/src/main/kotlin/net/corda/node/services/events/PersistentScheduledFlowRepository.kt @@ -2,8 +2,8 @@ package net.corda.node.services.events import net.corda.core.contracts.ScheduledStateRef import net.corda.core.contracts.StateRef -import net.corda.core.crypto.SecureHash import net.corda.core.schemas.PersistentStateRef +import net.corda.node.services.vault.toStateRef import net.corda.nodeapi.internal.persistence.CordaPersistence interface ScheduledFlowRepository { @@ -25,9 +25,8 @@ class PersistentScheduledFlowRepository(val database: CordaPersistence) : Schedu } private fun fromPersistentEntity(scheduledStateRecord: NodeSchedulerService.PersistentScheduledState): Pair { - val txId = scheduledStateRecord.output.txId - val index = scheduledStateRecord.output.index - return Pair(StateRef(SecureHash.create(txId), index), ScheduledStateRef(StateRef(SecureHash.create(txId), index), scheduledStateRecord.scheduledAt)) + val stateRef = scheduledStateRecord.output.toStateRef() + return Pair(stateRef, ScheduledStateRef(stateRef, scheduledStateRecord.scheduledAt)) } override fun delete(key: StateRef): Boolean { diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/PersistentUniquenessProvider.kt b/node/src/main/kotlin/net/corda/node/services/transactions/PersistentUniquenessProvider.kt index 66ec2007fa..aa69d50db3 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/PersistentUniquenessProvider.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/PersistentUniquenessProvider.kt @@ -25,6 +25,7 @@ import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.serialization.serialize import net.corda.core.utilities.contextLogger import net.corda.core.utilities.debug +import net.corda.node.services.vault.toStateRef import net.corda.node.utilities.AppendOnlyPersistentMap import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.NODE_DATABASE_PREFIX @@ -157,13 +158,7 @@ class PersistentUniquenessProvider(val clock: Clock, val database: CordaPersiste toPersistentEntityKey = { PersistentStateRef(it.txhash.toString(), it.index) }, fromPersistentEntity = { //TODO null check will become obsolete after making DB/JPA columns not nullable - val txId = it.id.txId - val index = it.id.index - Pair( - StateRef(txhash = SecureHash.create(txId), index = index), - SecureHash.create(it.consumingTxHash) - ) - + Pair(it.id.toStateRef(), SecureHash.create(it.consumingTxHash)) }, toPersistentEntity = { (txHash, index): StateRef, id: SecureHash -> CommittedState( diff --git a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt index 6db962cdce..5109bb0d3e 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt @@ -3,28 +3,65 @@ package net.corda.node.services.vault import co.paralleluniverse.fibers.Suspendable import co.paralleluniverse.strands.Strand import net.corda.core.CordaRuntimeException -import net.corda.core.contracts.* +import net.corda.core.contracts.Amount +import net.corda.core.contracts.ContractState +import net.corda.core.contracts.FungibleAsset +import net.corda.core.contracts.FungibleState +import net.corda.core.contracts.Issued +import net.corda.core.contracts.OwnableState +import net.corda.core.contracts.StateAndRef +import net.corda.core.contracts.StateRef +import net.corda.core.contracts.TransactionState import net.corda.core.crypto.SecureHash import net.corda.core.crypto.containsAny import net.corda.core.flows.HospitalizeFlowException -import net.corda.core.internal.* +import net.corda.core.internal.ThreadBox +import net.corda.core.internal.TransactionDeserialisationException +import net.corda.core.internal.VisibleForTesting +import net.corda.core.internal.bufferUntilSubscribed +import net.corda.core.internal.tee +import net.corda.core.internal.uncheckedCast import net.corda.core.messaging.DataFeed -import net.corda.core.node.ServicesForResolution import net.corda.core.node.StatesToRecord -import net.corda.core.node.services.* -import net.corda.core.node.services.Vault.ConstraintInfo.Companion.constraintInfo -import net.corda.core.node.services.vault.* +import net.corda.core.node.services.KeyManagementService +import net.corda.core.node.services.StatesNotAvailableException +import net.corda.core.node.services.Vault +import net.corda.core.node.services.VaultQueryException +import net.corda.core.node.services.VaultService +import net.corda.core.node.services.queryBy +import net.corda.core.node.services.vault.DEFAULT_PAGE_NUM +import net.corda.core.node.services.vault.DEFAULT_PAGE_SIZE +import net.corda.core.node.services.vault.PageSpecification +import net.corda.core.node.services.vault.QueryCriteria +import net.corda.core.node.services.vault.Sort +import net.corda.core.node.services.vault.SortAttribute +import net.corda.core.node.services.vault.builder import net.corda.core.observable.internal.OnResilientSubscribe import net.corda.core.schemas.PersistentStateRef import net.corda.core.serialization.SingletonSerializeAsToken -import net.corda.core.transactions.* -import net.corda.core.utilities.* +import net.corda.core.transactions.ContractUpgradeWireTransaction +import net.corda.core.transactions.CoreTransaction +import net.corda.core.transactions.FullTransaction +import net.corda.core.transactions.LedgerTransaction +import net.corda.core.transactions.NotaryChangeWireTransaction +import net.corda.core.transactions.WireTransaction +import net.corda.core.utilities.NonEmptySet +import net.corda.core.utilities.contextLogger +import net.corda.core.utilities.debug +import net.corda.core.utilities.toNonEmptySet +import net.corda.core.utilities.trace +import net.corda.node.internal.NodeServicesForResolution import net.corda.node.services.api.SchemaService import net.corda.node.services.api.VaultServiceInternal import net.corda.node.services.schema.PersistentStateService import net.corda.node.services.statemachine.FlowStateMachineImpl -import net.corda.nodeapi.internal.persistence.* +import net.corda.nodeapi.internal.persistence.CordaPersistence +import net.corda.nodeapi.internal.persistence.bufferUntilDatabaseCommit +import net.corda.nodeapi.internal.persistence.contextTransactionOrNull +import net.corda.nodeapi.internal.persistence.currentDBSession +import net.corda.nodeapi.internal.persistence.wrapWithDatabaseTransaction import org.hibernate.Session +import org.hibernate.query.Query import rx.Observable import rx.exceptions.OnErrorNotImplementedException import rx.subjects.PublishSubject @@ -32,7 +69,8 @@ import java.security.PublicKey import java.sql.SQLException import java.time.Clock import java.time.Instant -import java.util.* +import java.util.Arrays +import java.util.UUID import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.CopyOnWriteArraySet import javax.persistence.PersistenceException @@ -54,9 +92,9 @@ import javax.persistence.criteria.Root class NodeVaultService( private val clock: Clock, private val keyManagementService: KeyManagementService, - private val servicesForResolution: ServicesForResolution, + private val servicesForResolution: NodeServicesForResolution, private val database: CordaPersistence, - private val schemaService: SchemaService, + schemaService: SchemaService, private val appClassloader: ClassLoader ) : SingletonSerializeAsToken(), VaultServiceInternal { companion object { @@ -196,7 +234,7 @@ class NodeVaultService( if (lockId != null) { lockId = null lockUpdateTime = clock.instant() - log.trace("Releasing soft lock on consumed state: $stateRef") + log.trace { "Releasing soft lock on consumed state: $stateRef" } } session.save(state) } @@ -227,7 +265,7 @@ class NodeVaultService( } // we are not inside a flow, we are most likely inside a CordaService; // we will expose, by default, subscribing of -non unsubscribing- rx.Observers to rawUpdates. - return _rawUpdatesPublisher.resilientOnError() + _rawUpdatesPublisher.resilientOnError() } override val updates: Observable> @@ -639,7 +677,20 @@ class NodeVaultService( @Throws(VaultQueryException::class) override fun _queryBy(criteria: QueryCriteria, paging: PageSpecification, sorting: Sort, contractStateType: Class): Vault.Page { try { - return _queryBy(criteria, paging, sorting, contractStateType, false) + // We decrement by one if the client requests MAX_VALUE, assuming they can not notice this because they don't have enough memory + // to request MAX_VALUE states at once. + val validPaging = if (paging.pageSize == Integer.MAX_VALUE) { + paging.copy(pageSize = Integer.MAX_VALUE - 1) + } else { + checkVaultQuery(paging.pageSize >= 1) { "Page specification: invalid page size ${paging.pageSize} [minimum is 1]" } + paging + } + if (!validPaging.isDefault) { + checkVaultQuery(validPaging.pageNumber >= DEFAULT_PAGE_NUM) { + "Page specification: invalid page number ${validPaging.pageNumber} [page numbers start from $DEFAULT_PAGE_NUM]" + } + } + return queryBy(criteria, validPaging, sorting, contractStateType) } catch (e: VaultQueryException) { throw e } catch (e: Exception) { @@ -647,100 +698,100 @@ class NodeVaultService( } } - @Throws(VaultQueryException::class) - private fun _queryBy(criteria: QueryCriteria, paging_: PageSpecification, sorting: Sort, contractStateType: Class, skipPagingChecks: Boolean): Vault.Page { - // We decrement by one if the client requests MAX_PAGE_SIZE, assuming they can not notice this because they don't have enough memory - // to request `MAX_PAGE_SIZE` states at once. - val paging = if (paging_.pageSize == Integer.MAX_VALUE) { - paging_.copy(pageSize = Integer.MAX_VALUE - 1) - } else { - paging_ - } + private fun queryBy(criteria: QueryCriteria, + paging: PageSpecification, + sorting: Sort, + contractStateType: Class): Vault.Page { log.debug { "Vault Query for contract type: $contractStateType, criteria: $criteria, pagination: $paging, sorting: $sorting" } return database.transaction { // calculate total results where a page specification has been defined - var totalStates = -1L - if (!skipPagingChecks && !paging.isDefault) { - val count = builder { VaultSchemaV1.VaultStates::recordedTime.count() } - val countCriteria = QueryCriteria.VaultCustomQueryCriteria(count, Vault.StateStatus.ALL) - val results = _queryBy(criteria.and(countCriteria), PageSpecification(), Sort(emptyList()), contractStateType, true) // only skip pagination checks for total results count query - totalStates = results.otherResults.last() as Long - } + val totalStatesAvailable = if (paging.isDefault) -1 else queryTotalStateCount(criteria, contractStateType) - val session = getSession() - - val criteriaQuery = criteriaBuilder.createQuery(Tuple::class.java) - val queryRootVaultStates = criteriaQuery.from(VaultSchemaV1.VaultStates::class.java) - - // TODO: revisit (use single instance of parser for all queries) - val criteriaParser = HibernateQueryCriteriaParser(contractStateType, contractStateTypeMappings, criteriaBuilder, criteriaQuery, queryRootVaultStates) - - // parse criteria and build where predicates - criteriaParser.parse(criteria, sorting) - - // prepare query for execution - val query = session.createQuery(criteriaQuery) - - // pagination checks - if (!skipPagingChecks && !paging.isDefault) { - // pagination - if (paging.pageNumber < DEFAULT_PAGE_NUM) throw VaultQueryException("Page specification: invalid page number ${paging.pageNumber} [page numbers start from $DEFAULT_PAGE_NUM]") - if (paging.pageSize < 1) throw VaultQueryException("Page specification: invalid page size ${paging.pageSize} [minimum is 1]") - if (paging.pageSize > MAX_PAGE_SIZE) throw VaultQueryException("Page specification: invalid page size ${paging.pageSize} [maximum is $MAX_PAGE_SIZE]") - } - - // For both SQLServer and PostgresSQL, firstResult must be >= 0. So we set a floor at 0. - // TODO: This is a catch-all solution. But why is the default pageNumber set to be -1 in the first place? - // Even if we set the default pageNumber to be 1 instead, that may not cover the non-default cases. - // So the floor may be necessary anyway. - query.firstResult = maxOf(0, (paging.pageNumber - 1) * paging.pageSize) - val pageSize = paging.pageSize + 1 - query.maxResults = if (pageSize > 0) pageSize else Integer.MAX_VALUE // detection too many results, protected against overflow + val (query, stateTypes) = createQuery(criteria, contractStateType, sorting) + query.setResultWindow(paging) // execution val results = query.resultList // final pagination check (fail-fast on too many results when no pagination specified) - if (!skipPagingChecks && paging.isDefault && results.size > DEFAULT_PAGE_SIZE) { - throw VaultQueryException("There are ${results.size} results, which exceeds the limit of $DEFAULT_PAGE_SIZE for queries that do not specify paging. In order to retrieve these results, provide a `PageSpecification(pageNumber, pageSize)` to the method invoked.") + checkVaultQuery(!paging.isDefault || results.size != paging.pageSize + 1) { + "There are more results than the limit of $DEFAULT_PAGE_SIZE for queries that do not specify paging. " + + "In order to retrieve these results, provide a PageSpecification to the method invoked." } - val statesAndRefs: MutableList> = mutableListOf() - val statesMeta: MutableList = mutableListOf() + + val resultsIterator = results.iterator() + + // From page 2 and onwards, the first result is the previous page anchor + val previousPageAnchor = if (paging.pageNumber > DEFAULT_PAGE_NUM && resultsIterator.hasNext()) { + val previousVaultState = resultsIterator.next()[0] as VaultSchemaV1.VaultStates + previousVaultState.stateRef!!.toStateRef() + } else { + null + } + + val statesMetadata: MutableList = mutableListOf() val otherResults: MutableList = mutableListOf() - val stateRefs = mutableSetOf() - results.asSequence() - .forEachIndexed { index, result -> - if (result[0] is VaultSchemaV1.VaultStates) { - if (!paging.isDefault && index == paging.pageSize) // skip last result if paged - return@forEachIndexed - val vaultState = result[0] as VaultSchemaV1.VaultStates - val stateRef = StateRef(SecureHash.create(vaultState.stateRef!!.txId), vaultState.stateRef!!.index) - stateRefs.add(stateRef) - statesMeta.add(Vault.StateMetadata(stateRef, - vaultState.contractStateClassName, - vaultState.recordedTime, - vaultState.consumedTime, - vaultState.stateStatus, - vaultState.notary, - vaultState.lockId, - vaultState.lockUpdateTime, - vaultState.relevancyStatus, - constraintInfo(vaultState.constraintType, vaultState.constraintData) - )) - } else { - // TODO: improve typing of returned other results - log.debug { "OtherResults: ${Arrays.toString(result.toArray())}" } - otherResults.addAll(result.toArray().asList()) - } - } - if (stateRefs.isNotEmpty()) - statesAndRefs.addAll(uncheckedCast(servicesForResolution.loadStates(stateRefs))) + for (result in resultsIterator) { + val result0 = result[0] + if (result0 is VaultSchemaV1.VaultStates) { + statesMetadata.add(result0.toStateMetadata()) + } else { + log.debug { "OtherResults: ${Arrays.toString(result.toArray())}" } + otherResults.addAll(result.toArray().asList()) + } + } - Vault.Page(states = statesAndRefs, statesMetadata = statesMeta, stateTypes = criteriaParser.stateTypes, totalStatesAvailable = totalStates, otherResults = otherResults) + val states: List> = servicesForResolution.loadStates( + statesMetadata.mapTo(LinkedHashSet()) { it.ref }, + ArrayList() + ) + + Vault.Page(states, statesMetadata, totalStatesAvailable, stateTypes, otherResults, previousPageAnchor) } } + private fun Query<*>.setResultWindow(paging: PageSpecification) { + // For both SQLServer and PostgresSQL, firstResult must be >= 0. + firstResult = 0 + if (paging.isDefault) { + // Peek ahead and see if there are more results in case pagination should be done + maxResults = paging.pageSize + 1 + } else if (paging.pageNumber == DEFAULT_PAGE_NUM) { + maxResults = paging.pageSize + } else { + // In addition to aligning the query to the correct result window for the page, also include the previous page's last + // result for the previousPageAnchor value. + firstResult = ((paging.pageNumber - 1) * paging.pageSize) - 1 + maxResults = paging.pageSize + 1 + } + } + + private fun queryTotalStateCount(baseCriteria: QueryCriteria, contractStateType: Class): Long { + val count = builder { VaultSchemaV1.VaultStates::recordedTime.count() } + val countCriteria = QueryCriteria.VaultCustomQueryCriteria(count, Vault.StateStatus.ALL) + val criteria = baseCriteria.and(countCriteria) + val (query) = createQuery(criteria, contractStateType, null) + val results = query.resultList + return results.last().toArray().last() as Long + } + + private fun createQuery(criteria: QueryCriteria, + contractStateType: Class, + sorting: Sort?): Pair, Vault.StateStatus> { + val criteriaQuery = criteriaBuilder.createQuery(Tuple::class.java) + val criteriaParser = HibernateQueryCriteriaParser( + contractStateType, + contractStateTypeMappings, + criteriaBuilder, + criteriaQuery, + criteriaQuery.from(VaultSchemaV1.VaultStates::class.java) + ) + criteriaParser.parse(criteria, sorting) + val query = getSession().createQuery(criteriaQuery) + return Pair(query, criteriaParser.stateTypes) + } + /** * Returns a [DataFeed] containing the results of the provided query, along with the associated observable, containing any subsequent updates. * @@ -775,6 +826,12 @@ class NodeVaultService( } } + private inline fun checkVaultQuery(value: Boolean, lazyMessage: () -> Any) { + if (!value) { + throw VaultQueryException(lazyMessage().toString()) + } + } + private fun filterContractStates(update: Vault.Update, contractStateType: Class) = update.copy(consumed = filterByContractState(contractStateType, update.consumed), produced = filterByContractState(contractStateType, update.produced)) @@ -802,6 +859,7 @@ class NodeVaultService( } private fun getSession() = database.currentOrNew().session + /** * Derive list from existing vault states and then incrementally update using vault observables */ diff --git a/node/src/main/kotlin/net/corda/node/services/vault/VaultSchema.kt b/node/src/main/kotlin/net/corda/node/services/vault/VaultSchema.kt index 06844d40d0..09c71fe1f7 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/VaultSchema.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/VaultSchema.kt @@ -2,7 +2,9 @@ package net.corda.node.services.vault import net.corda.core.contracts.ContractState import net.corda.core.contracts.MAX_ISSUER_REF_SIZE +import net.corda.core.contracts.StateRef import net.corda.core.contracts.UniqueIdentifier +import net.corda.core.crypto.SecureHash import net.corda.core.crypto.toStringShort import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party @@ -192,3 +194,19 @@ object VaultSchemaV1 : MappedSchema( ) : IndirectStatePersistable } +fun PersistentStateRef.toStateRef(): StateRef = StateRef(SecureHash.create(txId), index) + +fun VaultSchemaV1.VaultStates.toStateMetadata(): Vault.StateMetadata { + return Vault.StateMetadata( + stateRef!!.toStateRef(), + contractStateClassName, + recordedTime, + consumedTime, + stateStatus, + notary, + lockId, + lockUpdateTime, + relevancyStatus, + Vault.ConstraintInfo.constraintInfo(constraintType, constraintData) + ) +} diff --git a/node/src/main/kotlin/net/corda/notary/experimental/bftsmart/BFTSmartNotaryService.kt b/node/src/main/kotlin/net/corda/notary/experimental/bftsmart/BFTSmartNotaryService.kt index a570ccd7b5..76094c2a1d 100644 --- a/node/src/main/kotlin/net/corda/notary/experimental/bftsmart/BFTSmartNotaryService.kt +++ b/node/src/main/kotlin/net/corda/notary/experimental/bftsmart/BFTSmartNotaryService.kt @@ -21,6 +21,7 @@ import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.unwrap import net.corda.node.services.api.ServiceHubInternal import net.corda.node.services.transactions.PersistentUniquenessProvider +import net.corda.node.services.vault.toStateRef import net.corda.node.utilities.AppendOnlyPersistentMap import net.corda.nodeapi.internal.persistence.NODE_DATABASE_PREFIX import java.security.PublicKey @@ -41,6 +42,8 @@ class BFTSmartNotaryService( ) : NotaryService() { companion object { private val log = contextLogger() + + @Suppress("unused") // Used by NotaryLoader via reflection @JvmStatic val serializationFilter get() = { clazz: Class<*> -> @@ -147,12 +150,7 @@ class BFTSmartNotaryService( toPersistentEntityKey = { PersistentStateRef(it.txhash.toString(), it.index) }, fromPersistentEntity = { //TODO null check will become obsolete after making DB/JPA columns not nullable - val txId = it.id.txId - val index = it.id.index - Pair( - StateRef(txhash = SecureHash.create(txId), index = index), - SecureHash.create(it.consumingTxHash) - ) + Pair(it.id.toStateRef(), SecureHash.create(it.consumingTxHash)) }, toPersistentEntity = { (txHash, index): StateRef, id: SecureHash -> CommittedState( diff --git a/node/src/main/kotlin/net/corda/notary/jpa/JPAUniquenessProvider.kt b/node/src/main/kotlin/net/corda/notary/jpa/JPAUniquenessProvider.kt index d38a3f35b7..b678478da6 100644 --- a/node/src/main/kotlin/net/corda/notary/jpa/JPAUniquenessProvider.kt +++ b/node/src/main/kotlin/net/corda/notary/jpa/JPAUniquenessProvider.kt @@ -24,6 +24,7 @@ import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.serialization.serialize import net.corda.core.utilities.contextLogger import net.corda.core.utilities.debug +import net.corda.node.services.vault.toStateRef import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.NODE_DATABASE_PREFIX import net.corda.notary.common.InternalResult @@ -142,10 +143,6 @@ class JPAUniquenessProvider( fun encodeStateRef(s: StateRef): PersistentStateRef { return PersistentStateRef(s.txhash.toString(), s.index) } - - fun decodeStateRef(s: PersistentStateRef): StateRef { - return StateRef(txhash = SecureHash.create(s.txId), index = s.index) - } } /** @@ -215,15 +212,15 @@ class JPAUniquenessProvider( committedStates.addAll(existing) } - return committedStates.map { - val stateRef = StateRef(txhash = SecureHash.create(it.id.txId), index = it.id.index) + return committedStates.associate { + val stateRef = it.id.toStateRef() val consumingTxId = SecureHash.create(it.consumingTxHash) if (stateRef in references) { stateRef to StateConsumptionDetails(consumingTxId.reHash(), type = StateConsumptionDetails.ConsumedStateType.REFERENCE_INPUT_STATE) } else { stateRef to StateConsumptionDetails(consumingTxId.reHash()) } - }.toMap() + } } private fun withRetry(block: () -> T): T { diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/HibernateConfigurationTest.kt b/node/src/test/kotlin/net/corda/node/services/persistence/HibernateConfigurationTest.kt index 1efb349ca0..30cdbe7f59 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/HibernateConfigurationTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/HibernateConfigurationTest.kt @@ -28,12 +28,14 @@ import net.corda.finance.schemas.CashSchemaV1 import net.corda.finance.test.SampleCashSchemaV1 import net.corda.finance.test.SampleCashSchemaV2 import net.corda.finance.test.SampleCashSchemaV3 +import net.corda.node.internal.NodeServicesForResolution import net.corda.node.services.api.WritableTransactionStorage import net.corda.node.services.schema.ContractStateAndRef import net.corda.node.services.schema.NodeSchemaService import net.corda.node.services.schema.PersistentStateService import net.corda.node.services.vault.NodeVaultService import net.corda.node.services.vault.VaultSchemaV1 +import net.corda.node.services.vault.toStateRef import net.corda.node.testing.DummyFungibleContract import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.DatabaseConfig @@ -48,7 +50,6 @@ import net.corda.testing.internal.vault.VaultFiller import net.corda.testing.node.MockServices import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties import org.assertj.core.api.Assertions -import org.assertj.core.api.Assertions.`in` import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatThrownBy import org.hibernate.SessionFactory @@ -122,7 +123,14 @@ class HibernateConfigurationTest { services = object : MockServices(cordappPackages, BOB_NAME, mock().also { doReturn(null).whenever(it).verifyAndRegisterIdentity(argThat { name == BOB_NAME }) }, generateKeyPair(), dummyNotary.keyPair) { - override val vaultService = NodeVaultService(Clock.systemUTC(), keyManagementService, servicesForResolution, database, schemaService, cordappClassloader).apply { start() } + override val vaultService = NodeVaultService( + Clock.systemUTC(), + keyManagementService, + servicesForResolution as NodeServicesForResolution, + database, + schemaService, + cordappClassloader + ).apply { start() } override fun recordTransactions(statesToRecord: StatesToRecord, txs: Iterable) { for (stx in txs) { (validatedTransactions as WritableTransactionStorage).addTransaction(stx) @@ -183,7 +191,7 @@ class HibernateConfigurationTest { // execute query val queryResults = entityManager.createQuery(criteriaQuery).resultList val coins = queryResults.map { - services.loadState(toStateRef(it.stateRef!!)).data + services.loadState(it.stateRef!!.toStateRef()).data }.sumCash() assertThat(coins.toDecimal() >= BigDecimal("50.00")) } @@ -739,7 +747,7 @@ class HibernateConfigurationTest { val queryResults = entityManager.createQuery(criteriaQuery).resultList queryResults.forEach { - val cashState = services.loadState(toStateRef(it.stateRef!!)).data as Cash.State + val cashState = services.loadState(it.stateRef!!.toStateRef()).data as Cash.State println("${it.stateRef} with owner: ${cashState.owner.owningKey.toBase58String()}") } @@ -823,7 +831,7 @@ class HibernateConfigurationTest { // execute query val queryResults = entityManager.createQuery(criteriaQuery).resultList queryResults.forEach { - val cashState = services.loadState(toStateRef(it.stateRef!!)).data as Cash.State + val cashState = services.loadState(it.stateRef!!.toStateRef()).data as Cash.State println("${it.stateRef} with owner ${cashState.owner.owningKey.toBase58String()} and participants ${cashState.participants.map { it.owningKey.toBase58String() }}") } @@ -961,10 +969,6 @@ class HibernateConfigurationTest { } } - private fun toStateRef(pStateRef: PersistentStateRef): StateRef { - return StateRef(SecureHash.create(pStateRef.txId), pStateRef.index) - } - @Test(timeout=300_000) fun `schema change`() { fun createNewDB(schemas: Set, initialiseSchema: Boolean = true): CordaPersistence { diff --git a/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt b/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt index 09964b6602..6344331c28 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt @@ -20,7 +20,6 @@ import net.corda.finance.* import net.corda.finance.contracts.CommercialPaper import net.corda.finance.contracts.Commodity import net.corda.finance.contracts.DealState -import net.corda.finance.workflows.asset.selection.AbstractCashSelection import net.corda.finance.contracts.asset.Cash import net.corda.finance.schemas.CashSchemaV1 import net.corda.finance.schemas.CashSchemaV1.PersistentCashState @@ -28,6 +27,7 @@ import net.corda.finance.schemas.CommercialPaperSchemaV1 import net.corda.finance.test.SampleCashSchemaV2 import net.corda.finance.test.SampleCashSchemaV3 import net.corda.finance.workflows.CommercialPaperUtils +import net.corda.finance.workflows.asset.selection.AbstractCashSelection import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.nodeapi.internal.persistence.DatabaseTransaction @@ -1669,22 +1669,28 @@ abstract class VaultQueryTestsBase : VaultQueryParties { // DOCEND VaultQueryExample7 assertThat(results.states).hasSize(10) assertThat(results.totalStatesAvailable).isEqualTo(100) + assertThat(results.previousPageAnchor).isNull() } } // pagination: last page @Test(timeout=300_000) - fun `all states with paging specification - last`() { + fun `all states with paging specification - last`() { database.transaction { vaultFiller.fillWithSomeTestCash(95.DOLLARS, notaryServices, 95, DUMMY_CASH_ISSUER) + val criteria = VaultQueryCriteria(status = Vault.StateStatus.ALL) + // Last page implies we need to perform a row count for the Query first, // and then re-query for a given offset defined by (count - pageSize) - val pagingSpec = PageSpecification(10, 10) + val lastPage = PageSpecification(10, 10) + val lastPageResults = vaultService.queryBy(criteria, paging = lastPage) + assertThat(lastPageResults.states).hasSize(5) // should retrieve states 90..94 + assertThat(lastPageResults.totalStatesAvailable).isEqualTo(95) - val criteria = VaultQueryCriteria(status = Vault.StateStatus.ALL) - val results = vaultService.queryBy(criteria, paging = pagingSpec) - assertThat(results.states).hasSize(5) // should retrieve states 90..94 - assertThat(results.totalStatesAvailable).isEqualTo(95) + // Make sure the previousPageAnchor points to the previous page's last result + val penultimatePage = lastPage.copy(pageNumber = lastPage.pageNumber - 1) + val penultimatePageResults = vaultService.queryBy(criteria, paging = penultimatePage) + assertThat(lastPageResults.previousPageAnchor).isEqualTo(penultimatePageResults.statesMetadata.last().ref) } } @@ -1723,7 +1729,7 @@ abstract class VaultQueryTestsBase : VaultQueryParties { @Test(timeout=300_000) fun `pagination not specified but more than default results available`() { expectedEx.expect(VaultQueryException::class.java) - expectedEx.expectMessage("provide a `PageSpecification(pageNumber, pageSize)`") + expectedEx.expectMessage("provide a PageSpecification") database.transaction { vaultFiller.fillWithSomeTestCash(201.DOLLARS, notaryServices, 201, DUMMY_CASH_ISSUER) @@ -1735,16 +1741,23 @@ abstract class VaultQueryTestsBase : VaultQueryParties { // example of querying states with paging using totalStatesAvailable private fun queryStatesWithPaging(vaultService: VaultService, pageSize: Int): List> { // DOCSTART VaultQueryExample24 - var pageNumber = DEFAULT_PAGE_NUM - val states = mutableListOf>() - do { - val pageSpec = PageSpecification(pageNumber = pageNumber, pageSize = pageSize) - val results = vaultService.queryBy(VaultQueryCriteria(), pageSpec) - states.addAll(results.states) - pageNumber++ - } while ((pageSpec.pageSize * (pageNumber - 1)) <= results.totalStatesAvailable) + retry@ + while (true) { + var pageNumber = DEFAULT_PAGE_NUM + val states = mutableListOf>() + do { + val pageSpec = PageSpecification(pageNumber = pageNumber, pageSize = pageSize) + val results = vaultService.queryBy(VaultQueryCriteria(), pageSpec) + if (results.previousPageAnchor != states.lastOrNull()?.ref) { + // Start querying from the 1st page again if we find the vault has changed from underneath us. + continue@retry + } + states.addAll(results.states) + pageNumber++ + } while ((pageSpec.pageSize * (pageNumber - 1)) <= results.totalStatesAvailable) + return states + } // DOCEND VaultQueryExample24 - return states.toList() } // test paging query example works @@ -1760,6 +1773,51 @@ abstract class VaultQueryTestsBase : VaultQueryParties { } } + @Test(timeout = 300_000) + fun `detecting changes to the database whilst pages are loaded`() { + val criteria = VaultQueryCriteria() + val sorting = Sort(setOf(Sort.SortColumn(SortAttribute.Standard(Sort.LinearStateAttribute.EXTERNAL_ID)))) + + fun loadPagesAndCheckAnchors(): List> { + val pages = (1..3).map { vaultService.queryBy(criteria, PageSpecification(it, 10), sorting) } + assertThat(pages[0].previousPageAnchor).isNull() + assertThat(pages[1].previousPageAnchor).isEqualTo(pages[0].states.last().ref) + assertThat(pages[2].previousPageAnchor).isEqualTo(pages[1].states.last().ref) + return pages + } + + database.transaction { + vaultFiller.fillWithSomeTestDeals(dealIds = (10..30).map(Int::toString)) + val pagesV1 = loadPagesAndCheckAnchors() + + vaultFiller.fillWithSomeTestDeals(dealIds = listOf("25.5")) // Insert a state into the middle of the second page + val pagesV2 = loadPagesAndCheckAnchors() + + assertThat(pagesV2[2].previousPageAnchor) + .isNotEqualTo(pagesV1[2].previousPageAnchor) // The previously loaded page is no longer in-sync + .isEqualTo(pagesV1[1].states.let { it[it.lastIndex - 1].ref }) // The anchor now points to the second to last entry + assertThat(pagesV2[1].previousPageAnchor).isEqualTo(pagesV1[1].previousPageAnchor) // The first page is unaffected + + vaultFiller.consumeDeals(pagesV1[0].states.take(1)) // Consume the first state + val pagesV3 = loadPagesAndCheckAnchors() + + assertThat(pagesV3[1].previousPageAnchor) + .isNotEqualTo(pagesV1[1].previousPageAnchor) // Now the first page is no longer in-sync + .isEqualTo(pagesV1[1].states[0].ref) // The top of the second page has now moved into the first page + + vaultFiller.consumeDeals(pagesV3[2].states) // Consume the entire third page + val pagesV4 = loadPagesAndCheckAnchors() + // There are now no states for the third page, but it will still have an anchor + assertThat(pagesV4[2].states).isEmpty() + + vaultFiller.consumeDeals(pagesV3[1].states.takeLast(1)) // Consume the third page anchor + + val thirdPageV5 = vaultService.queryBy(criteria, PageSpecification(3, 10), sorting) + assertThat(thirdPageV5.states).isEmpty() + assertThat(thirdPageV5.previousPageAnchor).isNull() + } + } + // test paging with aggregate function and group by clause @Test(timeout=300_000) fun `test paging with aggregate function and group by clause`() { diff --git a/node/src/test/kotlin/net/corda/node/services/vault/VaultSoftLockManagerTest.kt b/node/src/test/kotlin/net/corda/node/services/vault/VaultSoftLockManagerTest.kt index 7e771e9904..ac621c9bff 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/VaultSoftLockManagerTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/VaultSoftLockManagerTest.kt @@ -10,7 +10,6 @@ import net.corda.core.flows.InitiatingFlow import net.corda.core.identity.AbstractParty import net.corda.core.internal.FlowStateMachine import net.corda.core.internal.uncheckedCast -import net.corda.core.node.ServicesForResolution import net.corda.core.node.services.KeyManagementService import net.corda.core.node.services.queryBy import net.corda.core.node.services.vault.QueryCriteria.SoftLockingCondition @@ -29,6 +28,7 @@ import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.testing.core.singleIdentity import net.corda.testing.flows.registerCoreFlowFactory import net.corda.coretesting.internal.rigorousMock +import net.corda.node.internal.NodeServicesForResolution import net.corda.testing.node.internal.InternalMockNetwork import net.corda.testing.node.internal.enclosedCordapp import net.corda.testing.node.internal.startFlow @@ -86,7 +86,7 @@ class VaultSoftLockManagerTest { private val mockNet = InternalMockNetwork(cordappsForAllNodes = listOf(enclosedCordapp()), defaultFactory = { args -> object : InternalMockNetwork.MockNode(args) { override fun makeVaultService(keyManagementService: KeyManagementService, - services: ServicesForResolution, + services: NodeServicesForResolution, database: CordaPersistence, cordappLoader: CordappLoader): VaultServiceInternal { val node = this diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt index f8a369ca0d..7c19eb0265 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt @@ -44,6 +44,7 @@ import net.corda.core.utilities.NetworkHostAndPort import net.corda.coretesting.internal.DEV_ROOT_CA import net.corda.node.VersionInfo import net.corda.node.internal.ServicesForResolutionImpl +import net.corda.node.internal.NodeServicesForResolution import net.corda.node.internal.cordapp.JarScanningCordappLoader import net.corda.node.services.api.SchemaService import net.corda.node.services.api.ServiceHubInternal @@ -498,7 +499,14 @@ open class MockServices private constructor( get() = ServicesForResolutionImpl(identityService, attachments, cordappProvider, networkParametersService, validatedTransactions) internal fun makeVaultService(schemaService: SchemaService, database: CordaPersistence, cordappLoader: CordappLoader): VaultServiceInternal { - return NodeVaultService(clock, keyManagementService, servicesForResolution, database, schemaService, cordappLoader.appClassLoader).apply { start() } + return NodeVaultService( + clock, + keyManagementService, + servicesForResolution as NodeServicesForResolution, + database, + schemaService, + cordappLoader.appClassLoader + ).apply { start() } } // This needs to be internal as MutableClassToInstanceMap is a guava type and shouldn't be part of our public API From 2c775bcc41853522c0005946521b74eb7a1a341d Mon Sep 17 00:00:00 2001 From: Jose Coll Date: Fri, 2 Jun 2023 16:05:28 +0100 Subject: [PATCH 028/126] ENT-9924 Update recording of transaction flow recovery metadata into Send/Receive transaction flows. (#7374) --- .../coretests/flows/FinalityFlowTests.kt | 124 +++++++++++++++-- .../net/corda/core/flows/FinalityFlow.kt | 73 ++++++---- .../net/corda/core/flows/FlowTransaction.kt | 10 +- .../core/flows/ReceiveTransactionFlow.kt | 52 +++++-- .../corda/core/flows/SendTransactionFlow.kt | 42 +++++- .../core/internal/ServiceHubCoreInternal.kt | 16 ++- .../kotlin/net/corda/core/node/ServiceHub.kt | 2 + .../node/services/api/ServiceHubInternal.kt | 29 ++-- .../network/PersistentPartyInfoCache.kt | 1 - .../persistence/DBTransactionStorage.kt | 7 +- .../DBTransactionStorageLedgerRecovery.kt | 71 +++++----- .../node/messaging/TwoPartyTradeFlowTests.kt | 14 +- ...DBTransactionStorageLedgerRecoveryTests.kt | 129 +++++++++++++----- .../persistence/DBTransactionStorageTests.kt | 13 +- .../test/flows/CashIssueWithObserversFlow.kt | 18 ++- .../flows/CashPaymentWithObserversFlow.kt | 82 +++++++++++ .../node/internal/MockTransactionStorage.kt | 7 +- .../kotlin/net/corda/testing/dsl/TestDSL.kt | 7 +- 18 files changed, 530 insertions(+), 167 deletions(-) create mode 100644 testing/cordapps/cashobservers/src/main/kotlin/net/corda/finance/test/flows/CashPaymentWithObserversFlow.kt diff --git a/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt b/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt index 77a7731d39..35dc41dadd 100644 --- a/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt +++ b/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt @@ -10,6 +10,7 @@ import net.corda.core.contracts.StateAndRef import net.corda.core.contracts.TransactionVerificationException import net.corda.core.crypto.SecureHash import net.corda.core.crypto.TransactionSignature +import net.corda.core.flows.DistributionList import net.corda.core.flows.FinalityFlow import net.corda.core.flows.FlowException import net.corda.core.flows.FlowLogic @@ -26,6 +27,7 @@ import net.corda.core.flows.StartableByRPC import net.corda.core.flows.TransactionMetadata import net.corda.core.flows.TransactionStatus import net.corda.core.flows.UnexpectedFlowEndException +import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party import net.corda.core.internal.FetchDataFlow import net.corda.core.internal.PLATFORM_VERSION @@ -47,9 +49,11 @@ import net.corda.finance.flows.CashIssueFlow import net.corda.finance.flows.CashPaymentFlow import net.corda.finance.issuedBy import net.corda.finance.test.flows.CashIssueWithObserversFlow +import net.corda.finance.test.flows.CashPaymentWithObserversFlow import net.corda.node.services.persistence.DBTransactionStorage import net.corda.node.services.persistence.DBTransactionStorageLedgerRecovery -import net.corda.node.services.persistence.DistributionRecord +import net.corda.node.services.persistence.ReceiverDistributionRecord +import net.corda.node.services.persistence.SenderDistributionRecord import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.testing.contracts.DummyContract import net.corda.testing.core.ALICE_NAME @@ -350,28 +354,120 @@ class FinalityFlowTests : WithFinality { assertThat(aliceNode.services.validatedTransactions.getTransaction(stx.id)).isNotNull assertThat(bobNode.services.validatedTransactions.getTransaction(stx.id)).isNotNull - assertThat(getSenderRecoveryData(stx.id, aliceNode.database)).isNotNull - assertThat(getReceiverRecoveryData(stx.id, bobNode.database)).isNotNull + getSenderRecoveryData(stx.id, aliceNode.database).apply { + assertEquals(1, this.size) + assertEquals(StatesToRecord.ONLY_RELEVANT, this[0].statesToRecord) + assertEquals(BOB_NAME.hashCode().toLong(), this[0].peerPartyId) + } + getReceiverRecoveryData(stx.id, bobNode.database).apply { + assertEquals(StatesToRecord.ALL_VISIBLE, this?.statesToRecord) + assertEquals(StatesToRecord.ONLY_RELEVANT, this?.senderStatesToRecord) + assertEquals(aliceNode.info.singleIdentity().name.hashCode().toLong(), this?.initiatorPartyId) + assertEquals(mapOf(BOB_NAME.hashCode().toLong() to StatesToRecord.ALL_VISIBLE), this?.peersToStatesToRecord) + } } - private fun getSenderRecoveryData(id: SecureHash, database: CordaPersistence): DistributionRecord? { + @Test(timeout=300_000) + fun `two phase finality flow payment transaction with observers`() { + val bobNode = createBob(platformVersion = PlatformVersionSwitches.TWO_PHASE_FINALITY) + val charlieNode = createNode(CHARLIE_NAME, platformVersion = PlatformVersionSwitches.TWO_PHASE_FINALITY) + + // issue some cash + aliceNode.startFlow(CashIssueFlow(Amount(1000L, GBP), OpaqueBytes.of(1), notary)).resultFuture.getOrThrow().stx + + // standard issuance with observers passed in as FinalityFlow sessions + val stx = aliceNode.startFlowAndRunNetwork(CashPaymentWithObserversFlow( + amount = Amount(100L, GBP), + recipient = bobNode.info.singleIdentity(), + observers = setOf(charlieNode.info.singleIdentity()))).resultFuture.getOrThrow() + + assertThat(aliceNode.services.validatedTransactions.getTransaction(stx.id)).isNotNull + assertThat(bobNode.services.validatedTransactions.getTransaction(stx.id)).isNotNull + assertThat(charlieNode.services.validatedTransactions.getTransaction(stx.id)).isNotNull + + getSenderRecoveryData(stx.id, aliceNode.database).apply { + assertEquals(2, this.size) + assertEquals(StatesToRecord.ONLY_RELEVANT, this[0].statesToRecord) + assertEquals(BOB_NAME.hashCode().toLong(), this[0].peerPartyId) + assertEquals(StatesToRecord.ONLY_RELEVANT, this[1].statesToRecord) + assertEquals(CHARLIE_NAME.hashCode().toLong(), this[1].peerPartyId) + } + getReceiverRecoveryData(stx.id, bobNode.database).apply { + assertEquals(StatesToRecord.ONLY_RELEVANT, this?.statesToRecord) + assertEquals(StatesToRecord.ONLY_RELEVANT, this?.senderStatesToRecord) + assertEquals(aliceNode.info.singleIdentity().name.hashCode().toLong(), this?.initiatorPartyId) + // note: Charlie assertion here is using the hinted StatesToRecord value passed to it from Alice + assertEquals(mapOf(BOB_NAME.hashCode().toLong() to StatesToRecord.ONLY_RELEVANT, + CHARLIE_NAME.hashCode().toLong() to StatesToRecord.ALL_VISIBLE), this?.peersToStatesToRecord) + } + getReceiverRecoveryData(stx.id, charlieNode.database).apply { + assertEquals(StatesToRecord.ONLY_RELEVANT, this?.statesToRecord) + assertEquals(StatesToRecord.ONLY_RELEVANT, this?.senderStatesToRecord) + // note: Charlie assertion here is using actually default StatesToRecord.ONLY_RELEVANT + assertEquals(mapOf(BOB_NAME.hashCode().toLong() to StatesToRecord.ONLY_RELEVANT, + CHARLIE_NAME.hashCode().toLong() to StatesToRecord.ONLY_RELEVANT), this?.peersToStatesToRecord) + } + + // exercise the new FinalityFlow observerSessions constructor parameter + val stx3 = aliceNode.startFlowAndRunNetwork(CashPaymentWithObserversFlow( + amount = Amount(100L, GBP), + recipient = bobNode.info.singleIdentity(), + observers = setOf(charlieNode.info.singleIdentity()), + useObserverSessions = true)).resultFuture.getOrThrow() + + assertThat(aliceNode.services.validatedTransactions.getTransaction(stx3.id)).isNotNull + assertThat(bobNode.services.validatedTransactions.getTransaction(stx3.id)).isNotNull + assertThat(charlieNode.services.validatedTransactions.getTransaction(stx3.id)).isNotNull + + assertEquals(2, getSenderRecoveryData(stx3.id, aliceNode.database).size) + assertThat(getReceiverRecoveryData(stx3.id, bobNode.database)).isNotNull + assertThat(getReceiverRecoveryData(stx3.id, charlieNode.database)).isNotNull + } + + @Test(timeout=300_000) + fun `two phase finality flow payment transaction using confidential identities`() { + val bobNode = createBob(platformVersion = PlatformVersionSwitches.TWO_PHASE_FINALITY) + + aliceNode.startFlow(CashIssueFlow(Amount(1000L, GBP), OpaqueBytes.of(1), notary)).resultFuture.getOrThrow().stx + val stx = aliceNode.startFlowAndRunNetwork(CashPaymentFlow( + amount = Amount(100L, GBP), + recipient = bobNode.info.singleIdentity(), + anonymous = true)).resultFuture.getOrThrow().stx + + assertThat(aliceNode.services.validatedTransactions.getTransaction(stx.id)).isNotNull + assertThat(bobNode.services.validatedTransactions.getTransaction(stx.id)).isNotNull + + getSenderRecoveryData(stx.id, aliceNode.database).apply { + assertEquals(1, this.size) + assertEquals(StatesToRecord.ONLY_RELEVANT, this[0].statesToRecord) + assertEquals(BOB_NAME.hashCode().toLong(), this[0].peerPartyId) + } + getReceiverRecoveryData(stx.id, bobNode.database).apply { + assertEquals(StatesToRecord.ONLY_RELEVANT, this?.statesToRecord) + assertEquals(StatesToRecord.ONLY_RELEVANT, this?.senderStatesToRecord) + assertEquals(aliceNode.info.singleIdentity().name.hashCode().toLong(), this?.initiatorPartyId) + assertEquals(mapOf(BOB_NAME.hashCode().toLong() to StatesToRecord.ONLY_RELEVANT), this?.peersToStatesToRecord) + } + } + + private fun getSenderRecoveryData(id: SecureHash, database: CordaPersistence): List { val fromDb = database.transaction { session.createQuery( "from ${DBTransactionStorageLedgerRecovery.DBSenderDistributionRecord::class.java.name} where tx_id = :transactionId", DBTransactionStorageLedgerRecovery.DBSenderDistributionRecord::class.java ).setParameter("transactionId", id.toString()).resultList.map { it } } - return fromDb.singleOrNull()?.toSenderDistributionRecord() + return fromDb.map { it.toSenderDistributionRecord() }.also { println("SenderDistributionRecord\n$it") } } - private fun getReceiverRecoveryData(id: SecureHash, database: CordaPersistence): DistributionRecord? { + private fun getReceiverRecoveryData(id: SecureHash, database: CordaPersistence): ReceiverDistributionRecord? { val fromDb = database.transaction { session.createQuery( "from ${DBTransactionStorageLedgerRecovery.DBReceiverDistributionRecord::class.java.name} where tx_id = :transactionId", DBTransactionStorageLedgerRecovery.DBReceiverDistributionRecord::class.java ).setParameter("transactionId", id.toString()).resultList.map { it } } - return fromDb.singleOrNull()?.toReceiverDistributionRecord(MockCryptoService(emptyMap())) + return fromDb.singleOrNull()?.toReceiverDistributionRecord(MockCryptoService(emptyMap())).also { println("ReceiverDistributionRecord\n$it") } } @StartableByRPC @@ -445,12 +541,10 @@ class FinalityFlowTests : WithFinality { override fun call(): SignedTransaction { // Mimic ReceiveFinalityFlow but fail to finalise try { - val stx = subFlow(ReceiveTransactionFlow(otherSideSession, - checkSufficientSignatures = false, statesToRecord = StatesToRecord.ONLY_RELEVANT, deferredAck = true)) + val stx = subFlow(ReceiveTransactionFlow(otherSideSession, false, StatesToRecord.ONLY_RELEVANT, true)) require(NotarySigCheck.needsNotarySignature(stx)) logger.info("Peer recording transaction without notary signature.") - (serviceHub as ServiceHubCoreInternal).recordUnnotarisedTransaction(stx, - TransactionMetadata(otherSideSession.counterparty.name, StatesToRecord.ONLY_RELEVANT)) + (serviceHub as ServiceHubCoreInternal).recordUnnotarisedTransaction(stx) otherSideSession.send(FetchDataFlow.Request.End) // Finish fetching data (overrideAutoAck) logger.info("Peer recorded transaction without notary signature.") @@ -494,7 +588,8 @@ class FinalityFlowTests : WithFinality { val txBuilder = DummyContract.move(stateAndRef, newOwner) val stxn = serviceHub.signInitialTransaction(txBuilder, ourIdentity.owningKey) val sessionWithCounterParty = initiateFlow(newOwner) - subFlow(SendTransactionFlow(sessionWithCounterParty, stxn)) + subFlow(SendTransactionFlow(sessionWithCounterParty, stxn, + TransactionMetadata(ourIdentity.name, DistributionList(StatesToRecord.ONLY_RELEVANT, mapOf(BOB_NAME to StatesToRecord.ONLY_RELEVANT))))) throw UnexpectedFlowEndException("${stxn.id}") } } @@ -514,6 +609,11 @@ class FinalityFlowTests : WithFinality { version = MOCK_VERSION_INFO.copy(platformVersion = platformVersion))) } + private fun createNode(legalName: CordaX500Name, cordapps: List = emptyList(), platformVersion: Int = PLATFORM_VERSION): TestStartedNode { + return mockNet.createNode(InternalMockNodeParameters(legalName = legalName, additionalCordapps = cordapps, + version = MOCK_VERSION_INFO.copy(platformVersion = platformVersion))) + } + private fun TestStartedNode.issuesCashTo(recipient: TestStartedNode): SignedTransaction { return issuesCashTo(recipient.info.singleIdentity()) } diff --git a/core/src/main/kotlin/net/corda/core/flows/FinalityFlow.kt b/core/src/main/kotlin/net/corda/core/flows/FinalityFlow.kt index 9219edccd4..4770244c72 100644 --- a/core/src/main/kotlin/net/corda/core/flows/FinalityFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/FinalityFlow.kt @@ -6,6 +6,7 @@ import net.corda.core.crypto.SecureHash import net.corda.core.crypto.TransactionSignature import net.corda.core.crypto.isFulfilledBy import net.corda.core.flows.NotarySigCheck.needsNotarySignature +import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party import net.corda.core.identity.groupAbstractPartyByWellKnownParty import net.corda.core.internal.FetchDataFlow @@ -15,6 +16,7 @@ import net.corda.core.internal.pushToLoggingContext import net.corda.core.internal.telemetry.telemetryServiceInternal import net.corda.core.internal.warnOnce import net.corda.core.node.StatesToRecord +import net.corda.core.node.StatesToRecord.ALL_VISIBLE import net.corda.core.node.StatesToRecord.ONLY_RELEVANT import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.SignedTransaction @@ -41,6 +43,9 @@ import java.time.Duration * can also be included, but they must specify [StatesToRecord.ALL_VISIBLE] for statesToRecord if they wish to record the * contract states into their vaults. * + * As of 4.11 a list of observer [FlowSession] can be specified to indicate sessions with transaction non-participants (e.g. observers). + * This enables ledger recovery to default these sessions associated StatesToRecord value to [StatesToRecord.ALL_VISIBLE]. + * * The flow returns the same transaction but with the additional signatures from the notary. * * NOTE: This is an inlined flow but for backwards compatibility is annotated with [InitiatingFlow]. @@ -55,13 +60,14 @@ class FinalityFlow private constructor(val transaction: SignedTransaction, override val progressTracker: ProgressTracker, private val sessions: Collection, private val newApi: Boolean, - private val statesToRecord: StatesToRecord = ONLY_RELEVANT) : FlowLogic() { + private val statesToRecord: StatesToRecord = ONLY_RELEVANT, + private val observerSessions: Collection = emptySet()) : FlowLogic() { @CordaInternal - data class ExtraConstructorArgs(val oldParticipants: Collection, val sessions: Collection, val newApi: Boolean, val statesToRecord: StatesToRecord) + data class ExtraConstructorArgs(val oldParticipants: Collection, val sessions: Collection, val newApi: Boolean, val statesToRecord: StatesToRecord, val observerSessions: Collection) @CordaInternal - fun getExtraConstructorArgs() = ExtraConstructorArgs(oldParticipants, sessions, newApi, statesToRecord) + fun getExtraConstructorArgs() = ExtraConstructorArgs(oldParticipants, sessions, newApi, statesToRecord, observerSessions) @Deprecated(DEPRECATION_MSG) constructor(transaction: SignedTransaction, extraRecipients: Set, progressTracker: ProgressTracker) : this( @@ -133,6 +139,10 @@ class FinalityFlow private constructor(val transaction: SignedTransaction, progressTracker: ProgressTracker ) : this(transaction, oldParticipants, progressTracker, sessions, true) + constructor(transaction: SignedTransaction, + sessions: Collection, + observerSessions: Collection) : this(transaction, emptyList(), tracker(), sessions, true, observerSessions = observerSessions) + companion object { private const val DEPRECATION_MSG = "It is unsafe to use this constructor as it requires nodes to automatically " + "accept notarised transactions without first checking their relevancy. Instead, use one of the constructors " + @@ -158,6 +168,9 @@ class FinalityFlow private constructor(val transaction: SignedTransaction, fun tracker() = ProgressTracker(RECORD_UNNOTARISED, BROADCASTING_PRE_NOTARISATION, NOTARISING, BROADCASTING_POST_NOTARISATION, BROADCASTING_NOTARY_ERROR, FINALISING_TRANSACTION, BROADCASTING) } + private lateinit var externalTxParticipants: Set + private lateinit var txnMetadata: TransactionMetadata + @Suspendable @Suppress("ComplexMethod", "NestedBlockDepth") @Throws(NotaryException::class) @@ -169,6 +182,9 @@ class FinalityFlow private constructor(val transaction: SignedTransaction, require(sessions.none { serviceHub.myInfo.isLegalIdentity(it.counterparty) }) { "Do not provide flow sessions for the local node. FinalityFlow will record the notarised transaction locally." } + sessions.intersect(observerSessions.toSet()).let { + require(it.isEmpty()) { "The following parties are specified both in flow sessions and observer flow sessions: $it" } + } } // Note: this method is carefully broken up to minimize the amount of data reachable from the stack at @@ -179,7 +195,7 @@ class FinalityFlow private constructor(val transaction: SignedTransaction, transaction.pushToLoggingContext() logCommandData() val ledgerTransaction = verifyTx() - val externalTxParticipants = extractExternalParticipants(ledgerTransaction) + externalTxParticipants = extractExternalParticipants(ledgerTransaction) if (newApi) { val sessionParties = sessions.map { it.counterparty }.toSet() @@ -199,12 +215,14 @@ class FinalityFlow private constructor(val transaction: SignedTransaction, // - broadcast notary signature to external participants (finalise remotely) // - finalise locally - val (oldPlatformSessions, newPlatformSessions) = sessions.partition { + val (oldPlatformSessions, newPlatformSessions) = (sessions + observerSessions).partition { serviceHub.networkMapCache.getNodeByLegalIdentity(it.counterparty)?.platformVersion!! < PlatformVersionSwitches.TWO_PHASE_FINALITY } val requiresNotarisation = needsNotarySignature(transaction) val useTwoPhaseFinality = serviceHub.myInfo.platformVersion >= PlatformVersionSwitches.TWO_PHASE_FINALITY + txnMetadata = TransactionMetadata(serviceHub.myInfo.legalIdentities.first().name, + DistributionList(statesToRecord, deriveStatesToRecord(newPlatformSessions))) if (useTwoPhaseFinality) { val stxn = if (requiresNotarisation) { recordLocallyAndBroadcast(newPlatformSessions, transaction) @@ -226,11 +244,7 @@ class FinalityFlow private constructor(val transaction: SignedTransaction, } else { if (newPlatformSessions.isNotEmpty()) - finaliseLocallyAndBroadcast(newPlatformSessions, transaction, - TransactionMetadata( - serviceHub.myInfo.legalIdentities.first().name, - statesToRecord, - sessions.map { it.counterparty.name }.toSet())) + finaliseLocallyAndBroadcast(newPlatformSessions, transaction) else recordTransactionLocally(transaction) transaction @@ -258,9 +272,9 @@ class FinalityFlow private constructor(val transaction: SignedTransaction, } @Suspendable - private fun finaliseLocallyAndBroadcast(sessions: Collection, tx: SignedTransaction, metadata: TransactionMetadata) { + private fun finaliseLocallyAndBroadcast(sessions: Collection, tx: SignedTransaction) { serviceHub.telemetryServiceInternal.span("${this::class.java.name}#finaliseLocallyAndBroadcast", flowLogic = this) { - finaliseLocally(tx, metadata = metadata) + finaliseLocally(tx) progressTracker.currentStep = BROADCASTING broadcast(sessions, tx) } @@ -272,7 +286,8 @@ class FinalityFlow private constructor(val transaction: SignedTransaction, sessions.forEach { session -> try { logger.debug { "Sending transaction to party $session." } - subFlow(SendTransactionFlow(session, tx)) + subFlow(SendTransactionFlow(session, tx, txnMetadata)) + txnMetadata = txnMetadata.copy(persist = false) } catch (e: UnexpectedFlowEndException) { throw UnexpectedFlowEndException( "${session.counterparty} has finished prematurely and we're trying to send them a transaction." + @@ -285,6 +300,13 @@ class FinalityFlow private constructor(val transaction: SignedTransaction, } } + private fun deriveStatesToRecord(newPlatformSessions: Collection): Map { + val derivedObserverSessions = newPlatformSessions.map { it.counterparty }.toSet() - externalTxParticipants + val txParticipantSessions = externalTxParticipants + return txParticipantSessions.map { it.name to ONLY_RELEVANT }.toMap() + + (derivedObserverSessions + observerSessions.map { it.counterparty }).map { it.name to ALL_VISIBLE } + } + @Suspendable private fun broadcastSignaturesAndFinalise(sessions: Collection, notarySignatures: List) { progressTracker.currentStep = BROADCASTING_POST_NOTARISATION @@ -309,12 +331,11 @@ class FinalityFlow private constructor(val transaction: SignedTransaction, } @Suspendable - private fun finaliseLocally(stx: SignedTransaction, notarySignatures: List = emptyList(), - metadata: TransactionMetadata? = null) { + private fun finaliseLocally(stx: SignedTransaction, notarySignatures: List = emptyList()) { progressTracker.currentStep = FINALISING_TRANSACTION serviceHub.telemetryServiceInternal.span("${this::class.java.name}#finaliseLocally", flowLogic = this) { if (notarySignatures.isEmpty()) { - (serviceHub as ServiceHubCoreInternal).finalizeTransaction(stx, statesToRecord, metadata!!) + (serviceHub as ServiceHubCoreInternal).finalizeTransaction(stx, statesToRecord) logger.info("Finalised transaction locally.") } else { (serviceHub as ServiceHubCoreInternal).finalizeTransactionWithExtraSignatures(stx, notarySignatures, statesToRecord) @@ -355,7 +376,8 @@ class FinalityFlow private constructor(val transaction: SignedTransaction, for (session in sessions) { try { logger.debug { "Sending transaction to party $session." } - subFlow(SendTransactionFlow(session, tx)) + subFlow(SendTransactionFlow(session, tx, txnMetadata)) + txnMetadata = txnMetadata.copy(persist = false) } catch (e: UnexpectedFlowEndException) { throw UnexpectedFlowEndException( "${session.counterparty} has finished prematurely and we're trying to send them the finalised transaction. " + @@ -378,7 +400,8 @@ class FinalityFlow private constructor(val transaction: SignedTransaction, if (!serviceHub.myInfo.isLegalIdentity(recipient)) { logger.debug { "Sending transaction to party $recipient." } val session = initiateFlow(recipient) - subFlow(SendTransactionFlow(session, notarised)) + subFlow(SendTransactionFlow(session, notarised, txnMetadata)) + txnMetadata = txnMetadata.copy(persist = false) logger.info("Party $recipient received the transaction.") } } @@ -404,11 +427,7 @@ class FinalityFlow private constructor(val transaction: SignedTransaction, private fun recordUnnotarisedTransaction(tx: SignedTransaction): SignedTransaction { progressTracker.currentStep = RECORD_UNNOTARISED serviceHub.telemetryServiceInternal.span("${this::class.java.name}#recordUnnotarisedTransaction", flowLogic = this) { - (serviceHub as ServiceHubCoreInternal).recordUnnotarisedTransaction(tx, - TransactionMetadata( - serviceHub.myInfo.legalIdentities.first().name, - statesToRecord, - sessions.map { it.counterparty.name }.toSet())) + (serviceHub as ServiceHubCoreInternal).recordUnnotarisedTransaction(tx) logger.info("Recorded un-notarised transaction locally.") return tx } @@ -487,7 +506,7 @@ class ReceiveFinalityFlow @JvmOverloads constructor(private val otherSideSession @Suppress("ComplexMethod", "NestedBlockDepth") @Suspendable override fun call(): SignedTransaction { - val stx = subFlow(ReceiveTransactionFlow(otherSideSession, checkSufficientSignatures = false, statesToRecord = statesToRecord, deferredAck = true)) + val stx = subFlow(ReceiveTransactionFlow(otherSideSession, false, statesToRecord, true)) val requiresNotarisation = needsNotarySignature(stx) val fromTwoPhaseFinalityNode = serviceHub.networkMapCache.getNodeByLegalIdentity(otherSideSession.counterparty)?.platformVersion!! >= PlatformVersionSwitches.TWO_PHASE_FINALITY @@ -495,8 +514,7 @@ class ReceiveFinalityFlow @JvmOverloads constructor(private val otherSideSession if (requiresNotarisation) { serviceHub.telemetryServiceInternal.span("${this::class.java.name}#recordUnnotarisedTransaction", flowLogic = this) { logger.debug { "Peer recording transaction without notary signature." } - (serviceHub as ServiceHubCoreInternal).recordUnnotarisedTransaction(stx, - TransactionMetadata(otherSideSession.counterparty.name, statesToRecord)) + (serviceHub as ServiceHubCoreInternal).recordUnnotarisedTransaction(stx) } otherSideSession.send(FetchDataFlow.Request.End) // Finish fetching data (deferredAck) logger.info("Peer recorded transaction without notary signature. Waiting to receive notary signature.") @@ -522,8 +540,7 @@ class ReceiveFinalityFlow @JvmOverloads constructor(private val otherSideSession } } else { serviceHub.telemetryServiceInternal.span("${this::class.java.name}#finalizeTransaction", flowLogic = this) { - (serviceHub as ServiceHubCoreInternal).finalizeTransaction(stx, statesToRecord, - TransactionMetadata(otherSideSession.counterparty.name, statesToRecord)) + (serviceHub as ServiceHubCoreInternal).finalizeTransaction(stx, statesToRecord) logger.info("Peer recorded transaction with recovery metadata.") } otherSideSession.send(FetchDataFlow.Request.End) // Finish fetching data (deferredAck) diff --git a/core/src/main/kotlin/net/corda/core/flows/FlowTransaction.kt b/core/src/main/kotlin/net/corda/core/flows/FlowTransaction.kt index 8f0c4a901a..532a90299d 100644 --- a/core/src/main/kotlin/net/corda/core/flows/FlowTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/flows/FlowTransaction.kt @@ -24,8 +24,14 @@ data class FlowTransactionInfo( @CordaSerializable data class TransactionMetadata( val initiator: CordaX500Name, - val statesToRecord: StatesToRecord? = StatesToRecord.ONLY_RELEVANT, - val peers: Set? = null + val distributionList: DistributionList, + val persist: Boolean = true // hint to persist to transactional store +) + +@CordaSerializable +data class DistributionList( + val senderStatesToRecord: StatesToRecord, + val peersToStatesToRecord: Map ) @CordaSerializable diff --git a/core/src/main/kotlin/net/corda/core/flows/ReceiveTransactionFlow.kt b/core/src/main/kotlin/net/corda/core/flows/ReceiveTransactionFlow.kt index 4f5d04b6d9..6b46c96de5 100644 --- a/core/src/main/kotlin/net/corda/core/flows/ReceiveTransactionFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/ReceiveTransactionFlow.kt @@ -1,8 +1,13 @@ package net.corda.core.flows import co.paralleluniverse.fibers.Suspendable -import net.corda.core.contracts.* +import net.corda.core.contracts.AttachmentResolutionException +import net.corda.core.contracts.ContractState +import net.corda.core.contracts.StateAndRef +import net.corda.core.contracts.TransactionResolutionException +import net.corda.core.contracts.TransactionVerificationException import net.corda.core.internal.ResolveTransactionsFlow +import net.corda.core.internal.ServiceHubCoreInternal import net.corda.core.internal.checkParameterHash import net.corda.core.internal.pushToLoggingContext import net.corda.core.node.StatesToRecord @@ -53,19 +58,23 @@ open class ReceiveTransactionFlow constructor(private val otherSideSession: Flow } else { logger.trace { "Receiving a transaction (but without checking the signatures) from ${otherSideSession.counterparty}" } } - val stx = otherSideSession.receive().unwrap { - it.pushToLoggingContext() - logger.info("Received transaction acknowledgement request from party ${otherSideSession.counterparty}.") - checkParameterHash(it.networkParametersHash) - subFlow(ResolveTransactionsFlow(it, otherSideSession, statesToRecord, deferredAck)) - logger.info("Transaction dependencies resolution completed.") - try { - it.verify(serviceHub, checkSufficientSignatures) - it - } catch (e: Exception) { - logger.warn("Transaction verification failed.") - throw e - } + + val payload = otherSideSession.receive().unwrap { it } + val stx = + if (payload is SignedTransactionWithDistributionList) { + recordTransactionMetadata(payload.stx, payload.distributionList) + payload.stx + } else payload as SignedTransaction + stx.pushToLoggingContext() + logger.info("Received transaction acknowledgement request from party ${otherSideSession.counterparty}.") + checkParameterHash(stx.networkParametersHash) + subFlow(ResolveTransactionsFlow(stx, otherSideSession, statesToRecord, deferredAck)) + logger.info("Transaction dependencies resolution completed.") + try { + stx.verify(serviceHub, checkSufficientSignatures) + } catch (e: Exception) { + logger.warn("Transaction verification failed.") + throw e } if (checkSufficientSignatures) { // We should only send a transaction to the vault for processing if we did in fact fully verify it, and @@ -78,6 +87,21 @@ open class ReceiveTransactionFlow constructor(private val otherSideSession: Flow return stx } + @Suspendable + private fun recordTransactionMetadata(stx: SignedTransaction, distributionList: DistributionList?) { + distributionList?.let { + val txnMetadata = TransactionMetadata(otherSideSession.counterparty.name, + DistributionList(distributionList.senderStatesToRecord, + distributionList.peersToStatesToRecord.map { (peer, peerStatesToRecord) -> + if (peer == ourIdentity.name) + peer to statesToRecord // use actual value + else + peer to peerStatesToRecord // use hinted value + }.toMap())) + (serviceHub as ServiceHubCoreInternal).recordTransactionRecoveryMetadata(stx.id, txnMetadata, ourIdentity.name) + } + } + /** * Hook to perform extra checks on the received transaction just before it's recorded. The transaction has already * been resolved and verified at this point. diff --git a/core/src/main/kotlin/net/corda/core/flows/SendTransactionFlow.kt b/core/src/main/kotlin/net/corda/core/flows/SendTransactionFlow.kt index 233a89236b..68f89469d2 100644 --- a/core/src/main/kotlin/net/corda/core/flows/SendTransactionFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/SendTransactionFlow.kt @@ -4,14 +4,24 @@ import co.paralleluniverse.fibers.Suspendable import net.corda.core.contracts.NamedByHash import net.corda.core.contracts.StateAndRef import net.corda.core.crypto.SecureHash +import net.corda.core.identity.CordaX500Name import net.corda.core.internal.* +import net.corda.core.node.StatesToRecord import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.SerializedBytes import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize import net.corda.core.transactions.SignedTransaction -import net.corda.core.utilities.unwrap import net.corda.core.utilities.trace +import net.corda.core.utilities.unwrap +import kotlin.collections.List +import kotlin.collections.MutableSet +import kotlin.collections.Set +import kotlin.collections.flatMap +import kotlin.collections.map +import kotlin.collections.mutableSetOf +import kotlin.collections.plus +import kotlin.collections.toSet /** * In the words of Matt working code is more important then pretty code. This class that contains code that may @@ -66,8 +76,16 @@ class MaybeSerializedSignedTransaction(override val id: SecureHash, val serializ * * @param otherSide the target party. * @param stx the [SignedTransaction] being sent to the [otherSideSession]. + * @property txnMetadata transaction recovery metadata (eg. used by Two Phase Finality). */ -open class SendTransactionFlow(otherSide: FlowSession, stx: SignedTransaction) : DataVendingFlow(otherSide, stx) +open class SendTransactionFlow(otherSide: FlowSession, stx: SignedTransaction, txnMetadata: TransactionMetadata) : DataVendingFlow(otherSide, stx, txnMetadata) { + constructor(otherSide: FlowSession, stx: SignedTransaction) : this(otherSide, stx, + TransactionMetadata(DUMMY_PARTICIPANT_NAME, DistributionList(StatesToRecord.NONE, mapOf(otherSide.counterparty.name to StatesToRecord.ALL_VISIBLE)))) + // Note: DUMMY_PARTICIPANT_NAME to be substituted with actual "ourIdentity.name" in flow call() + companion object { + val DUMMY_PARTICIPANT_NAME = CordaX500Name("Transaction Participant", "London", "GB") + } +} /** * The [SendStateAndRefFlow] should be used to send a list of input [StateAndRef] to another peer that wishes to verify @@ -80,7 +98,9 @@ open class SendTransactionFlow(otherSide: FlowSession, stx: SignedTransaction) : */ open class SendStateAndRefFlow(otherSideSession: FlowSession, stateAndRefs: List>) : DataVendingFlow(otherSideSession, stateAndRefs) -open class DataVendingFlow(val otherSideSession: FlowSession, val payload: Any) : FlowLogic() { +open class DataVendingFlow(val otherSideSession: FlowSession, val payload: Any, val txnMetadata: TransactionMetadata? = null) : FlowLogic() { + constructor(otherSideSession: FlowSession, payload: Any) : this(otherSideSession, payload, null) + @Suspendable protected open fun sendPayloadAndReceiveDataRequest(otherSideSession: FlowSession, payload: Any) = otherSideSession.sendAndReceive(payload) @@ -89,6 +109,7 @@ open class DataVendingFlow(val otherSideSession: FlowSession, val payload: Any) // User can override this method to perform custom request verification. } + @Suppress("ComplexCondition", "ComplexMethod") @Suspendable override fun call(): Void? { val networkMaxMessageSize = serviceHub.networkParameters.maxMessageSize @@ -117,6 +138,15 @@ open class DataVendingFlow(val otherSideSession: FlowSession, val payload: Any) else -> throw Exception("Unknown payload type: ${payload::class.java} ?") } + // store and share transaction recovery metadata if required + val useTwoPhaseFinality = serviceHub.myInfo.platformVersion >= PlatformVersionSwitches.TWO_PHASE_FINALITY + val toTwoPhaseFinalityNode = serviceHub.networkMapCache.getNodeByLegalIdentity(otherSideSession.counterparty)?.platformVersion!! >= PlatformVersionSwitches.TWO_PHASE_FINALITY + if (txnMetadata != null && toTwoPhaseFinalityNode && useTwoPhaseFinality && payload is SignedTransaction) { + payload = SignedTransactionWithDistributionList(payload, txnMetadata.distributionList) + if (txnMetadata.persist) + (serviceHub as ServiceHubCoreInternal).recordTransactionRecoveryMetadata(payload.stx.id, txnMetadata.copy(initiator = ourIdentity.name), ourIdentity.name) + } + // This loop will receive [FetchDataFlow.Request] continuously until the `otherSideSession` has all the data they need // to resolve the transaction, a [FetchDataFlow.EndRequest] will be sent from the `otherSideSession` to indicate end of // data request. @@ -240,3 +270,9 @@ open class DataVendingFlow(val otherSideSession: FlowSession, val payload: Any) } } } + +@CordaSerializable +data class SignedTransactionWithDistributionList( + val stx: SignedTransaction, + val distributionList: DistributionList +) \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/internal/ServiceHubCoreInternal.kt b/core/src/main/kotlin/net/corda/core/internal/ServiceHubCoreInternal.kt index 264353f932..239c37166e 100644 --- a/core/src/main/kotlin/net/corda/core/internal/ServiceHubCoreInternal.kt +++ b/core/src/main/kotlin/net/corda/core/internal/ServiceHubCoreInternal.kt @@ -5,6 +5,7 @@ import net.corda.core.DeleteForDJVM import net.corda.core.crypto.SecureHash import net.corda.core.crypto.TransactionSignature import net.corda.core.flows.TransactionMetadata +import net.corda.core.identity.CordaX500Name import net.corda.core.internal.notary.NotaryService import net.corda.core.node.ServiceHub import net.corda.core.node.StatesToRecord @@ -35,9 +36,8 @@ interface ServiceHubCoreInternal : ServiceHub { * This is expected to be run within a database transaction. * * @param txn The transaction to record. - * @param metadata Finality flow recovery metadata. */ - fun recordUnnotarisedTransaction(txn: SignedTransaction, metadata: TransactionMetadata) + fun recordUnnotarisedTransaction(txn: SignedTransaction) /** * Removes transaction from data store. @@ -61,9 +61,17 @@ interface ServiceHubCoreInternal : ServiceHub { * * @param txn The transaction to record. * @param statesToRecord how the vault should treat the output states of the transaction. - * @param metadata Finality flow recovery metadata. */ - fun finalizeTransaction(txn: SignedTransaction, statesToRecord: StatesToRecord, metadata: TransactionMetadata) + fun finalizeTransaction(txn: SignedTransaction, statesToRecord: StatesToRecord) + + /** + * Records [TransactionMetadata] for a given txnId. + * + * @param txnId The SecureHash of a transaction. + * @param txnMetadata The recovery metadata associated with a transaction. + * @param caller The CordaX500Name of the party calling this operation. + */ + fun recordTransactionRecoveryMetadata(txnId: SecureHash, txnMetadata: TransactionMetadata, caller: CordaX500Name) } interface TransactionsResolver { diff --git a/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt b/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt index 30163d6442..4b3456d084 100644 --- a/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt +++ b/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt @@ -14,6 +14,7 @@ import net.corda.core.internal.PlatformVersionSwitches.TWO_PHASE_FINALITY import net.corda.core.internal.telemetry.TelemetryComponent import net.corda.core.node.services.* import net.corda.core.node.services.diagnostics.DiagnosticsService +import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.SerializeAsToken import net.corda.core.transactions.FilteredTransaction import net.corda.core.transactions.LedgerTransaction @@ -90,6 +91,7 @@ interface ServicesForResolution { * Controls whether the transaction is sent to the vault at all, and if so whether states have to be relevant * or not in order to be recorded. Used in [ServiceHub.recordTransactions] */ +@CordaSerializable enum class StatesToRecord { /** The received transaction is not sent to the vault at all. This is used within transaction resolution. */ NONE, diff --git a/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt b/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt index 4d2e1e7b44..fb7a6d9f16 100644 --- a/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt +++ b/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt @@ -8,6 +8,7 @@ import net.corda.core.flows.FlowLogic import net.corda.core.flows.TransactionMetadata import net.corda.core.flows.StateMachineRunId import net.corda.core.flows.TransactionStatus +import net.corda.core.identity.CordaX500Name import net.corda.core.internal.FlowStateMachineHandle import net.corda.core.internal.NamedCacheFactory import net.corda.core.internal.ResolveTransactionsFlow @@ -194,6 +195,9 @@ interface ServiceHubInternal : ServiceHubCoreInternal { override fun recordTransactions(statesToRecord: StatesToRecord, txs: Iterable) = recordTransactions(statesToRecord, txs, SIGNATURE_VERIFICATION_DISABLED) + override fun recordTransactionRecoveryMetadata(txnId: SecureHash, txnMetadata: TransactionMetadata, caller: CordaX500Name) = + validatedTransactions.addTransactionRecoveryMetadata(txnId, txnMetadata, caller) + @Suppress("NestedBlockDepth") @VisibleForTesting fun recordTransactions(statesToRecord: StatesToRecord, txs: Iterable, disableSignatureVerification: Boolean) { @@ -240,27 +244,25 @@ interface ServiceHubInternal : ServiceHubCoreInternal { ) } - override fun finalizeTransaction(txn: SignedTransaction, statesToRecord: StatesToRecord, metadata: TransactionMetadata) { + override fun finalizeTransaction(txn: SignedTransaction, statesToRecord: StatesToRecord) { requireSupportedHashType(txn) if (txn.coreTransaction is WireTransaction) txn.verifyRequiredSignatures() database.transaction { recordTransactions(statesToRecord, listOf(txn), validatedTransactions, stateMachineRecordedTransactionMapping, vaultService, database) { - val isInitiator = metadata.initiator == myInfo.legalIdentities.first().name - validatedTransactions.finalizeTransaction(txn, metadata, isInitiator) + validatedTransactions.finalizeTransaction(txn) } } } - override fun recordUnnotarisedTransaction(txn: SignedTransaction, metadata: TransactionMetadata) { + override fun recordUnnotarisedTransaction(txn: SignedTransaction) { if (txn.coreTransaction is WireTransaction) { txn.notary?.let { notary -> txn.verifySignaturesExcept(notary.owningKey) } ?: txn.verifyRequiredSignatures() } database.transaction { - val isInitiator = metadata.initiator == myInfo.legalIdentities.first().name - validatedTransactions.addUnnotarisedTransaction(txn, metadata, isInitiator) + validatedTransactions.addUnnotarisedTransaction(txn) } } @@ -360,10 +362,18 @@ interface WritableTransactionStorage : TransactionStorage { * Add an un-notarised transaction to the store with a status of *MISSING_TRANSACTION_SIG* and inclusive of flow recovery metadata. * * @param transaction The transaction to be recorded. - * @param metadata Finality flow recovery metadata. * @return true if the transaction was recorded as a *new* transaction, false if the transaction already exists. */ - fun addUnnotarisedTransaction(transaction: SignedTransaction, metadata: TransactionMetadata, isInitiator: Boolean): Boolean + fun addUnnotarisedTransaction(transaction: SignedTransaction): Boolean + + /** + * Record transaction recovery metadata for a given transaction id. + * + * @param id The SecureHash of the transaction to be recorded. + * @param metadata transaction recovery metadata. + * @param caller The CordaX500Name of the party calling this operation. + */ + fun addTransactionRecoveryMetadata(id: SecureHash, metadata: TransactionMetadata, caller: CordaX500Name) /** * Removes an un-notarised transaction (with a status of *MISSING_TRANSACTION_SIG*) from the data store. @@ -375,10 +385,9 @@ interface WritableTransactionStorage : TransactionStorage { * Add a finalised transaction to the store with flow recovery metadata. * * @param transaction The transaction to be recorded. - * @param metadata Finality flow recovery metadata. * @return true if the transaction was recorded as a *new* transaction, false if the transaction already exists. */ - fun finalizeTransaction(transaction: SignedTransaction, metadata: TransactionMetadata, isInitiator: Boolean): Boolean + fun finalizeTransaction(transaction: SignedTransaction): Boolean /** * Update a previously un-notarised transaction including associated notary signatures. diff --git a/node/src/main/kotlin/net/corda/node/services/network/PersistentPartyInfoCache.kt b/node/src/main/kotlin/net/corda/node/services/network/PersistentPartyInfoCache.kt index bb3c9aed9f..e9ba12a3a6 100644 --- a/node/src/main/kotlin/net/corda/node/services/network/PersistentPartyInfoCache.kt +++ b/node/src/main/kotlin/net/corda/node/services/network/PersistentPartyInfoCache.kt @@ -56,7 +56,6 @@ class PersistentPartyInfoCache(private val networkMapCache: PersistentNetworkMap private fun updateInfoDB(partyHashCode: Long, partyName: CordaX500Name) { database.transaction { if (queryByPartyId(session, partyHashCode) == null) { - println("PartyInfo: $partyHashCode -> $partyName") session.save(DBTransactionStorageLedgerRecovery.DBRecoveryPartyInfo(partyHashCode, partyName.toString())) } } diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt index b78e2adb35..597d1a8675 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt @@ -4,6 +4,7 @@ import net.corda.core.concurrent.CordaFuture import net.corda.core.crypto.SecureHash import net.corda.core.crypto.TransactionSignature import net.corda.core.flows.TransactionMetadata +import net.corda.core.identity.CordaX500Name import net.corda.core.internal.NamedCacheFactory import net.corda.core.internal.ThreadBox import net.corda.core.internal.VisibleForTesting @@ -208,12 +209,14 @@ open class DBTransactionStorage(private val database: CordaPersistence, cacheFac updateTransaction(transaction.id) } - override fun addUnnotarisedTransaction(transaction: SignedTransaction, metadata: TransactionMetadata, isInitiator: Boolean) = + override fun addUnnotarisedTransaction(transaction: SignedTransaction) = addTransaction(transaction, TransactionStatus.IN_FLIGHT) { false } - override fun finalizeTransaction(transaction: SignedTransaction, metadata: TransactionMetadata, isInitiator: Boolean) = + override fun addTransactionRecoveryMetadata(id: SecureHash, metadata: TransactionMetadata, caller: CordaX500Name) { } + + override fun finalizeTransaction(transaction: SignedTransaction) = addTransaction(transaction) { false } diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecovery.kt b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecovery.kt index f7b65472ac..5abbff52a4 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecovery.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecovery.kt @@ -10,7 +10,6 @@ import net.corda.core.node.services.vault.Sort import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize -import net.corda.core.transactions.SignedTransaction import net.corda.node.CordaClock import net.corda.node.services.network.PersistentPartyInfoCache import net.corda.nodeapi.internal.cryptoservice.CryptoService @@ -19,6 +18,7 @@ import net.corda.nodeapi.internal.persistence.NODE_DATABASE_PREFIX import net.corda.serialization.internal.CordaSerializationEncoding import org.hibernate.annotations.Immutable import java.io.Serializable +import java.lang.IllegalStateException import java.time.Instant import java.util.concurrent.atomic.AtomicLong import javax.persistence.Column @@ -100,13 +100,13 @@ class DBTransactionStorageLedgerRecovery(private val database: CordaPersistence, @Column(name = "sender_states_to_record", nullable = false) val senderStatesToRecord: StatesToRecord ) { - constructor(key: Key, txId: SecureHash, initiatorPartyId: Long, peerPartyIds: Set, statesToRecord: StatesToRecord, cryptoService: CryptoService) : + constructor(key: Key, txId: SecureHash, initiatorPartyId: Long, peersToStatesToRecord: Map, senderStatesToRecord: StatesToRecord, receiverStatesToRecord: StatesToRecord, cryptoService: CryptoService) : this(PersistentKey(key), txId = txId.toString(), senderPartyId = initiatorPartyId, - distributionList = cryptoService.encrypt(peerPartyIds.serialize(context = contextToUse().withEncoding(CordaSerializationEncoding.SNAPPY)).bytes), - receiverStatesToRecord = statesToRecord, - senderStatesToRecord = StatesToRecord.NONE // to be set in follow-up PR. + distributionList = cryptoService.encrypt(peersToStatesToRecord.serialize(context = contextToUse().withEncoding(CordaSerializationEncoding.SNAPPY)).bytes), + receiverStatesToRecord = receiverStatesToRecord, + senderStatesToRecord = senderStatesToRecord ) fun toReceiverDistributionRecord(cryptoService: CryptoService) = @@ -115,6 +115,7 @@ class DBTransactionStorageLedgerRecovery(private val database: CordaPersistence, this.senderPartyId, cryptoService.decrypt(this.distributionList).deserialize(context = contextToUse()), this.receiverStatesToRecord, + this.senderStatesToRecord, this.compositeKey.timestamp ) } @@ -141,17 +142,33 @@ class DBTransactionStorageLedgerRecovery(private val database: CordaPersistence, } } - override fun addUnnotarisedTransaction(transaction: SignedTransaction, metadata: TransactionMetadata, isInitiator: Boolean): Boolean { - return addTransaction(transaction, TransactionStatus.IN_FLIGHT) { - addTransactionRecoveryMetadata(transaction.id, metadata, isInitiator, clock) + @Suppress("IMPLICIT_CAST_TO_ANY") + override fun addTransactionRecoveryMetadata(id: SecureHash, metadata: TransactionMetadata, caller: CordaX500Name) { + database.transaction { + if (caller == metadata.initiator) { + metadata.distributionList.peersToStatesToRecord.map { (peer, _) -> + val senderDistributionRecord = DBSenderDistributionRecord(PersistentKey(Key(clock.instant())), + id.toString(), + partyInfoCache.getPartyIdByCordaX500Name(peer), + metadata.distributionList.senderStatesToRecord) + session.save(senderDistributionRecord) + } + } else { + val receiverStatesToRecord = metadata.distributionList.peersToStatesToRecord[caller] ?: throw IllegalStateException("Missing peer $caller in distribution list of Receiver recovery metadata") + val receiverDistributionRecord = + DBReceiverDistributionRecord(Key(clock.instant()), + id, + partyInfoCache.getPartyIdByCordaX500Name(metadata.initiator), + metadata.distributionList.peersToStatesToRecord.map { (peer, statesToRecord) -> + partyInfoCache.getPartyIdByCordaX500Name(peer) to statesToRecord }.toMap(), + metadata.distributionList.senderStatesToRecord, + receiverStatesToRecord, + cryptoService) + session.save(receiverDistributionRecord) + } } } - override fun finalizeTransaction(transaction: SignedTransaction, metadata: TransactionMetadata, isInitiator: Boolean) = - addTransaction(transaction) { - addTransactionRecoveryMetadata(transaction.id, metadata, isInitiator, clock) - } - override fun removeUnnotarisedTransaction(id: SecureHash): Boolean { return database.transaction { super.removeUnnotarisedTransaction(id) @@ -260,31 +277,6 @@ class DBTransactionStorageLedgerRecovery(private val database: CordaPersistence, results.map { it.toReceiverDistributionRecord(cryptoService) }.toList() } } - - @Suppress("IMPLICIT_CAST_TO_ANY") - private fun addTransactionRecoveryMetadata(txId: SecureHash, metadata: TransactionMetadata, isInitiator: Boolean, clock: CordaClock): Boolean { - database.transaction { - if (isInitiator) { - metadata.peers?.map { peer -> - val senderDistributionRecord = DBSenderDistributionRecord(PersistentKey(Key(clock.instant())), - txId.toString(), - partyInfoCache.getPartyIdByCordaX500Name(peer), - metadata.statesToRecord ?: StatesToRecord.ONLY_RELEVANT) - session.save(senderDistributionRecord) - } - } else { - val receiverDistributionRecord = - DBReceiverDistributionRecord(Key(clock.instant()), - txId, - partyInfoCache.getPartyIdByCordaX500Name(metadata.initiator), - metadata.peers?.map { partyInfoCache.getPartyIdByCordaX500Name(it) }?.toSet() ?: emptySet(), - metadata.statesToRecord ?: StatesToRecord.ONLY_RELEVANT, - cryptoService) - session.save(receiverDistributionRecord) - } - } - return false - } } // TO DO: https://r3-cev.atlassian.net/browse/ENT-9876 @@ -316,8 +308,9 @@ data class SenderDistributionRecord( data class ReceiverDistributionRecord( override val txId: SecureHash, val initiatorPartyId: Long, // CordaX500Name hashCode() - val peerPartyIds: Set, // CordaX500Name hashCode() + val peersToStatesToRecord: Map, // CordaX500Name hashCode() -> StatesToRecord override val statesToRecord: StatesToRecord, + val senderStatesToRecord: StatesToRecord, override val timestamp: Instant ) : DistributionRecord(txId, statesToRecord, timestamp) diff --git a/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt b/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt index 6f87a4f525..bd25b3c512 100644 --- a/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt +++ b/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt @@ -801,23 +801,29 @@ class TwoPartyTradeFlowTests(private val anonymous: Boolean) { return true } - override fun addUnnotarisedTransaction(transaction: SignedTransaction, metadata: TransactionMetadata, isInitiator: Boolean): Boolean { + override fun addUnnotarisedTransaction(transaction: SignedTransaction): Boolean { database.transaction { records.add(TxRecord.Add(transaction)) - delegate.addUnnotarisedTransaction(transaction, metadata, isInitiator) + delegate.addUnnotarisedTransaction(transaction) } return true } + override fun addTransactionRecoveryMetadata(id: SecureHash, metadata: TransactionMetadata, caller: CordaX500Name) { + database.transaction { + delegate.addTransactionRecoveryMetadata(id, metadata, caller) + } + } + override fun removeUnnotarisedTransaction(id: SecureHash): Boolean { return database.transaction { delegate.removeUnnotarisedTransaction(id) } } - override fun finalizeTransaction(transaction: SignedTransaction, metadata: TransactionMetadata, isInitiator: Boolean): Boolean { + override fun finalizeTransaction(transaction: SignedTransaction): Boolean { database.transaction { - delegate.finalizeTransaction(transaction, metadata, isInitiator) + delegate.finalizeTransaction(transaction) } return true } diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecoveryTests.kt b/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecoveryTests.kt index 4e17a0b6f9..3766e5629f 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecoveryTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecoveryTests.kt @@ -6,10 +6,13 @@ import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SignableData import net.corda.core.crypto.SignatureMetadata import net.corda.core.crypto.sign +import net.corda.core.flows.DistributionList import net.corda.core.flows.TransactionMetadata import net.corda.core.flows.RecoveryTimeWindow import net.corda.core.node.NodeInfo -import net.corda.core.node.StatesToRecord +import net.corda.core.node.StatesToRecord.ALL_VISIBLE +import net.corda.core.node.StatesToRecord.ONLY_RELEVANT +import net.corda.core.node.StatesToRecord.NONE import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.WireTransaction import net.corda.core.utilities.NetworkHostAndPort @@ -79,14 +82,18 @@ class DBTransactionStorageLedgerRecoveryTests { @Test(timeout = 300_000) fun `query local ledger for transactions with recovery peers within time window`() { val beforeFirstTxn = now() - transactionRecovery.addUnnotarisedTransaction(newTransaction(), TransactionMetadata(ALICE_NAME, StatesToRecord.ALL_VISIBLE, setOf(BOB_NAME)), true) + val txn = newTransaction() + transactionRecovery.addUnnotarisedTransaction(txn) + transactionRecovery.addTransactionRecoveryMetadata(txn.id, TransactionMetadata(ALICE_NAME, DistributionList(ALL_VISIBLE, mapOf(BOB_NAME to ONLY_RELEVANT))), ALICE_NAME) val timeWindow = RecoveryTimeWindow(fromTime = beforeFirstTxn, untilTime = beforeFirstTxn.plus(1, ChronoUnit.MINUTES)) val results = transactionRecovery.querySenderDistributionRecords(timeWindow) assertEquals(1, results.size) val afterFirstTxn = now() - transactionRecovery.addUnnotarisedTransaction(newTransaction(), TransactionMetadata(ALICE_NAME, StatesToRecord.ONLY_RELEVANT, setOf(CHARLIE_NAME)), true) + val txn2 = newTransaction() + transactionRecovery.addUnnotarisedTransaction(txn2) + transactionRecovery.addTransactionRecoveryMetadata(txn2.id, TransactionMetadata(ALICE_NAME, DistributionList(ONLY_RELEVANT, mapOf(CHARLIE_NAME to ONLY_RELEVANT))), ALICE_NAME) assertEquals(2, transactionRecovery.querySenderDistributionRecords(timeWindow).size) assertEquals(1, transactionRecovery.querySenderDistributionRecords(RecoveryTimeWindow(fromTime = afterFirstTxn)).size) } @@ -94,9 +101,11 @@ class DBTransactionStorageLedgerRecoveryTests { @Test(timeout = 300_000) fun `query local ledger for transactions within timeWindow and excluding remoteTransactionIds`() { val transaction1 = newTransaction() - transactionRecovery.addUnnotarisedTransaction(transaction1, TransactionMetadata(ALICE_NAME, StatesToRecord.ALL_VISIBLE, setOf(BOB_NAME)), true) + transactionRecovery.addUnnotarisedTransaction(transaction1) + transactionRecovery.addTransactionRecoveryMetadata(transaction1.id, TransactionMetadata(ALICE_NAME, DistributionList(ALL_VISIBLE, mapOf(BOB_NAME to ONLY_RELEVANT))), ALICE_NAME) val transaction2 = newTransaction() - transactionRecovery.addUnnotarisedTransaction(transaction2, TransactionMetadata(ALICE_NAME, StatesToRecord.ALL_VISIBLE, setOf(BOB_NAME)), true) + transactionRecovery.addUnnotarisedTransaction(transaction2) + transactionRecovery.addTransactionRecoveryMetadata(transaction2.id, TransactionMetadata(ALICE_NAME, DistributionList(ALL_VISIBLE, mapOf(BOB_NAME to ONLY_RELEVANT))), ALICE_NAME) val timeWindow = RecoveryTimeWindow(fromTime = now().minus(1, ChronoUnit.DAYS)) val results = transactionRecovery.querySenderDistributionRecords(timeWindow, excludingTxnIds = setOf(transaction1.id)) assertEquals(1, results.size) @@ -106,18 +115,22 @@ class DBTransactionStorageLedgerRecoveryTests { fun `query local ledger by distribution record type`() { val transaction1 = newTransaction() // sender txn - transactionRecovery.addUnnotarisedTransaction(transaction1, TransactionMetadata(ALICE_NAME, StatesToRecord.ALL_VISIBLE, setOf(BOB_NAME)), true) + transactionRecovery.addUnnotarisedTransaction(transaction1) + transactionRecovery.addTransactionRecoveryMetadata(transaction1.id, TransactionMetadata(ALICE_NAME, DistributionList(ALL_VISIBLE, mapOf(BOB_NAME to ALL_VISIBLE))), ALICE_NAME) val transaction2 = newTransaction() // receiver txn - transactionRecovery.addUnnotarisedTransaction(transaction2, TransactionMetadata(BOB_NAME, StatesToRecord.ALL_VISIBLE, setOf(ALICE_NAME)), false) + transactionRecovery.addUnnotarisedTransaction(transaction2) + transactionRecovery.addTransactionRecoveryMetadata(transaction2.id, TransactionMetadata(BOB_NAME, DistributionList(ONLY_RELEVANT, mapOf(ALICE_NAME to ALL_VISIBLE))), ALICE_NAME) val timeWindow = RecoveryTimeWindow(fromTime = now().minus(1, ChronoUnit.DAYS)) transactionRecovery.queryDistributionRecords(timeWindow, recordType = DistributionRecordType.SENDER).let { assertEquals(1, it.size) - assertEquals((it[0] as SenderDistributionRecord).peerPartyId, BOB_NAME.hashCode().toLong()) + assertEquals(BOB_NAME.hashCode().toLong(), (it[0] as SenderDistributionRecord).peerPartyId) + assertEquals(ALL_VISIBLE, (it[0] as SenderDistributionRecord).statesToRecord) } transactionRecovery.queryDistributionRecords(timeWindow, recordType = DistributionRecordType.RECEIVER).let { assertEquals(1, it.size) - assertEquals((it[0] as ReceiverDistributionRecord).initiatorPartyId, BOB_NAME.hashCode().toLong()) + assertEquals(BOB_NAME.hashCode().toLong(), (it[0] as ReceiverDistributionRecord).initiatorPartyId) + assertEquals(ALL_VISIBLE, (it[0] as ReceiverDistributionRecord).statesToRecord) } val resultsAll = transactionRecovery.queryDistributionRecords(timeWindow, recordType = DistributionRecordType.ALL) assertEquals(2, resultsAll.size) @@ -125,18 +138,28 @@ class DBTransactionStorageLedgerRecoveryTests { @Test(timeout = 300_000) fun `query for sender distribution records by peers`() { - transactionRecovery.addUnnotarisedTransaction(newTransaction(), TransactionMetadata(ALICE_NAME, StatesToRecord.ALL_VISIBLE, setOf(BOB_NAME)), true) - transactionRecovery.addUnnotarisedTransaction(newTransaction(), TransactionMetadata(ALICE_NAME, StatesToRecord.ONLY_RELEVANT, setOf(CHARLIE_NAME)), true) - transactionRecovery.addUnnotarisedTransaction(newTransaction(), TransactionMetadata(ALICE_NAME, StatesToRecord.ONLY_RELEVANT, setOf(BOB_NAME, CHARLIE_NAME)), true) - transactionRecovery.addUnnotarisedTransaction(newTransaction(), TransactionMetadata(BOB_NAME, StatesToRecord.ONLY_RELEVANT, setOf(ALICE_NAME)), true) - transactionRecovery.addUnnotarisedTransaction(newTransaction(), TransactionMetadata(CHARLIE_NAME, StatesToRecord.ONLY_RELEVANT), true) + val txn1 = newTransaction() + transactionRecovery.addUnnotarisedTransaction(txn1) + transactionRecovery.addTransactionRecoveryMetadata(txn1.id, TransactionMetadata(ALICE_NAME, DistributionList(ALL_VISIBLE, mapOf(BOB_NAME to ALL_VISIBLE))), ALICE_NAME) + val txn2 = newTransaction() + transactionRecovery.addUnnotarisedTransaction(txn2) + transactionRecovery.addTransactionRecoveryMetadata(txn2.id, TransactionMetadata(ALICE_NAME, DistributionList(ONLY_RELEVANT, mapOf(CHARLIE_NAME to ONLY_RELEVANT))), ALICE_NAME) + val txn3 = newTransaction() + transactionRecovery.addUnnotarisedTransaction(txn3) + transactionRecovery.addTransactionRecoveryMetadata(txn3.id, TransactionMetadata(ALICE_NAME, DistributionList(ONLY_RELEVANT, mapOf(BOB_NAME to ONLY_RELEVANT, CHARLIE_NAME to ALL_VISIBLE))), ALICE_NAME) + val txn4 = newTransaction() + transactionRecovery.addUnnotarisedTransaction(txn4) + transactionRecovery.addTransactionRecoveryMetadata(txn4.id, TransactionMetadata(BOB_NAME, DistributionList(ONLY_RELEVANT, mapOf(ALICE_NAME to ONLY_RELEVANT))), BOB_NAME) + val txn5 = newTransaction() + transactionRecovery.addUnnotarisedTransaction(txn5) + transactionRecovery.addTransactionRecoveryMetadata(txn5.id, TransactionMetadata(CHARLIE_NAME, DistributionList(ONLY_RELEVANT, emptyMap())), CHARLIE_NAME) assertEquals(5, readSenderDistributionRecordFromDB().size) val timeWindow = RecoveryTimeWindow(fromTime = now().minus(1, ChronoUnit.DAYS)) transactionRecovery.querySenderDistributionRecords(timeWindow, peers = setOf(BOB_NAME)).let { assertEquals(2, it.size) - assertEquals(it[0].statesToRecord, StatesToRecord.ALL_VISIBLE) - assertEquals(it[1].statesToRecord, StatesToRecord.ONLY_RELEVANT) + assertEquals(it[0].statesToRecord, ALL_VISIBLE) + assertEquals(it[1].statesToRecord, ONLY_RELEVANT) } assertEquals(1, transactionRecovery.querySenderDistributionRecords(timeWindow, peers = setOf(ALICE_NAME)).size) assertEquals(2, transactionRecovery.querySenderDistributionRecords(timeWindow, peers = setOf(CHARLIE_NAME)).size) @@ -144,59 +167,93 @@ class DBTransactionStorageLedgerRecoveryTests { @Test(timeout = 300_000) fun `query for receiver distribution records by initiator`() { - transactionRecovery.addUnnotarisedTransaction(newTransaction(), TransactionMetadata(ALICE_NAME, StatesToRecord.ALL_VISIBLE, setOf(BOB_NAME, CHARLIE_NAME)), false) - transactionRecovery.addUnnotarisedTransaction(newTransaction(), TransactionMetadata(ALICE_NAME, StatesToRecord.ONLY_RELEVANT, setOf(BOB_NAME)), false) - transactionRecovery.addUnnotarisedTransaction(newTransaction(), TransactionMetadata(ALICE_NAME, StatesToRecord.NONE, setOf(CHARLIE_NAME)), false) - transactionRecovery.addUnnotarisedTransaction(newTransaction(), TransactionMetadata(BOB_NAME, StatesToRecord.ALL_VISIBLE, setOf(ALICE_NAME)), false) - transactionRecovery.addUnnotarisedTransaction(newTransaction(), TransactionMetadata(CHARLIE_NAME, StatesToRecord.ONLY_RELEVANT), false) + val txn1 = newTransaction() + transactionRecovery.addUnnotarisedTransaction(txn1) + transactionRecovery.addTransactionRecoveryMetadata(txn1.id, TransactionMetadata(ALICE_NAME, + DistributionList(ONLY_RELEVANT, mapOf(BOB_NAME to ALL_VISIBLE, CHARLIE_NAME to ALL_VISIBLE))), BOB_NAME) + val txn2 = newTransaction() + transactionRecovery.addUnnotarisedTransaction(txn2) + transactionRecovery.addTransactionRecoveryMetadata(txn2.id, TransactionMetadata(ALICE_NAME, + DistributionList(ONLY_RELEVANT, mapOf(BOB_NAME to ONLY_RELEVANT))), BOB_NAME) + val txn3 = newTransaction() + transactionRecovery.addUnnotarisedTransaction(txn3) + transactionRecovery.addTransactionRecoveryMetadata(txn3.id, TransactionMetadata(ALICE_NAME, + DistributionList(ONLY_RELEVANT, mapOf(CHARLIE_NAME to NONE))), CHARLIE_NAME) + val txn4 = newTransaction() + transactionRecovery.addUnnotarisedTransaction(txn4) + transactionRecovery.addTransactionRecoveryMetadata(txn4.id, TransactionMetadata(BOB_NAME, + DistributionList(ONLY_RELEVANT, mapOf(ALICE_NAME to ALL_VISIBLE))), ALICE_NAME) + val txn5 = newTransaction() + transactionRecovery.addUnnotarisedTransaction(txn5) + transactionRecovery.addTransactionRecoveryMetadata(txn5.id, TransactionMetadata(CHARLIE_NAME, + DistributionList(ONLY_RELEVANT, mapOf(BOB_NAME to ONLY_RELEVANT))), BOB_NAME) val timeWindow = RecoveryTimeWindow(fromTime = now().minus(1, ChronoUnit.DAYS)) transactionRecovery.queryReceiverDistributionRecords(timeWindow, initiators = setOf(ALICE_NAME)).let { assertEquals(3, it.size) - assertEquals(it[0].statesToRecord, StatesToRecord.ALL_VISIBLE) - assertEquals(it[1].statesToRecord, StatesToRecord.ONLY_RELEVANT) - assertEquals(it[2].statesToRecord, StatesToRecord.NONE) + assertEquals(it[0].statesToRecord, ALL_VISIBLE) + assertEquals(it[1].statesToRecord, ONLY_RELEVANT) + assertEquals(it[2].statesToRecord, NONE) } assertEquals(1, transactionRecovery.queryReceiverDistributionRecords(timeWindow, initiators = setOf(BOB_NAME)).size) assertEquals(1, transactionRecovery.queryReceiverDistributionRecords(timeWindow, initiators = setOf(CHARLIE_NAME)).size) assertEquals(2, transactionRecovery.queryReceiverDistributionRecords(timeWindow, initiators = setOf(BOB_NAME, CHARLIE_NAME)).size) } + @Test(timeout = 300_000) + fun `transaction without peers does not store recovery metadata in database`() { + val senderTransaction = newTransaction() + transactionRecovery.addUnnotarisedTransaction(senderTransaction) + transactionRecovery.addTransactionRecoveryMetadata(senderTransaction.id, TransactionMetadata(ALICE_NAME, DistributionList(ONLY_RELEVANT, emptyMap())), ALICE_NAME) + assertEquals(IN_FLIGHT, readTransactionFromDB(senderTransaction.id).status) + assertEquals(0, readSenderDistributionRecordFromDB(senderTransaction.id).size) + } + @Test(timeout = 300_000) fun `create un-notarised transaction with flow metadata and validate status in db`() { val senderTransaction = newTransaction() - transactionRecovery.addUnnotarisedTransaction(senderTransaction, TransactionMetadata(ALICE_NAME, StatesToRecord.ALL_VISIBLE, setOf(BOB_NAME)), true) + transactionRecovery.addUnnotarisedTransaction(senderTransaction) + transactionRecovery.addTransactionRecoveryMetadata(senderTransaction.id, + TransactionMetadata(ALICE_NAME, DistributionList(ALL_VISIBLE, mapOf(BOB_NAME to ALL_VISIBLE))), ALICE_NAME) assertEquals(IN_FLIGHT, readTransactionFromDB(senderTransaction.id).status) readSenderDistributionRecordFromDB(senderTransaction.id).let { assertEquals(1, it.size) - assertEquals(StatesToRecord.ALL_VISIBLE, it[0].statesToRecord) + assertEquals(ALL_VISIBLE, it[0].statesToRecord) assertEquals(BOB_NAME, partyInfoCache.getCordaX500NameByPartyId(it[0].peerPartyId)) } val receiverTransaction = newTransaction() - transactionRecovery.addUnnotarisedTransaction(receiverTransaction, TransactionMetadata(ALICE_NAME, StatesToRecord.ONLY_RELEVANT, setOf(BOB_NAME)), false) + transactionRecovery.addUnnotarisedTransaction(receiverTransaction) + transactionRecovery.addTransactionRecoveryMetadata(receiverTransaction.id, + TransactionMetadata(ALICE_NAME, DistributionList(ONLY_RELEVANT, mapOf(BOB_NAME to ALL_VISIBLE))), BOB_NAME) assertEquals(IN_FLIGHT, readTransactionFromDB(receiverTransaction.id).status) readReceiverDistributionRecordFromDB(receiverTransaction.id).let { - assertEquals(StatesToRecord.ONLY_RELEVANT, it.statesToRecord) + assertEquals(ALL_VISIBLE, it.statesToRecord) + assertEquals(ONLY_RELEVANT, it.senderStatesToRecord) assertEquals(ALICE_NAME, partyInfoCache.getCordaX500NameByPartyId(it.initiatorPartyId)) - assertEquals(setOf(BOB_NAME), it.peerPartyIds.map { partyInfoCache.getCordaX500NameByPartyId(it) }.toSet() ) + assertEquals(setOf(BOB_NAME), it.peersToStatesToRecord.map { (peer, _) -> partyInfoCache.getCordaX500NameByPartyId(peer) }.toSet() ) } } @Test(timeout = 300_000) fun `finalize transaction with recovery metadata`() { val transaction = newTransaction(notarySig = false) - transactionRecovery.finalizeTransaction(transaction, - TransactionMetadata(ALICE_NAME), false) - + transactionRecovery.finalizeTransaction(transaction) + transactionRecovery.addTransactionRecoveryMetadata(transaction.id, + TransactionMetadata(ALICE_NAME, DistributionList(ONLY_RELEVANT, mapOf(CHARLIE_NAME to ALL_VISIBLE))), ALICE_NAME) assertEquals(VERIFIED, readTransactionFromDB(transaction.id).status) - assertEquals(StatesToRecord.ONLY_RELEVANT, readReceiverDistributionRecordFromDB(transaction.id).statesToRecord) + readSenderDistributionRecordFromDB(transaction.id).apply { + assertEquals(1, this.size) + assertEquals(ONLY_RELEVANT, this[0].statesToRecord) + } } @Test(timeout = 300_000) fun `remove un-notarised transaction and associated recovery metadata`() { val senderTransaction = newTransaction(notarySig = false) - transactionRecovery.addUnnotarisedTransaction(senderTransaction, TransactionMetadata(ALICE.name, peers = setOf(BOB.name, CHARLIE_NAME)), true) + transactionRecovery.addUnnotarisedTransaction(senderTransaction) + transactionRecovery.addTransactionRecoveryMetadata(senderTransaction.id, TransactionMetadata(ALICE.name, + DistributionList(ONLY_RELEVANT, mapOf(BOB.name to ONLY_RELEVANT, CHARLIE_NAME to ONLY_RELEVANT))), BOB.name) assertNull(transactionRecovery.getTransaction(senderTransaction.id)) assertEquals(IN_FLIGHT, readTransactionFromDB(senderTransaction.id).status) @@ -206,7 +263,9 @@ class DBTransactionStorageLedgerRecoveryTests { assertNull(transactionRecovery.getTransactionInternal(senderTransaction.id)) val receiverTransaction = newTransaction(notarySig = false) - transactionRecovery.addUnnotarisedTransaction(receiverTransaction, TransactionMetadata(ALICE.name), false) + transactionRecovery.addUnnotarisedTransaction(receiverTransaction) + transactionRecovery.addTransactionRecoveryMetadata(receiverTransaction.id, TransactionMetadata(ALICE.name, + DistributionList(ONLY_RELEVANT, mapOf(BOB.name to ONLY_RELEVANT))), ALICE.name) assertNull(transactionRecovery.getTransaction(receiverTransaction.id)) assertEquals(IN_FLIGHT, readTransactionFromDB(receiverTransaction.id).status) diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageTests.kt b/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageTests.kt index 3a4b8615b3..0d4b380375 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageTests.kt @@ -10,7 +10,6 @@ import net.corda.core.crypto.SignableData import net.corda.core.crypto.SignatureMetadata import net.corda.core.crypto.TransactionSignature import net.corda.core.crypto.sign -import net.corda.core.flows.TransactionMetadata import net.corda.core.serialization.deserialize import net.corda.core.toFuture import net.corda.core.transactions.SignedTransaction @@ -109,7 +108,7 @@ class DBTransactionStorageTests { val transactionClock = TransactionClock(now) newTransactionStorage(clock = transactionClock) val transaction = newTransaction() - transactionStorage.addUnnotarisedTransaction(transaction, TransactionMetadata(ALICE.party.name), true) + transactionStorage.addUnnotarisedTransaction(transaction) assertEquals(IN_FLIGHT, readTransactionFromDB(transaction.id).status) } @@ -132,7 +131,7 @@ class DBTransactionStorageTests { val transactionClock = TransactionClock(now) newTransactionStorage(clock = transactionClock) val transaction = newTransaction(notarySig = false) - transactionStorage.addUnnotarisedTransaction(transaction, TransactionMetadata(ALICE.party.name), true) + transactionStorage.addUnnotarisedTransaction(transaction) assertNull(transactionStorage.getTransaction(transaction.id)) assertEquals(IN_FLIGHT, readTransactionFromDB(transaction.id).status) transactionStorage.finalizeTransactionWithExtraSignatures(transaction, emptyList()) @@ -148,7 +147,7 @@ class DBTransactionStorageTests { val transactionClock = TransactionClock(now) newTransactionStorage(clock = transactionClock) val transaction = newTransaction(notarySig = false) - transactionStorage.addUnnotarisedTransaction(transaction, TransactionMetadata(ALICE.party.name), true) + transactionStorage.addUnnotarisedTransaction(transaction) assertNull(transactionStorage.getTransaction(transaction.id)) assertEquals(IN_FLIGHT, readTransactionFromDB(transaction.id).status) val notarySig = notarySig(transaction.id) @@ -165,7 +164,7 @@ class DBTransactionStorageTests { val transactionClock = TransactionClock(now) newTransactionStorage(clock = transactionClock) val transaction = newTransaction(notarySig = false) - transactionStorage.addUnnotarisedTransaction(transaction, TransactionMetadata(ALICE.party.name), true) + transactionStorage.addUnnotarisedTransaction(transaction) assertNull(transactionStorage.getTransaction(transaction.id)) assertEquals(IN_FLIGHT, readTransactionFromDB(transaction.id).status) @@ -199,7 +198,7 @@ class DBTransactionStorageTests { val transactionWithoutNotarySig = newTransaction(notarySig = false) // txn recorded as un-notarised (simulate ReceiverFinalityFlow in initial flow) - transactionStorage.addUnnotarisedTransaction(transactionWithoutNotarySig, TransactionMetadata(ALICE.party.name), false) + transactionStorage.addUnnotarisedTransaction(transactionWithoutNotarySig) assertEquals(IN_FLIGHT, readTransactionFromDB(transactionWithoutNotarySig.id).status) // txn then recorded as unverified (simulate ResolveTransactionFlow in follow-up flow) @@ -230,7 +229,7 @@ class DBTransactionStorageTests { val transactionWithoutNotarySigs = newTransaction(notarySig = false) // txn recorded as un-notarised (simulate ReceiverFinalityFlow in initial flow) - transactionStorage.addUnnotarisedTransaction(transactionWithoutNotarySigs, TransactionMetadata(ALICE.party.name), false) + transactionStorage.addUnnotarisedTransaction(transactionWithoutNotarySigs) assertEquals(IN_FLIGHT, readTransactionFromDB(transactionWithoutNotarySigs.id).status) // txn then recorded as unverified (simulate ResolveTransactionFlow in follow-up flow) diff --git a/testing/cordapps/cashobservers/src/main/kotlin/net/corda/finance/test/flows/CashIssueWithObserversFlow.kt b/testing/cordapps/cashobservers/src/main/kotlin/net/corda/finance/test/flows/CashIssueWithObserversFlow.kt index 9829f0e4c5..c860617078 100644 --- a/testing/cordapps/cashobservers/src/main/kotlin/net/corda/finance/test/flows/CashIssueWithObserversFlow.kt +++ b/testing/cordapps/cashobservers/src/main/kotlin/net/corda/finance/test/flows/CashIssueWithObserversFlow.kt @@ -2,17 +2,22 @@ package net.corda.finance.test.flows import co.paralleluniverse.fibers.Suspendable import net.corda.core.contracts.Amount +import net.corda.core.flows.FinalityFlow import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowSession import net.corda.core.flows.InitiatedBy import net.corda.core.flows.InitiatingFlow +import net.corda.core.flows.NotaryException import net.corda.core.flows.ReceiveFinalityFlow import net.corda.core.flows.StartableByRPC import net.corda.core.identity.Party +import net.corda.core.node.StatesToRecord +import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.OpaqueBytes import net.corda.finance.contracts.asset.Cash import net.corda.finance.flows.AbstractCashFlow +import net.corda.finance.flows.CashException import net.corda.finance.issuedBy import java.util.Currency @@ -32,9 +37,18 @@ class CashIssueWithObserversFlow(private val amount: Amount, val tx = serviceHub.signInitialTransaction(builder, signers) progressTracker.currentStep = Companion.FINALISING_TX val observerSessions = observers.map { initiateFlow(it) } - val notarised = finaliseTx(tx, observerSessions, "Unable to notarise issue") + val notarised = finalise(tx, observerSessions, "Unable to notarise issue") return Result(notarised, ourIdentity) } + + @Suspendable + private fun finalise(tx: SignedTransaction, sessions: Collection, message: String): SignedTransaction { + try { + return subFlow(FinalityFlow(tx, sessions)) + } catch (e: NotaryException) { + throw CashException(message, e) + } + } } @InitiatedBy(CashIssueWithObserversFlow::class) @@ -42,7 +56,7 @@ class CashIssueReceiverFlowWithObservers(private val otherSide: FlowSession) : F @Suspendable override fun call() { if (!serviceHub.myInfo.isLegalIdentity(otherSide.counterparty)) { - subFlow(ReceiveFinalityFlow(otherSide)) + subFlow(ReceiveFinalityFlow(otherSide, statesToRecord = StatesToRecord.ALL_VISIBLE)) } } } \ No newline at end of file diff --git a/testing/cordapps/cashobservers/src/main/kotlin/net/corda/finance/test/flows/CashPaymentWithObserversFlow.kt b/testing/cordapps/cashobservers/src/main/kotlin/net/corda/finance/test/flows/CashPaymentWithObserversFlow.kt new file mode 100644 index 0000000000..5192ac3ad4 --- /dev/null +++ b/testing/cordapps/cashobservers/src/main/kotlin/net/corda/finance/test/flows/CashPaymentWithObserversFlow.kt @@ -0,0 +1,82 @@ +package net.corda.finance.test.flows + +import co.paralleluniverse.fibers.Suspendable +import net.corda.core.contracts.Amount +import net.corda.core.contracts.InsufficientBalanceException +import net.corda.core.flows.FinalityFlow +import net.corda.core.flows.FlowLogic +import net.corda.core.flows.FlowSession +import net.corda.core.flows.InitiatedBy +import net.corda.core.flows.InitiatingFlow +import net.corda.core.flows.NotaryException +import net.corda.core.flows.ReceiveFinalityFlow +import net.corda.core.flows.StartableByRPC +import net.corda.core.identity.Party +import net.corda.core.transactions.SignedTransaction +import net.corda.core.transactions.TransactionBuilder +import net.corda.finance.flows.AbstractCashFlow +import net.corda.finance.flows.CashException +import net.corda.finance.workflows.asset.CashUtils +import java.util.Currency + +@StartableByRPC +@InitiatingFlow +open class CashPaymentWithObserversFlow( + val amount: Amount, + val recipient: Party, + val observers: Set, + private val useObserverSessions: Boolean = false +) : AbstractCashFlow(tracker()) { + + @Suspendable + override fun call(): SignedTransaction { + val recipientSession = initiateFlow(recipient) + val observerSessions = observers.map { initiateFlow(it) } + val builder = TransactionBuilder(notary = serviceHub.networkMapCache.notaryIdentities.first()) + logger.info("Generating spend for: ${builder.lockId}") + val (spendTX, keysForSigning) = try { + CashUtils.generateSpend( + serviceHub, + builder, + amount, + ourIdentityAndCert, + recipient + ) + } catch (e: InsufficientBalanceException) { + throw CashException("Insufficient cash for spend: ${e.message}", e) + } + + logger.info("Signing transaction for: ${spendTX.lockId}") + val tx = serviceHub.signInitialTransaction(spendTX, keysForSigning) + + logger.info("Finalising transaction for: ${tx.id}") + val sessionsForFinality = if (serviceHub.myInfo.isLegalIdentity(recipient)) emptyList() else listOf(recipientSession) + val notarised = finalise(tx, sessionsForFinality, observerSessions) + logger.info("Finalised transaction for: ${notarised.id}") + return notarised + } + + @Suspendable + private fun finalise(tx: SignedTransaction, + sessions: Collection, + observerSessions: Collection): SignedTransaction { + try { + return if (useObserverSessions) + subFlow(FinalityFlow(tx, sessions, observerSessions = observerSessions)) + else + subFlow(FinalityFlow(tx, sessions + observerSessions)) + } catch (e: NotaryException) { + throw CashException("Unable to notarise spend", e) + } + } +} + +@InitiatedBy(CashPaymentWithObserversFlow::class) +class CashPaymentReceiverWithObserversFlow(private val otherSide: FlowSession) : FlowLogic() { + @Suspendable + override fun call() { + if (!serviceHub.myInfo.isLegalIdentity(otherSide.counterparty)) { + subFlow(ReceiveFinalityFlow(otherSide)) + } + } +} diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/MockTransactionStorage.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/MockTransactionStorage.kt index 156651efd7..d9f26e9469 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/MockTransactionStorage.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/MockTransactionStorage.kt @@ -11,6 +11,7 @@ import net.corda.core.transactions.SignedTransaction import net.corda.node.services.api.WritableTransactionStorage import net.corda.core.flows.TransactionMetadata import net.corda.core.flows.TransactionStatus +import net.corda.core.identity.CordaX500Name import net.corda.testing.node.MockServices import rx.Observable import rx.subjects.PublishSubject @@ -55,15 +56,17 @@ open class MockTransactionStorage : WritableTransactionStorage, SingletonSeriali } } - override fun addUnnotarisedTransaction(transaction: SignedTransaction, metadata: TransactionMetadata, isInitiator: Boolean): Boolean { + override fun addUnnotarisedTransaction(transaction: SignedTransaction): Boolean { return txns.putIfAbsent(transaction.id, TxHolder(transaction, status = TransactionStatus.IN_FLIGHT)) == null } + override fun addTransactionRecoveryMetadata(id: SecureHash, metadata: TransactionMetadata, caller: CordaX500Name) { } + override fun removeUnnotarisedTransaction(id: SecureHash): Boolean { return txns.remove(id) != null } - override fun finalizeTransaction(transaction: SignedTransaction, metadata: TransactionMetadata, isInitiator: Boolean) = + override fun finalizeTransaction(transaction: SignedTransaction) = addTransaction(transaction) override fun finalizeTransactionWithExtraSignatures(transaction: SignedTransaction, signatures: Collection): Boolean { diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TestDSL.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TestDSL.kt index 5d0f74d25e..f7858056e5 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TestDSL.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TestDSL.kt @@ -9,6 +9,7 @@ import net.corda.core.crypto.SecureHash import net.corda.core.crypto.TransactionSignature import net.corda.core.flows.FlowException import net.corda.core.flows.TransactionMetadata +import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party import net.corda.core.internal.* import net.corda.core.internal.notary.NotaryService @@ -139,13 +140,15 @@ data class TestTransactionDSLInterpreter private constructor( override val attachmentsClassLoaderCache: AttachmentsClassLoaderCache = AttachmentsClassLoaderCacheImpl(TestingNamedCacheFactory()) - override fun recordUnnotarisedTransaction(txn: SignedTransaction, metadata: TransactionMetadata) {} + override fun recordUnnotarisedTransaction(txn: SignedTransaction) {} override fun removeUnnotarisedTransaction(id: SecureHash) {} override fun finalizeTransactionWithExtraSignatures(txn: SignedTransaction, sigs: Collection, statesToRecord: StatesToRecord) {} - override fun finalizeTransaction(txn: SignedTransaction, statesToRecord: StatesToRecord, metadata: TransactionMetadata) {} + override fun finalizeTransaction(txn: SignedTransaction, statesToRecord: StatesToRecord) {} + + override fun recordTransactionRecoveryMetadata(txnId: SecureHash, txnMetadata: TransactionMetadata, caller: CordaX500Name) {} } private fun copy(): TestTransactionDSLInterpreter = From f791adf4429521f0b25c80a52f9fa9213df81870 Mon Sep 17 00:00:00 2001 From: Jose Coll Date: Mon, 5 Jun 2023 16:59:06 +0100 Subject: [PATCH 029/126] ENT-9924 (Follow-up) Update recording of transaction flow recovery metadata into Send/Receive transaction flows. (#7382) --- .../coretests/flows/FinalityFlowTests.kt | 7 -- .../core/flows/ReceiveTransactionFlow.kt | 17 +-- .../corda/core/flows/SendTransactionFlow.kt | 7 +- .../core/internal/ServiceHubCoreInternal.kt | 17 ++- ...cContractWithSerializationWhitelistTest.kt | 2 + .../node/services/api/ServiceHubInternal.kt | 28 +++-- .../persistence/DBTransactionStorage.kt | 5 +- .../DBTransactionStorageLedgerRecovery.kt | 108 ++++++++++++------ .../migration/node-core.changelog-v25.xml | 3 - .../node/messaging/TwoPartyTradeFlowTests.kt | 11 +- ...DBTransactionStorageLedgerRecoveryTests.kt | 83 ++++++++------ .../node/internal/MockTransactionStorage.kt | 5 +- .../kotlin/net/corda/testing/dsl/TestDSL.kt | 4 +- 13 files changed, 182 insertions(+), 115 deletions(-) diff --git a/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt b/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt index 35dc41dadd..a6ce9e44ad 100644 --- a/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt +++ b/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt @@ -400,13 +400,6 @@ class FinalityFlowTests : WithFinality { assertEquals(mapOf(BOB_NAME.hashCode().toLong() to StatesToRecord.ONLY_RELEVANT, CHARLIE_NAME.hashCode().toLong() to StatesToRecord.ALL_VISIBLE), this?.peersToStatesToRecord) } - getReceiverRecoveryData(stx.id, charlieNode.database).apply { - assertEquals(StatesToRecord.ONLY_RELEVANT, this?.statesToRecord) - assertEquals(StatesToRecord.ONLY_RELEVANT, this?.senderStatesToRecord) - // note: Charlie assertion here is using actually default StatesToRecord.ONLY_RELEVANT - assertEquals(mapOf(BOB_NAME.hashCode().toLong() to StatesToRecord.ONLY_RELEVANT, - CHARLIE_NAME.hashCode().toLong() to StatesToRecord.ONLY_RELEVANT), this?.peersToStatesToRecord) - } // exercise the new FinalityFlow observerSessions constructor parameter val stx3 = aliceNode.startFlowAndRunNetwork(CashPaymentWithObserversFlow( diff --git a/core/src/main/kotlin/net/corda/core/flows/ReceiveTransactionFlow.kt b/core/src/main/kotlin/net/corda/core/flows/ReceiveTransactionFlow.kt index 6b46c96de5..435b14605b 100644 --- a/core/src/main/kotlin/net/corda/core/flows/ReceiveTransactionFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/ReceiveTransactionFlow.kt @@ -62,7 +62,7 @@ open class ReceiveTransactionFlow constructor(private val otherSideSession: Flow val payload = otherSideSession.receive().unwrap { it } val stx = if (payload is SignedTransactionWithDistributionList) { - recordTransactionMetadata(payload.stx, payload.distributionList) + (serviceHub as ServiceHubCoreInternal).recordReceiverTransactionRecoveryMetadata(payload.stx.id, otherSideSession.counterparty.name, ourIdentity.name, statesToRecord, payload.distributionList) payload.stx } else payload as SignedTransaction stx.pushToLoggingContext() @@ -87,21 +87,6 @@ open class ReceiveTransactionFlow constructor(private val otherSideSession: Flow return stx } - @Suspendable - private fun recordTransactionMetadata(stx: SignedTransaction, distributionList: DistributionList?) { - distributionList?.let { - val txnMetadata = TransactionMetadata(otherSideSession.counterparty.name, - DistributionList(distributionList.senderStatesToRecord, - distributionList.peersToStatesToRecord.map { (peer, peerStatesToRecord) -> - if (peer == ourIdentity.name) - peer to statesToRecord // use actual value - else - peer to peerStatesToRecord // use hinted value - }.toMap())) - (serviceHub as ServiceHubCoreInternal).recordTransactionRecoveryMetadata(stx.id, txnMetadata, ourIdentity.name) - } - } - /** * Hook to perform extra checks on the received transaction just before it's recorded. The transaction has already * been resolved and verified at this point. diff --git a/core/src/main/kotlin/net/corda/core/flows/SendTransactionFlow.kt b/core/src/main/kotlin/net/corda/core/flows/SendTransactionFlow.kt index 68f89469d2..a02a105961 100644 --- a/core/src/main/kotlin/net/corda/core/flows/SendTransactionFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/SendTransactionFlow.kt @@ -142,9 +142,8 @@ open class DataVendingFlow(val otherSideSession: FlowSession, val payload: Any, val useTwoPhaseFinality = serviceHub.myInfo.platformVersion >= PlatformVersionSwitches.TWO_PHASE_FINALITY val toTwoPhaseFinalityNode = serviceHub.networkMapCache.getNodeByLegalIdentity(otherSideSession.counterparty)?.platformVersion!! >= PlatformVersionSwitches.TWO_PHASE_FINALITY if (txnMetadata != null && toTwoPhaseFinalityNode && useTwoPhaseFinality && payload is SignedTransaction) { - payload = SignedTransactionWithDistributionList(payload, txnMetadata.distributionList) - if (txnMetadata.persist) - (serviceHub as ServiceHubCoreInternal).recordTransactionRecoveryMetadata(payload.stx.id, txnMetadata.copy(initiator = ourIdentity.name), ourIdentity.name) + val encryptedDistributionList = (serviceHub as ServiceHubCoreInternal).recordSenderTransactionRecoveryMetadata(payload.id, txnMetadata.copy(initiator = ourIdentity.name)) + payload = SignedTransactionWithDistributionList(payload, encryptedDistributionList!!) } // This loop will receive [FetchDataFlow.Request] continuously until the `otherSideSession` has all the data they need @@ -274,5 +273,5 @@ open class DataVendingFlow(val otherSideSession: FlowSession, val payload: Any, @CordaSerializable data class SignedTransactionWithDistributionList( val stx: SignedTransaction, - val distributionList: DistributionList + val distributionList: ByteArray ) \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/internal/ServiceHubCoreInternal.kt b/core/src/main/kotlin/net/corda/core/internal/ServiceHubCoreInternal.kt index 239c37166e..f5febbad97 100644 --- a/core/src/main/kotlin/net/corda/core/internal/ServiceHubCoreInternal.kt +++ b/core/src/main/kotlin/net/corda/core/internal/ServiceHubCoreInternal.kt @@ -65,13 +65,24 @@ interface ServiceHubCoreInternal : ServiceHub { fun finalizeTransaction(txn: SignedTransaction, statesToRecord: StatesToRecord) /** - * Records [TransactionMetadata] for a given txnId. + * Records Sender [TransactionMetadata] for a given txnId. * * @param txnId The SecureHash of a transaction. * @param txnMetadata The recovery metadata associated with a transaction. - * @param caller The CordaX500Name of the party calling this operation. + * @return encrypted distribution list (hashed peers -> StatesToRecord values). */ - fun recordTransactionRecoveryMetadata(txnId: SecureHash, txnMetadata: TransactionMetadata, caller: CordaX500Name) + fun recordSenderTransactionRecoveryMetadata(txnId: SecureHash, txnMetadata: TransactionMetadata): ByteArray? + + /** + * Records Received [TransactionMetadata] for a given txnId. + * + * @param txnId The SecureHash of a transaction. + * @param sender The sender of the transaction. + * @param receiver The receiver of the transaction. + * @param receiverStatesToRecord The StatesToRecord value of the receiver. + * @param encryptedDistributionList encrypted distribution list (hashed peers -> StatesToRecord values) + */ + fun recordReceiverTransactionRecoveryMetadata(txnId: SecureHash, sender: CordaX500Name, receiver: CordaX500Name, receiverStatesToRecord: StatesToRecord, encryptedDistributionList: ByteArray) } interface TransactionsResolver { diff --git a/node/src/integration-test/kotlin/net/corda/node/services/DeterministicContractWithSerializationWhitelistTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/DeterministicContractWithSerializationWhitelistTest.kt index 9b0e057453..af905d76f3 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/DeterministicContractWithSerializationWhitelistTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/DeterministicContractWithSerializationWhitelistTest.kt @@ -20,11 +20,13 @@ import net.corda.testing.node.internal.cordappWithPackages import org.assertj.core.api.Assertions.assertThat import org.junit.BeforeClass import org.junit.ClassRule +import org.junit.Ignore import org.junit.Test import org.junit.jupiter.api.assertDoesNotThrow import org.junit.jupiter.api.assertThrows @Suppress("FunctionName") +@Ignore class DeterministicContractWithSerializationWhitelistTest { companion object { val logger = loggerFor() diff --git a/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt b/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt index fb7a6d9f16..a5ef5f054a 100644 --- a/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt +++ b/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt @@ -195,8 +195,11 @@ interface ServiceHubInternal : ServiceHubCoreInternal { override fun recordTransactions(statesToRecord: StatesToRecord, txs: Iterable) = recordTransactions(statesToRecord, txs, SIGNATURE_VERIFICATION_DISABLED) - override fun recordTransactionRecoveryMetadata(txnId: SecureHash, txnMetadata: TransactionMetadata, caller: CordaX500Name) = - validatedTransactions.addTransactionRecoveryMetadata(txnId, txnMetadata, caller) + override fun recordSenderTransactionRecoveryMetadata(txnId: SecureHash, txnMetadata: TransactionMetadata) = + validatedTransactions.addSenderTransactionRecoveryMetadata(txnId, txnMetadata) + + override fun recordReceiverTransactionRecoveryMetadata(txnId: SecureHash, sender: CordaX500Name, receiver: CordaX500Name, receiverStatesToRecord: StatesToRecord, encryptedDistributionList: ByteArray) = + validatedTransactions.addReceiverTransactionRecoveryMetadata(txnId, sender, receiver, receiverStatesToRecord, encryptedDistributionList) @Suppress("NestedBlockDepth") @VisibleForTesting @@ -367,13 +370,24 @@ interface WritableTransactionStorage : TransactionStorage { fun addUnnotarisedTransaction(transaction: SignedTransaction): Boolean /** - * Record transaction recovery metadata for a given transaction id. + * Records Sender [TransactionMetadata] for a given txnId. * - * @param id The SecureHash of the transaction to be recorded. - * @param metadata transaction recovery metadata. - * @param caller The CordaX500Name of the party calling this operation. + * @param id The SecureHash of a transaction. + * @param metadata The recovery metadata associated with a transaction. + * @return encrypted distribution list (hashed peers -> StatesToRecord values). */ - fun addTransactionRecoveryMetadata(id: SecureHash, metadata: TransactionMetadata, caller: CordaX500Name) + fun addSenderTransactionRecoveryMetadata(id: SecureHash, metadata: TransactionMetadata): ByteArray? + + /** + * Records Received [TransactionMetadata] for a given txnId. + * + * @param id The SecureHash of a transaction. + * @param sender The sender of the transaction. + * @param receiver The receiver of the transaction. + * @param receiverStatesToRecord The StatesToRecord value of the receiver. + * @param encryptedDistributionList encrypted distribution list (hashed peers -> StatesToRecord values) + */ + fun addReceiverTransactionRecoveryMetadata(id: SecureHash, sender: CordaX500Name, receiver: CordaX500Name, receiverStatesToRecord: StatesToRecord, encryptedDistributionList: ByteArray) /** * Removes an un-notarised transaction (with a status of *MISSING_TRANSACTION_SIG*) from the data store. diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt index 597d1a8675..56acdfd61b 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt @@ -11,6 +11,7 @@ import net.corda.core.internal.VisibleForTesting import net.corda.core.internal.bufferUntilSubscribed import net.corda.core.internal.concurrent.doneFuture import net.corda.core.messaging.DataFeed +import net.corda.core.node.StatesToRecord import net.corda.core.serialization.SerializationContext import net.corda.core.serialization.SerializationDefaults import net.corda.core.serialization.SerializedBytes @@ -214,7 +215,9 @@ open class DBTransactionStorage(private val database: CordaPersistence, cacheFac false } - override fun addTransactionRecoveryMetadata(id: SecureHash, metadata: TransactionMetadata, caller: CordaX500Name) { } + override fun addSenderTransactionRecoveryMetadata(id: SecureHash, metadata: TransactionMetadata): ByteArray? { return null } + + override fun addReceiverTransactionRecoveryMetadata(id: SecureHash, sender: CordaX500Name, receiver: CordaX500Name, receiverStatesToRecord: StatesToRecord, encryptedDistributionList: ByteArray) { } override fun finalizeTransaction(transaction: SignedTransaction) = addTransaction(transaction) { diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecovery.kt b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecovery.kt index 5abbff52a4..159fe1383a 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecovery.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecovery.kt @@ -8,17 +8,17 @@ import net.corda.core.internal.NamedCacheFactory import net.corda.core.node.StatesToRecord import net.corda.core.node.services.vault.Sort import net.corda.core.serialization.CordaSerializable -import net.corda.core.serialization.deserialize -import net.corda.core.serialization.serialize import net.corda.node.CordaClock import net.corda.node.services.network.PersistentPartyInfoCache import net.corda.nodeapi.internal.cryptoservice.CryptoService import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.NODE_DATABASE_PREFIX -import net.corda.serialization.internal.CordaSerializationEncoding import org.hibernate.annotations.Immutable +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.DataInputStream +import java.io.DataOutputStream import java.io.Serializable -import java.lang.IllegalStateException import java.time.Instant import java.util.concurrent.atomic.AtomicLong import javax.persistence.Column @@ -87,37 +87,34 @@ class DBTransactionStorageLedgerRecovery(private val database: CordaPersistence, @Column(name = "sender_party_id", nullable = true) val senderPartyId: Long, - /** Encrypted information for use by Sender (eg. partyId's of flow peers) **/ + /** Encrypted recovery information for sole use by Sender **/ @Lob @Column(name = "distribution_list", nullable = false) val distributionList: ByteArray, /** states to record: NONE, ALL_VISIBLE, ONLY_RELEVANT */ @Column(name = "receiver_states_to_record", nullable = false) - val receiverStatesToRecord: StatesToRecord, - - /** states to record: NONE, ALL_VISIBLE, ONLY_RELEVANT */ - @Column(name = "sender_states_to_record", nullable = false) - val senderStatesToRecord: StatesToRecord + val receiverStatesToRecord: StatesToRecord ) { - constructor(key: Key, txId: SecureHash, initiatorPartyId: Long, peersToStatesToRecord: Map, senderStatesToRecord: StatesToRecord, receiverStatesToRecord: StatesToRecord, cryptoService: CryptoService) : + constructor(key: Key, txId: SecureHash, initiatorPartyId: Long, encryptedDistributionList: ByteArray, receiverStatesToRecord: StatesToRecord) : this(PersistentKey(key), txId = txId.toString(), senderPartyId = initiatorPartyId, - distributionList = cryptoService.encrypt(peersToStatesToRecord.serialize(context = contextToUse().withEncoding(CordaSerializationEncoding.SNAPPY)).bytes), - receiverStatesToRecord = receiverStatesToRecord, - senderStatesToRecord = senderStatesToRecord + distributionList = encryptedDistributionList, + receiverStatesToRecord = receiverStatesToRecord ) - fun toReceiverDistributionRecord(cryptoService: CryptoService) = - ReceiverDistributionRecord( + fun toReceiverDistributionRecord(cryptoService: CryptoService): ReceiverDistributionRecord { + val hashedDL = HashedDistributionList.deserialize(cryptoService.decrypt(this.distributionList)) + return ReceiverDistributionRecord( SecureHash.parse(this.txId), this.senderPartyId, - cryptoService.decrypt(this.distributionList).deserialize(context = contextToUse()), + hashedDL.peerHashToStatesToRecord, this.receiverStatesToRecord, - this.senderStatesToRecord, + hashedDL.senderStatesToRecord, this.compositeKey.timestamp ) + } } @Entity @@ -142,10 +139,9 @@ class DBTransactionStorageLedgerRecovery(private val database: CordaPersistence, } } - @Suppress("IMPLICIT_CAST_TO_ANY") - override fun addTransactionRecoveryMetadata(id: SecureHash, metadata: TransactionMetadata, caller: CordaX500Name) { - database.transaction { - if (caller == metadata.initiator) { + override fun addSenderTransactionRecoveryMetadata(id: SecureHash, metadata: TransactionMetadata): ByteArray { + return database.transaction { + if (metadata.persist) metadata.distributionList.peersToStatesToRecord.map { (peer, _) -> val senderDistributionRecord = DBSenderDistributionRecord(PersistentKey(Key(clock.instant())), id.toString(), @@ -153,19 +149,22 @@ class DBTransactionStorageLedgerRecovery(private val database: CordaPersistence, metadata.distributionList.senderStatesToRecord) session.save(senderDistributionRecord) } - } else { - val receiverStatesToRecord = metadata.distributionList.peersToStatesToRecord[caller] ?: throw IllegalStateException("Missing peer $caller in distribution list of Receiver recovery metadata") - val receiverDistributionRecord = - DBReceiverDistributionRecord(Key(clock.instant()), - id, - partyInfoCache.getPartyIdByCordaX500Name(metadata.initiator), - metadata.distributionList.peersToStatesToRecord.map { (peer, statesToRecord) -> - partyInfoCache.getPartyIdByCordaX500Name(peer) to statesToRecord }.toMap(), - metadata.distributionList.senderStatesToRecord, - receiverStatesToRecord, - cryptoService) - session.save(receiverDistributionRecord) - } + val hashedPeersToStatesToRecord = metadata.distributionList.peersToStatesToRecord.map { (peer, statesToRecord) -> + partyInfoCache.getPartyIdByCordaX500Name(peer) to statesToRecord }.toMap() + val hashedDistributionList = HashedDistributionList(metadata.distributionList.senderStatesToRecord, hashedPeersToStatesToRecord) + cryptoService.encrypt(hashedDistributionList.serialize()) + } + } + + override fun addReceiverTransactionRecoveryMetadata(id: SecureHash, sender: CordaX500Name, receiver: CordaX500Name, receiverStatesToRecord: StatesToRecord, encryptedDistributionList: ByteArray) { + database.transaction { + val receiverDistributionRecord = + DBReceiverDistributionRecord(Key(clock.instant()), + id, + partyInfoCache.getPartyIdByCordaX500Name(sender), + encryptedDistributionList, + receiverStatesToRecord) + session.save(receiverDistributionRecord) } } @@ -285,7 +284,7 @@ private fun CryptoService.decrypt(bytes: ByteArray): ByteArray { } // TO DO: https://r3-cev.atlassian.net/browse/ENT-9876 -private fun CryptoService.encrypt(bytes: ByteArray): ByteArray { +fun CryptoService.encrypt(bytes: ByteArray): ByteArray { return bytes } @@ -319,5 +318,42 @@ enum class DistributionRecordType { SENDER, RECEIVER, ALL } +@CordaSerializable +data class HashedDistributionList( + val senderStatesToRecord: StatesToRecord, + val peerHashToStatesToRecord: Map +) { + fun serialize(): ByteArray { + val baos = ByteArrayOutputStream() + val out = DataOutputStream(baos) + out.use { + out.writeByte(SERIALIZER_VERSION_ID) + out.writeByte(senderStatesToRecord.ordinal) + out.writeInt(peerHashToStatesToRecord.size) + for(entry in peerHashToStatesToRecord) { + out.writeLong(entry.key) + out.writeByte(entry.value.ordinal) + } + out.flush() + return baos.toByteArray() + } + } + companion object { + const val SERIALIZER_VERSION_ID = 1 + fun deserialize(bytes: ByteArray): HashedDistributionList { + val input = DataInputStream(ByteArrayInputStream(bytes)) + input.use { + assert(input.readByte().toInt() == SERIALIZER_VERSION_ID) { "Serialization version conflict." } + val senderStatesToRecord = StatesToRecord.values()[input.readByte().toInt()] + val numPeerHashToStatesToRecords = input.readInt() + val peerHashToStatesToRecord = mutableMapOf() + repeat (numPeerHashToStatesToRecords) { + peerHashToStatesToRecord[input.readLong()] = StatesToRecord.values()[input.readByte().toInt()] + } + return HashedDistributionList(senderStatesToRecord, peerHashToStatesToRecord) + } + } + } +} diff --git a/node/src/main/resources/migration/node-core.changelog-v25.xml b/node/src/main/resources/migration/node-core.changelog-v25.xml index d28037153f..d926460ba3 100644 --- a/node/src/main/resources/migration/node-core.changelog-v25.xml +++ b/node/src/main/resources/migration/node-core.changelog-v25.xml @@ -60,9 +60,6 @@ - - - diff --git a/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt b/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt index bd25b3c512..6d8c7e8392 100644 --- a/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt +++ b/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt @@ -31,6 +31,7 @@ import net.corda.core.internal.concurrent.map import net.corda.core.internal.rootCause import net.corda.core.messaging.DataFeed import net.corda.core.messaging.StateMachineTransactionMapping +import net.corda.core.node.StatesToRecord import net.corda.core.node.services.Vault import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.SingletonSerializeAsToken @@ -809,9 +810,15 @@ class TwoPartyTradeFlowTests(private val anonymous: Boolean) { return true } - override fun addTransactionRecoveryMetadata(id: SecureHash, metadata: TransactionMetadata, caller: CordaX500Name) { + override fun addSenderTransactionRecoveryMetadata(id: SecureHash, metadata: TransactionMetadata): ByteArray? { + return database.transaction { + delegate.addSenderTransactionRecoveryMetadata(id, metadata) + } + } + + override fun addReceiverTransactionRecoveryMetadata(id: SecureHash, sender: CordaX500Name, receiver: CordaX500Name, receiverStatesToRecord: StatesToRecord, encryptedDistributionList: ByteArray) { database.transaction { - delegate.addTransactionRecoveryMetadata(id, metadata, caller) + delegate.addReceiverTransactionRecoveryMetadata(id, sender, receiver, receiverStatesToRecord, encryptedDistributionList) } } diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecoveryTests.kt b/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecoveryTests.kt index 3766e5629f..c4ef9b2f48 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecoveryTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecoveryTests.kt @@ -7,12 +7,12 @@ import net.corda.core.crypto.SignableData import net.corda.core.crypto.SignatureMetadata import net.corda.core.crypto.sign import net.corda.core.flows.DistributionList -import net.corda.core.flows.TransactionMetadata import net.corda.core.flows.RecoveryTimeWindow +import net.corda.core.flows.TransactionMetadata import net.corda.core.node.NodeInfo import net.corda.core.node.StatesToRecord.ALL_VISIBLE -import net.corda.core.node.StatesToRecord.ONLY_RELEVANT import net.corda.core.node.StatesToRecord.NONE +import net.corda.core.node.StatesToRecord.ONLY_RELEVANT import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.WireTransaction import net.corda.core.utilities.NetworkHostAndPort @@ -84,7 +84,7 @@ class DBTransactionStorageLedgerRecoveryTests { val beforeFirstTxn = now() val txn = newTransaction() transactionRecovery.addUnnotarisedTransaction(txn) - transactionRecovery.addTransactionRecoveryMetadata(txn.id, TransactionMetadata(ALICE_NAME, DistributionList(ALL_VISIBLE, mapOf(BOB_NAME to ONLY_RELEVANT))), ALICE_NAME) + transactionRecovery.addSenderTransactionRecoveryMetadata(txn.id, TransactionMetadata(ALICE_NAME, DistributionList(ALL_VISIBLE, mapOf(BOB_NAME to ONLY_RELEVANT)))) val timeWindow = RecoveryTimeWindow(fromTime = beforeFirstTxn, untilTime = beforeFirstTxn.plus(1, ChronoUnit.MINUTES)) val results = transactionRecovery.querySenderDistributionRecords(timeWindow) @@ -93,7 +93,7 @@ class DBTransactionStorageLedgerRecoveryTests { val afterFirstTxn = now() val txn2 = newTransaction() transactionRecovery.addUnnotarisedTransaction(txn2) - transactionRecovery.addTransactionRecoveryMetadata(txn2.id, TransactionMetadata(ALICE_NAME, DistributionList(ONLY_RELEVANT, mapOf(CHARLIE_NAME to ONLY_RELEVANT))), ALICE_NAME) + transactionRecovery.addSenderTransactionRecoveryMetadata(txn2.id, TransactionMetadata(ALICE_NAME, DistributionList(ONLY_RELEVANT, mapOf(CHARLIE_NAME to ONLY_RELEVANT)))) assertEquals(2, transactionRecovery.querySenderDistributionRecords(timeWindow).size) assertEquals(1, transactionRecovery.querySenderDistributionRecords(RecoveryTimeWindow(fromTime = afterFirstTxn)).size) } @@ -102,10 +102,10 @@ class DBTransactionStorageLedgerRecoveryTests { fun `query local ledger for transactions within timeWindow and excluding remoteTransactionIds`() { val transaction1 = newTransaction() transactionRecovery.addUnnotarisedTransaction(transaction1) - transactionRecovery.addTransactionRecoveryMetadata(transaction1.id, TransactionMetadata(ALICE_NAME, DistributionList(ALL_VISIBLE, mapOf(BOB_NAME to ONLY_RELEVANT))), ALICE_NAME) + transactionRecovery.addSenderTransactionRecoveryMetadata(transaction1.id, TransactionMetadata(ALICE_NAME, DistributionList(ALL_VISIBLE, mapOf(BOB_NAME to ONLY_RELEVANT)))) val transaction2 = newTransaction() transactionRecovery.addUnnotarisedTransaction(transaction2) - transactionRecovery.addTransactionRecoveryMetadata(transaction2.id, TransactionMetadata(ALICE_NAME, DistributionList(ALL_VISIBLE, mapOf(BOB_NAME to ONLY_RELEVANT))), ALICE_NAME) + transactionRecovery.addSenderTransactionRecoveryMetadata(transaction2.id, TransactionMetadata(ALICE_NAME, DistributionList(ALL_VISIBLE, mapOf(BOB_NAME to ONLY_RELEVANT)))) val timeWindow = RecoveryTimeWindow(fromTime = now().minus(1, ChronoUnit.DAYS)) val results = transactionRecovery.querySenderDistributionRecords(timeWindow, excludingTxnIds = setOf(transaction1.id)) assertEquals(1, results.size) @@ -116,11 +116,12 @@ class DBTransactionStorageLedgerRecoveryTests { val transaction1 = newTransaction() // sender txn transactionRecovery.addUnnotarisedTransaction(transaction1) - transactionRecovery.addTransactionRecoveryMetadata(transaction1.id, TransactionMetadata(ALICE_NAME, DistributionList(ALL_VISIBLE, mapOf(BOB_NAME to ALL_VISIBLE))), ALICE_NAME) + transactionRecovery.addSenderTransactionRecoveryMetadata(transaction1.id, TransactionMetadata(ALICE_NAME, DistributionList(ALL_VISIBLE, mapOf(BOB_NAME to ALL_VISIBLE)))) val transaction2 = newTransaction() // receiver txn transactionRecovery.addUnnotarisedTransaction(transaction2) - transactionRecovery.addTransactionRecoveryMetadata(transaction2.id, TransactionMetadata(BOB_NAME, DistributionList(ONLY_RELEVANT, mapOf(ALICE_NAME to ALL_VISIBLE))), ALICE_NAME) + transactionRecovery.addReceiverTransactionRecoveryMetadata(transaction2.id, BOB_NAME, ALICE_NAME, ALL_VISIBLE, + DistributionList(ONLY_RELEVANT, mapOf(ALICE_NAME to ALL_VISIBLE)).toWire()) val timeWindow = RecoveryTimeWindow(fromTime = now().minus(1, ChronoUnit.DAYS)) transactionRecovery.queryDistributionRecords(timeWindow, recordType = DistributionRecordType.SENDER).let { assertEquals(1, it.size) @@ -140,19 +141,19 @@ class DBTransactionStorageLedgerRecoveryTests { fun `query for sender distribution records by peers`() { val txn1 = newTransaction() transactionRecovery.addUnnotarisedTransaction(txn1) - transactionRecovery.addTransactionRecoveryMetadata(txn1.id, TransactionMetadata(ALICE_NAME, DistributionList(ALL_VISIBLE, mapOf(BOB_NAME to ALL_VISIBLE))), ALICE_NAME) + transactionRecovery.addSenderTransactionRecoveryMetadata(txn1.id, TransactionMetadata(ALICE_NAME, DistributionList(ALL_VISIBLE, mapOf(BOB_NAME to ALL_VISIBLE)))) val txn2 = newTransaction() transactionRecovery.addUnnotarisedTransaction(txn2) - transactionRecovery.addTransactionRecoveryMetadata(txn2.id, TransactionMetadata(ALICE_NAME, DistributionList(ONLY_RELEVANT, mapOf(CHARLIE_NAME to ONLY_RELEVANT))), ALICE_NAME) + transactionRecovery.addSenderTransactionRecoveryMetadata(txn2.id, TransactionMetadata(ALICE_NAME, DistributionList(ONLY_RELEVANT, mapOf(CHARLIE_NAME to ONLY_RELEVANT)))) val txn3 = newTransaction() transactionRecovery.addUnnotarisedTransaction(txn3) - transactionRecovery.addTransactionRecoveryMetadata(txn3.id, TransactionMetadata(ALICE_NAME, DistributionList(ONLY_RELEVANT, mapOf(BOB_NAME to ONLY_RELEVANT, CHARLIE_NAME to ALL_VISIBLE))), ALICE_NAME) + transactionRecovery.addSenderTransactionRecoveryMetadata(txn3.id, TransactionMetadata(ALICE_NAME, DistributionList(ONLY_RELEVANT, mapOf(BOB_NAME to ONLY_RELEVANT, CHARLIE_NAME to ALL_VISIBLE)))) val txn4 = newTransaction() transactionRecovery.addUnnotarisedTransaction(txn4) - transactionRecovery.addTransactionRecoveryMetadata(txn4.id, TransactionMetadata(BOB_NAME, DistributionList(ONLY_RELEVANT, mapOf(ALICE_NAME to ONLY_RELEVANT))), BOB_NAME) + transactionRecovery.addSenderTransactionRecoveryMetadata(txn4.id, TransactionMetadata(BOB_NAME, DistributionList(ONLY_RELEVANT, mapOf(ALICE_NAME to ONLY_RELEVANT)))) val txn5 = newTransaction() transactionRecovery.addUnnotarisedTransaction(txn5) - transactionRecovery.addTransactionRecoveryMetadata(txn5.id, TransactionMetadata(CHARLIE_NAME, DistributionList(ONLY_RELEVANT, emptyMap())), CHARLIE_NAME) + transactionRecovery.addSenderTransactionRecoveryMetadata(txn5.id, TransactionMetadata(CHARLIE_NAME, DistributionList(ONLY_RELEVANT, emptyMap()))) assertEquals(5, readSenderDistributionRecordFromDB().size) val timeWindow = RecoveryTimeWindow(fromTime = now().minus(1, ChronoUnit.DAYS)) @@ -169,24 +170,24 @@ class DBTransactionStorageLedgerRecoveryTests { fun `query for receiver distribution records by initiator`() { val txn1 = newTransaction() transactionRecovery.addUnnotarisedTransaction(txn1) - transactionRecovery.addTransactionRecoveryMetadata(txn1.id, TransactionMetadata(ALICE_NAME, - DistributionList(ONLY_RELEVANT, mapOf(BOB_NAME to ALL_VISIBLE, CHARLIE_NAME to ALL_VISIBLE))), BOB_NAME) + transactionRecovery.addReceiverTransactionRecoveryMetadata(txn1.id, ALICE_NAME, BOB_NAME, ALL_VISIBLE, + DistributionList(ONLY_RELEVANT, mapOf(BOB_NAME to ALL_VISIBLE, CHARLIE_NAME to ALL_VISIBLE)).toWire()) val txn2 = newTransaction() transactionRecovery.addUnnotarisedTransaction(txn2) - transactionRecovery.addTransactionRecoveryMetadata(txn2.id, TransactionMetadata(ALICE_NAME, - DistributionList(ONLY_RELEVANT, mapOf(BOB_NAME to ONLY_RELEVANT))), BOB_NAME) + transactionRecovery.addReceiverTransactionRecoveryMetadata(txn2.id, ALICE_NAME, BOB_NAME, ONLY_RELEVANT, + DistributionList(ONLY_RELEVANT, mapOf(BOB_NAME to ONLY_RELEVANT)).toWire()) val txn3 = newTransaction() transactionRecovery.addUnnotarisedTransaction(txn3) - transactionRecovery.addTransactionRecoveryMetadata(txn3.id, TransactionMetadata(ALICE_NAME, - DistributionList(ONLY_RELEVANT, mapOf(CHARLIE_NAME to NONE))), CHARLIE_NAME) + transactionRecovery.addReceiverTransactionRecoveryMetadata(txn3.id, ALICE_NAME, CHARLIE_NAME, NONE, + DistributionList(ONLY_RELEVANT, mapOf(CHARLIE_NAME to NONE)).toWire()) val txn4 = newTransaction() transactionRecovery.addUnnotarisedTransaction(txn4) - transactionRecovery.addTransactionRecoveryMetadata(txn4.id, TransactionMetadata(BOB_NAME, - DistributionList(ONLY_RELEVANT, mapOf(ALICE_NAME to ALL_VISIBLE))), ALICE_NAME) + transactionRecovery.addReceiverTransactionRecoveryMetadata(txn4.id, BOB_NAME, ALICE_NAME, ONLY_RELEVANT, + DistributionList(ONLY_RELEVANT, mapOf(ALICE_NAME to ALL_VISIBLE)).toWire()) val txn5 = newTransaction() transactionRecovery.addUnnotarisedTransaction(txn5) - transactionRecovery.addTransactionRecoveryMetadata(txn5.id, TransactionMetadata(CHARLIE_NAME, - DistributionList(ONLY_RELEVANT, mapOf(BOB_NAME to ONLY_RELEVANT))), BOB_NAME) + transactionRecovery.addReceiverTransactionRecoveryMetadata(txn5.id, CHARLIE_NAME, BOB_NAME, ONLY_RELEVANT, + DistributionList(ONLY_RELEVANT, mapOf(BOB_NAME to ONLY_RELEVANT)).toWire()) val timeWindow = RecoveryTimeWindow(fromTime = now().minus(1, ChronoUnit.DAYS)) transactionRecovery.queryReceiverDistributionRecords(timeWindow, initiators = setOf(ALICE_NAME)).let { @@ -204,7 +205,7 @@ class DBTransactionStorageLedgerRecoveryTests { fun `transaction without peers does not store recovery metadata in database`() { val senderTransaction = newTransaction() transactionRecovery.addUnnotarisedTransaction(senderTransaction) - transactionRecovery.addTransactionRecoveryMetadata(senderTransaction.id, TransactionMetadata(ALICE_NAME, DistributionList(ONLY_RELEVANT, emptyMap())), ALICE_NAME) + transactionRecovery.addSenderTransactionRecoveryMetadata(senderTransaction.id, TransactionMetadata(ALICE_NAME, DistributionList(ONLY_RELEVANT, emptyMap()))) assertEquals(IN_FLIGHT, readTransactionFromDB(senderTransaction.id).status) assertEquals(0, readSenderDistributionRecordFromDB(senderTransaction.id).size) } @@ -213,8 +214,8 @@ class DBTransactionStorageLedgerRecoveryTests { fun `create un-notarised transaction with flow metadata and validate status in db`() { val senderTransaction = newTransaction() transactionRecovery.addUnnotarisedTransaction(senderTransaction) - transactionRecovery.addTransactionRecoveryMetadata(senderTransaction.id, - TransactionMetadata(ALICE_NAME, DistributionList(ALL_VISIBLE, mapOf(BOB_NAME to ALL_VISIBLE))), ALICE_NAME) + transactionRecovery.addSenderTransactionRecoveryMetadata(senderTransaction.id, + TransactionMetadata(ALICE_NAME, DistributionList(ALL_VISIBLE, mapOf(BOB_NAME to ALL_VISIBLE)))) assertEquals(IN_FLIGHT, readTransactionFromDB(senderTransaction.id).status) readSenderDistributionRecordFromDB(senderTransaction.id).let { assertEquals(1, it.size) @@ -224,8 +225,8 @@ class DBTransactionStorageLedgerRecoveryTests { val receiverTransaction = newTransaction() transactionRecovery.addUnnotarisedTransaction(receiverTransaction) - transactionRecovery.addTransactionRecoveryMetadata(receiverTransaction.id, - TransactionMetadata(ALICE_NAME, DistributionList(ONLY_RELEVANT, mapOf(BOB_NAME to ALL_VISIBLE))), BOB_NAME) + transactionRecovery.addReceiverTransactionRecoveryMetadata(receiverTransaction.id, ALICE_NAME, BOB_NAME, ALL_VISIBLE, + DistributionList(ONLY_RELEVANT, mapOf(BOB_NAME to ALL_VISIBLE)).toWire()) assertEquals(IN_FLIGHT, readTransactionFromDB(receiverTransaction.id).status) readReceiverDistributionRecordFromDB(receiverTransaction.id).let { assertEquals(ALL_VISIBLE, it.statesToRecord) @@ -239,8 +240,8 @@ class DBTransactionStorageLedgerRecoveryTests { fun `finalize transaction with recovery metadata`() { val transaction = newTransaction(notarySig = false) transactionRecovery.finalizeTransaction(transaction) - transactionRecovery.addTransactionRecoveryMetadata(transaction.id, - TransactionMetadata(ALICE_NAME, DistributionList(ONLY_RELEVANT, mapOf(CHARLIE_NAME to ALL_VISIBLE))), ALICE_NAME) + transactionRecovery.addSenderTransactionRecoveryMetadata(transaction.id, + TransactionMetadata(ALICE_NAME, DistributionList(ONLY_RELEVANT, mapOf(CHARLIE_NAME to ALL_VISIBLE)))) assertEquals(VERIFIED, readTransactionFromDB(transaction.id).status) readSenderDistributionRecordFromDB(transaction.id).apply { assertEquals(1, this.size) @@ -252,8 +253,8 @@ class DBTransactionStorageLedgerRecoveryTests { fun `remove un-notarised transaction and associated recovery metadata`() { val senderTransaction = newTransaction(notarySig = false) transactionRecovery.addUnnotarisedTransaction(senderTransaction) - transactionRecovery.addTransactionRecoveryMetadata(senderTransaction.id, TransactionMetadata(ALICE.name, - DistributionList(ONLY_RELEVANT, mapOf(BOB.name to ONLY_RELEVANT, CHARLIE_NAME to ONLY_RELEVANT))), BOB.name) + transactionRecovery.addReceiverTransactionRecoveryMetadata(senderTransaction.id, ALICE.name, BOB.name, ONLY_RELEVANT, + DistributionList(ONLY_RELEVANT, mapOf(BOB.name to ONLY_RELEVANT, CHARLIE_NAME to ONLY_RELEVANT)).toWire()) assertNull(transactionRecovery.getTransaction(senderTransaction.id)) assertEquals(IN_FLIGHT, readTransactionFromDB(senderTransaction.id).status) @@ -264,8 +265,8 @@ class DBTransactionStorageLedgerRecoveryTests { val receiverTransaction = newTransaction(notarySig = false) transactionRecovery.addUnnotarisedTransaction(receiverTransaction) - transactionRecovery.addTransactionRecoveryMetadata(receiverTransaction.id, TransactionMetadata(ALICE.name, - DistributionList(ONLY_RELEVANT, mapOf(BOB.name to ONLY_RELEVANT))), ALICE.name) + transactionRecovery.addReceiverTransactionRecoveryMetadata(receiverTransaction.id, ALICE.name, BOB.name, ONLY_RELEVANT, + DistributionList(ONLY_RELEVANT, mapOf(BOB.name to ONLY_RELEVANT)).toWire()) assertNull(transactionRecovery.getTransaction(receiverTransaction.id)) assertEquals(IN_FLIGHT, readTransactionFromDB(receiverTransaction.id).status) @@ -275,6 +276,13 @@ class DBTransactionStorageLedgerRecoveryTests { assertNull(transactionRecovery.getTransactionInternal(receiverTransaction.id)) } + @Test(timeout = 300_000) + fun `test lightweight serialization and deserialization of hashed distribution list payload`() { + val dl = HashedDistributionList(ALL_VISIBLE, + mapOf(BOB.name.hashCode().toLong() to NONE, CHARLIE_NAME.hashCode().toLong() to ONLY_RELEVANT)) + assertEquals(dl, dl.serialize().let { HashedDistributionList.deserialize(it) }) + } + private fun readTransactionFromDB(id: SecureHash): DBTransactionStorage.DBTransaction { val fromDb = database.transaction { session.createQuery( @@ -361,4 +369,11 @@ class DBTransactionStorageLedgerRecoveryTests { private fun notarySig(txId: SecureHash) = DUMMY_NOTARY.keyPair.sign(SignableData(txId, SignatureMetadata(1, Crypto.findSignatureScheme(DUMMY_NOTARY.publicKey).schemeNumberID))) + + private fun DistributionList.toWire(cryptoService: CryptoService = MockCryptoService(emptyMap())): ByteArray { + val hashedPeersToStatesToRecord = this.peersToStatesToRecord.map { (peer, statesToRecord) -> + partyInfoCache.getPartyIdByCordaX500Name(peer) to statesToRecord }.toMap() + val hashedDistributionList = HashedDistributionList(this.senderStatesToRecord, hashedPeersToStatesToRecord) + return cryptoService.encrypt(hashedDistributionList.serialize()) + } } diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/MockTransactionStorage.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/MockTransactionStorage.kt index d9f26e9469..f850aab58b 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/MockTransactionStorage.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/MockTransactionStorage.kt @@ -12,6 +12,7 @@ import net.corda.node.services.api.WritableTransactionStorage import net.corda.core.flows.TransactionMetadata import net.corda.core.flows.TransactionStatus import net.corda.core.identity.CordaX500Name +import net.corda.core.node.StatesToRecord import net.corda.testing.node.MockServices import rx.Observable import rx.subjects.PublishSubject @@ -60,7 +61,9 @@ open class MockTransactionStorage : WritableTransactionStorage, SingletonSeriali return txns.putIfAbsent(transaction.id, TxHolder(transaction, status = TransactionStatus.IN_FLIGHT)) == null } - override fun addTransactionRecoveryMetadata(id: SecureHash, metadata: TransactionMetadata, caller: CordaX500Name) { } + override fun addSenderTransactionRecoveryMetadata(id: SecureHash, metadata: TransactionMetadata): ByteArray? { return null } + + override fun addReceiverTransactionRecoveryMetadata(id: SecureHash, sender: CordaX500Name, receiver: CordaX500Name, receiverStatesToRecord: StatesToRecord, encryptedDistributionList: ByteArray) { } override fun removeUnnotarisedTransaction(id: SecureHash): Boolean { return txns.remove(id) != null diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TestDSL.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TestDSL.kt index f7858056e5..fd55d3645d 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TestDSL.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TestDSL.kt @@ -148,7 +148,9 @@ data class TestTransactionDSLInterpreter private constructor( override fun finalizeTransaction(txn: SignedTransaction, statesToRecord: StatesToRecord) {} - override fun recordTransactionRecoveryMetadata(txnId: SecureHash, txnMetadata: TransactionMetadata, caller: CordaX500Name) {} + override fun recordSenderTransactionRecoveryMetadata(txnId: SecureHash, txnMetadata: TransactionMetadata): ByteArray? { return null } + + override fun recordReceiverTransactionRecoveryMetadata(txnId: SecureHash, sender: CordaX500Name, receiver: CordaX500Name, receiverStatesToRecord: StatesToRecord, encryptedDistributionList: ByteArray) {} } private fun copy(): TestTransactionDSLInterpreter = From ff0693a5984e83763baa68a498e80c4317bd1435 Mon Sep 17 00:00:00 2001 From: Shams Asari Date: Tue, 6 Jun 2023 11:25:14 +0100 Subject: [PATCH 030/126] ENT-9793: Use streams when loading vault query pages --- .../node/services/vault/NodeVaultService.kt | 61 +++++++++++-------- 1 file changed, 34 insertions(+), 27 deletions(-) diff --git a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt index 5109bb0d3e..ec4984ea68 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt @@ -73,6 +73,7 @@ import java.util.Arrays import java.util.UUID import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.CopyOnWriteArraySet +import java.util.stream.Stream import javax.persistence.PersistenceException import javax.persistence.Tuple import javax.persistence.criteria.CriteriaBuilder @@ -690,7 +691,10 @@ class NodeVaultService( "Page specification: invalid page number ${validPaging.pageNumber} [page numbers start from $DEFAULT_PAGE_NUM]" } } - return queryBy(criteria, validPaging, sorting, contractStateType) + log.debug { "Vault Query for contract type: $contractStateType, criteria: $criteria, pagination: $validPaging, sorting: $sorting" } + return database.transaction { + queryBy(criteria, validPaging, sorting, contractStateType) + } } catch (e: VaultQueryException) { throw e } catch (e: Exception) { @@ -702,36 +706,25 @@ class NodeVaultService( paging: PageSpecification, sorting: Sort, contractStateType: Class): Vault.Page { - log.debug { "Vault Query for contract type: $contractStateType, criteria: $criteria, pagination: $paging, sorting: $sorting" } - return database.transaction { - // calculate total results where a page specification has been defined - val totalStatesAvailable = if (paging.isDefault) -1 else queryTotalStateCount(criteria, contractStateType) + // calculate total results where a page specification has been defined + val totalStatesAvailable = if (paging.isDefault) -1 else queryTotalStateCount(criteria, contractStateType) - val (query, stateTypes) = createQuery(criteria, contractStateType, sorting) - query.setResultWindow(paging) + val (query, stateTypes) = createQuery(criteria, contractStateType, sorting) + query.setResultWindow(paging) - // execution - val results = query.resultList - - // final pagination check (fail-fast on too many results when no pagination specified) - checkVaultQuery(!paging.isDefault || results.size != paging.pageSize + 1) { - "There are more results than the limit of $DEFAULT_PAGE_SIZE for queries that do not specify paging. " + - "In order to retrieve these results, provide a PageSpecification to the method invoked." - } + var previousPageAnchor: StateRef? = null + val statesMetadata: MutableList = mutableListOf() + val otherResults: MutableList = mutableListOf() + query.resultStream(paging).use { results -> val resultsIterator = results.iterator() // From page 2 and onwards, the first result is the previous page anchor - val previousPageAnchor = if (paging.pageNumber > DEFAULT_PAGE_NUM && resultsIterator.hasNext()) { + if (paging.pageNumber > DEFAULT_PAGE_NUM && resultsIterator.hasNext()) { val previousVaultState = resultsIterator.next()[0] as VaultSchemaV1.VaultStates - previousVaultState.stateRef!!.toStateRef() - } else { - null + previousPageAnchor = previousVaultState.stateRef!!.toStateRef() } - val statesMetadata: MutableList = mutableListOf() - val otherResults: MutableList = mutableListOf() - for (result in resultsIterator) { val result0 = result[0] if (result0 is VaultSchemaV1.VaultStates) { @@ -741,13 +734,27 @@ class NodeVaultService( otherResults.addAll(result.toArray().asList()) } } + } - val states: List> = servicesForResolution.loadStates( - statesMetadata.mapTo(LinkedHashSet()) { it.ref }, - ArrayList() - ) + val states: List> = servicesForResolution.loadStates( + statesMetadata.mapTo(LinkedHashSet()) { it.ref }, + ArrayList() + ) - Vault.Page(states, statesMetadata, totalStatesAvailable, stateTypes, otherResults, previousPageAnchor) + return Vault.Page(states, statesMetadata, totalStatesAvailable, stateTypes, otherResults, previousPageAnchor) + } + + private fun Query.resultStream(paging: PageSpecification): Stream { + return if (paging.isDefault) { + val allResults = resultList + // final pagination check (fail-fast on too many results when no pagination specified) + checkVaultQuery(allResults.size != paging.pageSize + 1) { + "There are more results than the limit of $DEFAULT_PAGE_SIZE for queries that do not specify paging. " + + "In order to retrieve these results, provide a PageSpecification to the method invoked." + } + allResults.stream() + } else { + stream() } } From c56ee1cc73852801a44280deed43d99dab3cbafb Mon Sep 17 00:00:00 2001 From: Jose Coll Date: Wed, 7 Jun 2023 09:30:24 +0100 Subject: [PATCH 031/126] Fix failing slow test. (#7387) --- .../net/corda/node/flows/FinalityFlowErrorHandlingTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node/src/integration-test-slow/kotlin/net/corda/node/flows/FinalityFlowErrorHandlingTest.kt b/node/src/integration-test-slow/kotlin/net/corda/node/flows/FinalityFlowErrorHandlingTest.kt index cbc4b43c94..7d800de41a 100644 --- a/node/src/integration-test-slow/kotlin/net/corda/node/flows/FinalityFlowErrorHandlingTest.kt +++ b/node/src/integration-test-slow/kotlin/net/corda/node/flows/FinalityFlowErrorHandlingTest.kt @@ -99,7 +99,7 @@ class GetFlowTransaction(private val txId: SecureHash) : FlowLogic ps.executeQuery().use { rs -> rs.next() - rs.getLong(2) // receiverPartyId + rs.getLong(4) // receiverPartyId } } return Pair(transactionStatus, receiverPartyId) From 41e9298b19c1cea69bcfed9144c7f6e3e3d58ede Mon Sep 17 00:00:00 2001 From: Connel McGovern <100574906+mcgovc@users.noreply.github.com> Date: Wed, 7 Jun 2023 13:49:40 +0100 Subject: [PATCH 032/126] ES-562: Updating modules to scan on Snyk nightly (#7392) --- .ci/dev/nightly-regression/JenkinsfileSnykScan | 2 +- .ci/dev/regression/Jenkinsfile | 2 +- .github/workflows/check-pr-title.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.ci/dev/nightly-regression/JenkinsfileSnykScan b/.ci/dev/nightly-regression/JenkinsfileSnykScan index 564bb516a9..6c0f81d698 100644 --- a/.ci/dev/nightly-regression/JenkinsfileSnykScan +++ b/.ci/dev/nightly-regression/JenkinsfileSnykScan @@ -3,5 +3,5 @@ cordaSnykScanPipeline ( snykTokenId: 'c4-os-snyk-api-token-secret', // specify the Gradle submodules to scan and monitor on snyk Server - modulesToScan: ['node', 'capsule', 'bridge', 'bridgecapsule'] + modulesToScan: ['node', 'capsule'] ) diff --git a/.ci/dev/regression/Jenkinsfile b/.ci/dev/regression/Jenkinsfile index 02dc1a403d..4bab8e416c 100644 --- a/.ci/dev/regression/Jenkinsfile +++ b/.ci/dev/regression/Jenkinsfile @@ -92,7 +92,7 @@ pipeline { steps { script { // Invoke Snyk for each Gradle sub project we wish to scan - def modulesToScan = ['node', 'capsule', 'bridge', 'bridgecapsule'] + def modulesToScan = ['node', 'capsule'] modulesToScan.each { module -> snykSecurityScan("${env.SNYK_API_KEY}", "--sub-project=$module --configuration-matching='^runtimeClasspath\$' --prune-repeated-subdependencies --debug --target-reference='${env.BRANCH_NAME}' --project-tags=Branch='${env.BRANCH_NAME.replaceAll("[^0-9|a-z|A-Z]+","_")}'") } diff --git a/.github/workflows/check-pr-title.yml b/.github/workflows/check-pr-title.yml index 99f8265078..331872fdb1 100644 --- a/.github/workflows/check-pr-title.yml +++ b/.github/workflows/check-pr-title.yml @@ -9,6 +9,6 @@ jobs: steps: - uses: morrisoncole/pr-lint-action@v1.6.1 with: - title-regex: '^((CORDA|AG|EG|ENT|INFRA|NAAS|ES)-\d+)(.*)' + title-regex: '^((CORDA|AG|EG|ENT|INFRA|ES)-\d+)(.*)' on-failed-regex-comment: "PR title failed to match regex -> `%regex%`" repo-token: "${{ secrets.GITHUB_TOKEN }}" From 0e877958fef4af6d880c17f37f979c3acd41f80d Mon Sep 17 00:00:00 2001 From: Jose Coll Date: Fri, 9 Jun 2023 17:19:43 +0100 Subject: [PATCH 033/126] ENT-10009 Enhance SendTransactionFlow to allow sending a txn to multiple sessions. (#7393) --- .ci/api-current.txt | 190 ++++++++++++++- .../coretests/flows/FinalityFlowTests.kt | 5 +- .../flows/TestNoSecurityDataVendingFlow.kt | 2 +- .../net/corda/core/flows/FinalityFlow.kt | 76 +++--- .../net/corda/core/flows/FlowTransaction.kt | 5 +- .../corda/core/flows/SendTransactionFlow.kt | 230 ++++++++++-------- ...inisticContractWithCustomSerializerTest.kt | 2 +- .../net/corda/node/internal/AbstractNode.kt | 14 +- .../DBTransactionStorageLedgerRecovery.kt | 15 +- 9 files changed, 362 insertions(+), 177 deletions(-) diff --git a/.ci/api-current.txt b/.ci/api-current.txt index e00001eee5..a9499a2158 100644 --- a/.ci/api-current.txt +++ b/.ci/api-current.txt @@ -1798,6 +1798,8 @@ public final class net.corda.core.crypto.Crypto extends java.lang.Object public static final boolean doVerify(net.corda.core.crypto.SecureHash, net.corda.core.crypto.TransactionSignature) public static final boolean doVerify(net.corda.core.crypto.SignatureScheme, java.security.PublicKey, byte[], byte[]) @NotNull + public static final byte[] encodePublicKey(java.security.PublicKey) + @NotNull public static final java.security.Provider findProvider(String) @NotNull public static final net.corda.core.crypto.SignatureScheme findSignatureScheme(int) @@ -2518,12 +2520,16 @@ public static final class net.corda.core.flows.ContractUpgradeFlow$Initiate exte protected net.corda.core.flows.AbstractStateReplacementFlow$UpgradeTx assembleTx() ## public class net.corda.core.flows.DataVendingFlow extends net.corda.core.flows.FlowLogic + public (java.util.Set, Object, net.corda.core.flows.TransactionMetadata) + public (java.util.Set, Object, net.corda.core.flows.TransactionMetadata, int, kotlin.jvm.internal.DefaultConstructorMarker) public (net.corda.core.flows.FlowSession, Object) + public (net.corda.core.flows.FlowSession, Object, net.corda.core.flows.TransactionMetadata) + public (net.corda.core.flows.FlowSession, Object, net.corda.core.flows.TransactionMetadata, int, kotlin.jvm.internal.DefaultConstructorMarker) @Suspendable @Nullable public Void call() @NotNull - public final net.corda.core.flows.FlowSession getOtherSideSession() + public final java.util.Set getOtherSessions() @NotNull public final Object getPayload() @Suspendable @@ -2535,10 +2541,29 @@ public class net.corda.core.flows.DataVendingFlow extends net.corda.core.flows.F @DoNotImplement public interface net.corda.core.flows.Destination ## +@CordaSerializable +public final class net.corda.core.flows.DistributionList extends java.lang.Object + public (net.corda.core.node.StatesToRecord, java.util.Map) + @NotNull + public final net.corda.core.node.StatesToRecord component1() + @NotNull + public final java.util.Map component2() + @NotNull + public final net.corda.core.flows.DistributionList copy(net.corda.core.node.StatesToRecord, java.util.Map) + public boolean equals(Object) + @NotNull + public final java.util.Map getPeersToStatesToRecord() + @NotNull + public final net.corda.core.node.StatesToRecord getSenderStatesToRecord() + public int hashCode() + @NotNull + public String toString() +## @InitiatingFlow public final class net.corda.core.flows.FinalityFlow extends net.corda.core.flows.FlowLogic public (net.corda.core.transactions.SignedTransaction) public (net.corda.core.transactions.SignedTransaction, java.util.Collection) + public (net.corda.core.transactions.SignedTransaction, java.util.Collection, java.util.Collection) public (net.corda.core.transactions.SignedTransaction, java.util.Collection, java.util.Collection, net.corda.core.utilities.ProgressTracker) public (net.corda.core.transactions.SignedTransaction, java.util.Collection, net.corda.core.node.StatesToRecord) public (net.corda.core.transactions.SignedTransaction, java.util.Collection, net.corda.core.node.StatesToRecord, net.corda.core.utilities.ProgressTracker) @@ -2570,12 +2595,32 @@ public static final class net.corda.core.flows.FinalityFlow$Companion$BROADCASTI public static final net.corda.core.flows.FinalityFlow$Companion$BROADCASTING INSTANCE ## @CordaSerializable +public static final class net.corda.core.flows.FinalityFlow$Companion$BROADCASTING_NOTARY_ERROR extends net.corda.core.utilities.ProgressTracker$Step + public static final net.corda.core.flows.FinalityFlow$Companion$BROADCASTING_NOTARY_ERROR INSTANCE +## +@CordaSerializable +public static final class net.corda.core.flows.FinalityFlow$Companion$BROADCASTING_POST_NOTARISATION extends net.corda.core.utilities.ProgressTracker$Step + public static final net.corda.core.flows.FinalityFlow$Companion$BROADCASTING_POST_NOTARISATION INSTANCE +## +@CordaSerializable +public static final class net.corda.core.flows.FinalityFlow$Companion$BROADCASTING_PRE_NOTARISATION extends net.corda.core.utilities.ProgressTracker$Step + public static final net.corda.core.flows.FinalityFlow$Companion$BROADCASTING_PRE_NOTARISATION INSTANCE +## +@CordaSerializable +public static final class net.corda.core.flows.FinalityFlow$Companion$FINALISING_TRANSACTION extends net.corda.core.utilities.ProgressTracker$Step + public static final net.corda.core.flows.FinalityFlow$Companion$FINALISING_TRANSACTION INSTANCE +## +@CordaSerializable public static final class net.corda.core.flows.FinalityFlow$Companion$NOTARISING extends net.corda.core.utilities.ProgressTracker$Step @NotNull public net.corda.core.utilities.ProgressTracker childProgressTracker() public static final net.corda.core.flows.FinalityFlow$Companion$NOTARISING INSTANCE ## @CordaSerializable +public static final class net.corda.core.flows.FinalityFlow$Companion$RECORD_UNNOTARISED extends net.corda.core.utilities.ProgressTracker$Step + public static final net.corda.core.flows.FinalityFlow$Companion$RECORD_UNNOTARISED INSTANCE +## +@CordaSerializable public class net.corda.core.flows.FlowException extends net.corda.core.CordaException implements net.corda.core.flows.IdentifiableException public () public (String) @@ -2877,6 +2922,37 @@ public static final class net.corda.core.flows.FlowStackSnapshot$Frame extends j public String toString() ## @CordaSerializable +public final class net.corda.core.flows.FlowTransactionInfo extends java.lang.Object + public (net.corda.core.flows.StateMachineRunId, String, net.corda.core.flows.TransactionStatus, java.time.Instant, net.corda.core.flows.TransactionMetadata) + @NotNull + public final net.corda.core.flows.StateMachineRunId component1() + @NotNull + public final String component2() + @NotNull + public final net.corda.core.flows.TransactionStatus component3() + @NotNull + public final java.time.Instant component4() + @Nullable + public final net.corda.core.flows.TransactionMetadata component5() + @NotNull + public final net.corda.core.flows.FlowTransactionInfo copy(net.corda.core.flows.StateMachineRunId, String, net.corda.core.flows.TransactionStatus, java.time.Instant, net.corda.core.flows.TransactionMetadata) + public boolean equals(Object) + @Nullable + public final net.corda.core.flows.TransactionMetadata getMetadata() + @NotNull + public final net.corda.core.flows.StateMachineRunId getStateMachineRunId() + @NotNull + public final net.corda.core.flows.TransactionStatus getStatus() + @NotNull + public final java.time.Instant getTimestamp() + @NotNull + public final String getTxId() + public int hashCode() + public final boolean isInitiator(net.corda.core.identity.CordaX500Name) + @NotNull + public String toString() +## +@CordaSerializable public class net.corda.core.flows.HospitalizeFlowException extends net.corda.core.CordaRuntimeException public () public (String) @@ -3138,10 +3214,16 @@ public static final class net.corda.core.flows.NotaryFlow$Client$Companion$REQUE public static final class net.corda.core.flows.NotaryFlow$Client$Companion$VALIDATING extends net.corda.core.utilities.ProgressTracker$Step public static final net.corda.core.flows.NotaryFlow$Client$Companion$VALIDATING INSTANCE ## +public final class net.corda.core.flows.NotarySigCheck extends java.lang.Object + public final boolean needsNotarySignature(net.corda.core.transactions.SignedTransaction) + public static final net.corda.core.flows.NotarySigCheck INSTANCE +## public final class net.corda.core.flows.ReceiveFinalityFlow extends net.corda.core.flows.FlowLogic public (net.corda.core.flows.FlowSession) public (net.corda.core.flows.FlowSession, net.corda.core.crypto.SecureHash) public (net.corda.core.flows.FlowSession, net.corda.core.crypto.SecureHash, net.corda.core.node.StatesToRecord) + public (net.corda.core.flows.FlowSession, net.corda.core.crypto.SecureHash, net.corda.core.node.StatesToRecord, Boolean) + public (net.corda.core.flows.FlowSession, net.corda.core.crypto.SecureHash, net.corda.core.node.StatesToRecord, Boolean, int, kotlin.jvm.internal.DefaultConstructorMarker) @Suspendable @NotNull public net.corda.core.transactions.SignedTransaction call() @@ -3157,6 +3239,8 @@ public class net.corda.core.flows.ReceiveTransactionFlow extends net.corda.core. public (net.corda.core.flows.FlowSession, boolean) public (net.corda.core.flows.FlowSession, boolean, net.corda.core.node.StatesToRecord) public (net.corda.core.flows.FlowSession, boolean, net.corda.core.node.StatesToRecord, int, kotlin.jvm.internal.DefaultConstructorMarker) + public (net.corda.core.flows.FlowSession, boolean, net.corda.core.node.StatesToRecord, boolean) + public (net.corda.core.flows.FlowSession, boolean, net.corda.core.node.StatesToRecord, boolean, int, kotlin.jvm.internal.DefaultConstructorMarker) @Suspendable @NotNull public net.corda.core.transactions.SignedTransaction call() @@ -3164,6 +3248,41 @@ public class net.corda.core.flows.ReceiveTransactionFlow extends net.corda.core. protected void checkBeforeRecording(net.corda.core.transactions.SignedTransaction) ## @CordaSerializable +public final class net.corda.core.flows.RecoveryTimeWindow extends java.lang.Object + public (java.time.Instant, java.time.Instant) + public (java.time.Instant, java.time.Instant, int, kotlin.jvm.internal.DefaultConstructorMarker) + @NotNull + public static final net.corda.core.flows.RecoveryTimeWindow between(java.time.Instant, java.time.Instant) + @NotNull + public final java.time.Instant component1() + @NotNull + public final java.time.Instant component2() + @NotNull + public final net.corda.core.flows.RecoveryTimeWindow copy(java.time.Instant, java.time.Instant) + public boolean equals(Object) + @NotNull + public static final net.corda.core.flows.RecoveryTimeWindow fromOnly(java.time.Instant) + @NotNull + public final java.time.Instant getFromTime() + @NotNull + public final java.time.Instant getUntilTime() + public int hashCode() + @NotNull + public String toString() + @NotNull + public static final net.corda.core.flows.RecoveryTimeWindow untilOnly(java.time.Instant) + public static final net.corda.core.flows.RecoveryTimeWindow$Companion Companion +## +public static final class net.corda.core.flows.RecoveryTimeWindow$Companion extends java.lang.Object + public (kotlin.jvm.internal.DefaultConstructorMarker) + @NotNull + public final net.corda.core.flows.RecoveryTimeWindow between(java.time.Instant, java.time.Instant) + @NotNull + public final net.corda.core.flows.RecoveryTimeWindow fromOnly(java.time.Instant) + @NotNull + public final net.corda.core.flows.RecoveryTimeWindow untilOnly(java.time.Instant) +## +@CordaSerializable public final class net.corda.core.flows.ResultSerializationException extends net.corda.core.CordaRuntimeException public (net.corda.core.serialization.internal.MissingSerializerException) ## @@ -3174,6 +3293,12 @@ public class net.corda.core.flows.SendStateAndRefFlow extends net.corda.core.flo ## public class net.corda.core.flows.SendTransactionFlow extends net.corda.core.flows.DataVendingFlow public (net.corda.core.flows.FlowSession, net.corda.core.transactions.SignedTransaction) + public static final net.corda.core.flows.SendTransactionFlow$Companion Companion +## +public static final class net.corda.core.flows.SendTransactionFlow$Companion extends java.lang.Object + public (kotlin.jvm.internal.DefaultConstructorMarker) + @NotNull + public final net.corda.core.identity.CordaX500Name getDUMMY_PARTICIPANT_NAME() ## public abstract class net.corda.core.flows.SignTransactionFlow extends net.corda.core.flows.FlowLogic public (net.corda.core.flows.FlowSession) @@ -3209,6 +3334,24 @@ public static final class net.corda.core.flows.SignTransactionFlow$Companion$SIG public static final class net.corda.core.flows.SignTransactionFlow$Companion$VERIFYING extends net.corda.core.utilities.ProgressTracker$Step public static final net.corda.core.flows.SignTransactionFlow$Companion$VERIFYING INSTANCE ## +@CordaSerializable +public final class net.corda.core.flows.SignedTransactionWithDistributionList extends java.lang.Object + public (net.corda.core.transactions.SignedTransaction, byte[]) + @NotNull + public final net.corda.core.transactions.SignedTransaction component1() + @NotNull + public final byte[] component2() + @NotNull + public final net.corda.core.flows.SignedTransactionWithDistributionList copy(net.corda.core.transactions.SignedTransaction, byte[]) + public boolean equals(Object) + @NotNull + public final byte[] getDistributionList() + @NotNull + public final net.corda.core.transactions.SignedTransaction getStx() + public int hashCode() + @NotNull + public String toString() +## public final class net.corda.core.flows.StackFrameDataToken extends java.lang.Object public (String) @NotNull @@ -3281,6 +3424,29 @@ public class net.corda.core.flows.StateReplacementException extends net.corda.co public (String, Throwable, int, kotlin.jvm.internal.DefaultConstructorMarker) ## @CordaSerializable +public final class net.corda.core.flows.TransactionMetadata extends java.lang.Object + public (net.corda.core.identity.CordaX500Name, net.corda.core.flows.DistributionList) + @NotNull + public final net.corda.core.identity.CordaX500Name component1() + @NotNull + public final net.corda.core.flows.DistributionList component2() + @NotNull + public final net.corda.core.flows.TransactionMetadata copy(net.corda.core.identity.CordaX500Name, net.corda.core.flows.DistributionList) + public boolean equals(Object) + @NotNull + public final net.corda.core.flows.DistributionList getDistributionList() + @NotNull + public final net.corda.core.identity.CordaX500Name getInitiator() + public int hashCode() + @NotNull + public String toString() +## +@CordaSerializable +public final class net.corda.core.flows.TransactionStatus extends java.lang.Enum + public static net.corda.core.flows.TransactionStatus valueOf(String) + public static net.corda.core.flows.TransactionStatus[] values() +## +@CordaSerializable public final class net.corda.core.flows.UnexpectedFlowEndException extends net.corda.core.CordaRuntimeException implements net.corda.core.flows.IdentifiableException public (String) public (String, Throwable) @@ -4140,6 +4306,7 @@ public interface net.corda.core.node.ServicesForResolution @NotNull public net.corda.core.transactions.LedgerTransaction specialise(net.corda.core.transactions.LedgerTransaction) ## +@CordaSerializable public final class net.corda.core.node.StatesToRecord extends java.lang.Enum public static net.corda.core.node.StatesToRecord valueOf(String) public static net.corda.core.node.StatesToRecord[] values() @@ -4473,6 +4640,8 @@ public static final class net.corda.core.node.services.Vault$ConstraintInfo$Type @CordaSerializable public static final class net.corda.core.node.services.Vault$Page extends java.lang.Object public (java.util.List>, java.util.List, long, net.corda.core.node.services.Vault$StateStatus, java.util.List) + public (java.util.List>, java.util.List, long, net.corda.core.node.services.Vault$StateStatus, java.util.List, net.corda.core.contracts.StateRef) + public (java.util.List, java.util.List, long, net.corda.core.node.services.Vault$StateStatus, java.util.List, net.corda.core.contracts.StateRef, int, kotlin.jvm.internal.DefaultConstructorMarker) @NotNull public final java.util.List> component1() @NotNull @@ -4482,11 +4651,17 @@ public static final class net.corda.core.node.services.Vault$Page extends java.l public final net.corda.core.node.services.Vault$StateStatus component4() @NotNull public final java.util.List component5() + @Nullable + public final net.corda.core.contracts.StateRef component6() @NotNull public final net.corda.core.node.services.Vault$Page copy(java.util.List>, java.util.List, long, net.corda.core.node.services.Vault$StateStatus, java.util.List) + @NotNull + public final net.corda.core.node.services.Vault$Page copy(java.util.List>, java.util.List, long, net.corda.core.node.services.Vault$StateStatus, java.util.List, net.corda.core.contracts.StateRef) public boolean equals(Object) @NotNull public final java.util.List getOtherResults() + @Nullable + public final net.corda.core.contracts.StateRef getPreviousPageAnchor() @NotNull public final net.corda.core.node.services.Vault$StateStatus getStateTypes() @NotNull @@ -8383,6 +8558,8 @@ public final class net.corda.testing.driver.DriverParameters extends java.lang.O public (boolean, java.nio.file.Path, net.corda.testing.driver.PortAllocation, net.corda.testing.driver.PortAllocation, java.util.Map, boolean, boolean, boolean, java.util.List, java.util.List, net.corda.testing.driver.JmxPolicy, net.corda.core.node.NetworkParameters, java.util.Map, boolean, java.util.Collection, java.nio.file.Path, java.util.List, java.util.Map, boolean, int, kotlin.jvm.internal.DefaultConstructorMarker) public (boolean, java.nio.file.Path, net.corda.testing.driver.PortAllocation, net.corda.testing.driver.PortAllocation, java.util.Map, boolean, boolean, boolean, java.util.List, java.util.List, net.corda.testing.driver.JmxPolicy, net.corda.core.node.NetworkParameters, java.util.Map, boolean, java.util.Collection, java.nio.file.Path, java.util.List, java.util.Map, boolean, boolean) public (boolean, java.nio.file.Path, net.corda.testing.driver.PortAllocation, net.corda.testing.driver.PortAllocation, java.util.Map, boolean, boolean, boolean, java.util.List, java.util.List, net.corda.testing.driver.JmxPolicy, net.corda.core.node.NetworkParameters, java.util.Map, boolean, java.util.Collection, java.nio.file.Path, java.util.List, java.util.Map, boolean, boolean, int, kotlin.jvm.internal.DefaultConstructorMarker) + public (boolean, java.nio.file.Path, net.corda.testing.driver.PortAllocation, net.corda.testing.driver.PortAllocation, java.util.Map, boolean, boolean, boolean, java.util.List, java.util.List, net.corda.testing.driver.JmxPolicy, net.corda.core.node.NetworkParameters, java.util.Map, boolean, java.util.Collection, java.nio.file.Path, java.util.List, java.util.Map, boolean, boolean, java.time.Duration) + public (boolean, java.nio.file.Path, net.corda.testing.driver.PortAllocation, net.corda.testing.driver.PortAllocation, java.util.Map, boolean, boolean, boolean, java.util.List, java.util.List, net.corda.testing.driver.JmxPolicy, net.corda.core.node.NetworkParameters, java.util.Map, boolean, java.util.Collection, java.nio.file.Path, java.util.List, java.util.Map, boolean, boolean, java.time.Duration, int, kotlin.jvm.internal.DefaultConstructorMarker) public (boolean, java.nio.file.Path, net.corda.testing.driver.PortAllocation, net.corda.testing.driver.PortAllocation, java.util.Map, boolean, boolean, boolean, java.util.List, java.util.List, net.corda.testing.driver.JmxPolicy, net.corda.core.node.NetworkParameters, boolean) public final boolean component1() @NotNull @@ -8407,6 +8584,8 @@ public final class net.corda.testing.driver.DriverParameters extends java.lang.O public final java.nio.file.Path component2() public final boolean component20() @NotNull + public final java.time.Duration component21() + @NotNull public final net.corda.testing.driver.PortAllocation component3() @NotNull public final net.corda.testing.driver.PortAllocation component4() @@ -8426,6 +8605,8 @@ public final class net.corda.testing.driver.DriverParameters extends java.lang.O @NotNull public final net.corda.testing.driver.DriverParameters copy(boolean, java.nio.file.Path, net.corda.testing.driver.PortAllocation, net.corda.testing.driver.PortAllocation, java.util.Map, boolean, boolean, boolean, java.util.List, java.util.List, net.corda.testing.driver.JmxPolicy, net.corda.core.node.NetworkParameters, java.util.Map, boolean, java.util.Collection, java.nio.file.Path, java.util.List, java.util.Map, boolean, boolean) @NotNull + public final net.corda.testing.driver.DriverParameters copy(boolean, java.nio.file.Path, net.corda.testing.driver.PortAllocation, net.corda.testing.driver.PortAllocation, java.util.Map, boolean, boolean, boolean, java.util.List, java.util.List, net.corda.testing.driver.JmxPolicy, net.corda.core.node.NetworkParameters, java.util.Map, boolean, java.util.Collection, java.nio.file.Path, java.util.List, java.util.Map, boolean, boolean, java.time.Duration) + @NotNull public final net.corda.testing.driver.DriverParameters copy(boolean, java.nio.file.Path, net.corda.testing.driver.PortAllocation, net.corda.testing.driver.PortAllocation, java.util.Map, boolean, boolean, boolean, java.util.List, java.util.List, net.corda.testing.driver.JmxPolicy, net.corda.core.node.NetworkParameters, java.util.Set) public boolean equals(Object) public final boolean getAllowHibernateToManageAppSchema() @@ -8451,6 +8632,8 @@ public final class net.corda.testing.driver.DriverParameters extends java.lang.O @NotNull public final java.util.Map getNotaryCustomOverrides() @NotNull + public final java.time.Duration getNotaryHandleTimeout() + @NotNull public final java.util.List getNotarySpecs() @NotNull public final net.corda.testing.driver.PortAllocation getPortAllocation() @@ -8491,6 +8674,8 @@ public final class net.corda.testing.driver.DriverParameters extends java.lang.O @NotNull public final net.corda.testing.driver.DriverParameters withNotaryCustomOverrides(java.util.Map) @NotNull + public final net.corda.testing.driver.DriverParameters withNotaryHandleTimeout(java.time.Duration) + @NotNull public final net.corda.testing.driver.DriverParameters withNotarySpecs(java.util.List) @NotNull public final net.corda.testing.driver.DriverParameters withPortAllocation(net.corda.testing.driver.PortAllocation) @@ -8723,6 +8908,7 @@ public final class net.corda.testing.flows.FlowTestsUtilsKt extends java.lang.Ob public static final java.util.Map> receiveAll(net.corda.core.flows.FlowLogic, kotlin.Pair>, kotlin.Pair>...) @NotNull public static final rx.Observable registerCoreFlowFactory(net.corda.testing.node.internal.TestStartedNode, Class>, Class, kotlin.jvm.functions.Function1, boolean) + public static final void waitForAllFlowsToComplete(net.corda.testing.driver.NodeHandle, int, long) ## @DoNotImplement public abstract class net.corda.testing.node.ClusterSpec extends java.lang.Object @@ -9144,7 +9330,9 @@ public class net.corda.testing.node.MockServices extends java.lang.Object implem @NotNull public static final kotlin.Pair makeTestDatabaseAndPersistentServices(java.util.List, net.corda.testing.core.TestIdentity, net.corda.core.node.NetworkParameters, java.util.Set, java.util.Set, net.corda.testing.internal.TestingNamedCacheFactory) public void recordTransactions(Iterable) + public final void recordTransactions(Iterable, boolean) public void recordTransactions(net.corda.core.node.StatesToRecord, Iterable) + public final void recordTransactions(net.corda.core.transactions.SignedTransaction, boolean) public void recordTransactions(net.corda.core.transactions.SignedTransaction, net.corda.core.transactions.SignedTransaction...) public void recordTransactions(boolean, Iterable) public void recordTransactions(boolean, net.corda.core.transactions.SignedTransaction, net.corda.core.transactions.SignedTransaction...) diff --git a/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt b/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt index a6ce9e44ad..c120a1b620 100644 --- a/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt +++ b/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt @@ -10,7 +10,6 @@ import net.corda.core.contracts.StateAndRef import net.corda.core.contracts.TransactionVerificationException import net.corda.core.crypto.SecureHash import net.corda.core.crypto.TransactionSignature -import net.corda.core.flows.DistributionList import net.corda.core.flows.FinalityFlow import net.corda.core.flows.FlowException import net.corda.core.flows.FlowLogic @@ -24,7 +23,6 @@ import net.corda.core.flows.ReceiveFinalityFlow import net.corda.core.flows.ReceiveTransactionFlow import net.corda.core.flows.SendTransactionFlow import net.corda.core.flows.StartableByRPC -import net.corda.core.flows.TransactionMetadata import net.corda.core.flows.TransactionStatus import net.corda.core.flows.UnexpectedFlowEndException import net.corda.core.identity.CordaX500Name @@ -581,8 +579,7 @@ class FinalityFlowTests : WithFinality { val txBuilder = DummyContract.move(stateAndRef, newOwner) val stxn = serviceHub.signInitialTransaction(txBuilder, ourIdentity.owningKey) val sessionWithCounterParty = initiateFlow(newOwner) - subFlow(SendTransactionFlow(sessionWithCounterParty, stxn, - TransactionMetadata(ourIdentity.name, DistributionList(StatesToRecord.ONLY_RELEVANT, mapOf(BOB_NAME to StatesToRecord.ONLY_RELEVANT))))) + subFlow(SendTransactionFlow(stxn, setOf(sessionWithCounterParty), emptySet(), StatesToRecord.ONLY_RELEVANT)) throw UnexpectedFlowEndException("${stxn.id}") } } diff --git a/core-tests/src/test/kotlin/net/corda/coretests/flows/TestNoSecurityDataVendingFlow.kt b/core-tests/src/test/kotlin/net/corda/coretests/flows/TestNoSecurityDataVendingFlow.kt index c203e64d5b..ee5c04db1f 100644 --- a/core-tests/src/test/kotlin/net/corda/coretests/flows/TestNoSecurityDataVendingFlow.kt +++ b/core-tests/src/test/kotlin/net/corda/coretests/flows/TestNoSecurityDataVendingFlow.kt @@ -15,7 +15,7 @@ class TestNoSecurityDataVendingFlow(otherSideSession: FlowSession) : DataVending // Hack to not send the first message. otherSideSession.receive() } else { - super.sendPayloadAndReceiveDataRequest(this.otherSideSession, payload) + super.sendPayloadAndReceiveDataRequest(this.otherSessions.first(), payload) } } } \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/flows/FinalityFlow.kt b/core/src/main/kotlin/net/corda/core/flows/FinalityFlow.kt index 4770244c72..a3c1d518ef 100644 --- a/core/src/main/kotlin/net/corda/core/flows/FinalityFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/FinalityFlow.kt @@ -6,7 +6,6 @@ import net.corda.core.crypto.SecureHash import net.corda.core.crypto.TransactionSignature import net.corda.core.crypto.isFulfilledBy import net.corda.core.flows.NotarySigCheck.needsNotarySignature -import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party import net.corda.core.identity.groupAbstractPartyByWellKnownParty import net.corda.core.internal.FetchDataFlow @@ -16,7 +15,6 @@ import net.corda.core.internal.pushToLoggingContext import net.corda.core.internal.telemetry.telemetryServiceInternal import net.corda.core.internal.warnOnce import net.corda.core.node.StatesToRecord -import net.corda.core.node.StatesToRecord.ALL_VISIBLE import net.corda.core.node.StatesToRecord.ONLY_RELEVANT import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.SignedTransaction @@ -169,7 +167,6 @@ class FinalityFlow private constructor(val transaction: SignedTransaction, } private lateinit var externalTxParticipants: Set - private lateinit var txnMetadata: TransactionMetadata @Suspendable @Suppress("ComplexMethod", "NestedBlockDepth") @@ -221,8 +218,7 @@ class FinalityFlow private constructor(val transaction: SignedTransaction, val requiresNotarisation = needsNotarySignature(transaction) val useTwoPhaseFinality = serviceHub.myInfo.platformVersion >= PlatformVersionSwitches.TWO_PHASE_FINALITY - txnMetadata = TransactionMetadata(serviceHub.myInfo.legalIdentities.first().name, - DistributionList(statesToRecord, deriveStatesToRecord(newPlatformSessions))) + if (useTwoPhaseFinality) { val stxn = if (requiresNotarisation) { recordLocallyAndBroadcast(newPlatformSessions, transaction) @@ -283,29 +279,24 @@ class FinalityFlow private constructor(val transaction: SignedTransaction, @Suspendable private fun broadcast(sessions: Collection, tx: SignedTransaction) { serviceHub.telemetryServiceInternal.span("${this::class.java.name}#broadcast", flowLogic = this) { - sessions.forEach { session -> - try { - logger.debug { "Sending transaction to party $session." } - subFlow(SendTransactionFlow(session, tx, txnMetadata)) - txnMetadata = txnMetadata.copy(persist = false) - } catch (e: UnexpectedFlowEndException) { - throw UnexpectedFlowEndException( - "${session.counterparty} has finished prematurely and we're trying to send them a transaction." + - "Did they forget to call ReceiveFinalityFlow? (${e.message})", - e.cause, - e.originalErrorId - ) - } + try { + logger.debug { "Sending transaction to party sessions: $sessions." } + val (participantSessions, observerSessions) = deriveSessions(sessions) + subFlow(SendTransactionFlow(tx, participantSessions, observerSessions, statesToRecord)) + } catch (e: UnexpectedFlowEndException) { + throw UnexpectedFlowEndException( + "One of the sessions ${sessions.map { it.counterparty }} has finished prematurely and we're trying to send them a transaction." + + "Did they forget to call ReceiveFinalityFlow? (${e.message})", + e.cause, + e.originalErrorId + ) } } } - private fun deriveStatesToRecord(newPlatformSessions: Collection): Map { - val derivedObserverSessions = newPlatformSessions.map { it.counterparty }.toSet() - externalTxParticipants - val txParticipantSessions = externalTxParticipants - return txParticipantSessions.map { it.name to ONLY_RELEVANT }.toMap() + - (derivedObserverSessions + observerSessions.map { it.counterparty }).map { it.name to ALL_VISIBLE } - } + private fun deriveSessions(newPlatformSessions: Collection) = + Pair(newPlatformSessions.filter { it.counterparty in externalTxParticipants }.toSet(), + (observerSessions + newPlatformSessions.filter { it.counterparty !in externalTxParticipants }).toSet()) @Suspendable private fun broadcastSignaturesAndFinalise(sessions: Collection, notarySignatures: List) { @@ -373,19 +364,16 @@ class FinalityFlow private constructor(val transaction: SignedTransaction, logger.info("Broadcasting complete transaction to other participants.") if (newApi) { oldV3Broadcast(tx, oldParticipants.toSet()) - for (session in sessions) { - try { - logger.debug { "Sending transaction to party $session." } - subFlow(SendTransactionFlow(session, tx, txnMetadata)) - txnMetadata = txnMetadata.copy(persist = false) - } catch (e: UnexpectedFlowEndException) { - throw UnexpectedFlowEndException( - "${session.counterparty} has finished prematurely and we're trying to send them the finalised transaction. " + - "Did they forget to call ReceiveFinalityFlow? (${e.message})", - e.cause, - e.originalErrorId - ) - } + try { + logger.debug { "Sending transaction to party sessions $sessions." } + subFlow(SendTransactionFlow(tx, sessions.toSet(), emptySet(), statesToRecord)) + } catch (e: UnexpectedFlowEndException) { + throw UnexpectedFlowEndException( + "One of the sessions ${sessions.map { it.counterparty }} has finished prematurely and we're trying to send them the finalised transaction. " + + "Did they forget to call ReceiveFinalityFlow? (${e.message})", + e.cause, + e.originalErrorId + ) } } else { oldV3Broadcast(tx, (externalTxParticipants + oldParticipants).toSet()) @@ -396,15 +384,11 @@ class FinalityFlow private constructor(val transaction: SignedTransaction, @Suspendable private fun oldV3Broadcast(notarised: SignedTransaction, recipients: Set) { - for (recipient in recipients) { - if (!serviceHub.myInfo.isLegalIdentity(recipient)) { - logger.debug { "Sending transaction to party $recipient." } - val session = initiateFlow(recipient) - subFlow(SendTransactionFlow(session, notarised, txnMetadata)) - txnMetadata = txnMetadata.copy(persist = false) - logger.info("Party $recipient received the transaction.") - } - } + val remoteRecipients = recipients.filter { !serviceHub.myInfo.isLegalIdentity(it) } + logger.debug { "Sending transaction to parties $remoteRecipients." } + val sessions = remoteRecipients.map { initiateFlow(it) }.toSet() + subFlow(SendTransactionFlow(notarised, sessions, emptySet(), statesToRecord)) + logger.info("Parties $remoteRecipients received the transaction.") } private fun logCommandData() { diff --git a/core/src/main/kotlin/net/corda/core/flows/FlowTransaction.kt b/core/src/main/kotlin/net/corda/core/flows/FlowTransaction.kt index 532a90299d..05539f7480 100644 --- a/core/src/main/kotlin/net/corda/core/flows/FlowTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/flows/FlowTransaction.kt @@ -23,9 +23,8 @@ data class FlowTransactionInfo( @CordaSerializable data class TransactionMetadata( - val initiator: CordaX500Name, - val distributionList: DistributionList, - val persist: Boolean = true // hint to persist to transactional store + val initiator: CordaX500Name, + val distributionList: DistributionList ) @CordaSerializable diff --git a/core/src/main/kotlin/net/corda/core/flows/SendTransactionFlow.kt b/core/src/main/kotlin/net/corda/core/flows/SendTransactionFlow.kt index a02a105961..0daa48633f 100644 --- a/core/src/main/kotlin/net/corda/core/flows/SendTransactionFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/SendTransactionFlow.kt @@ -74,14 +74,22 @@ class MaybeSerializedSignedTransaction(override val id: SecureHash, val serializ * the right point in the conversation to receive the sent transaction and perform the resolution back-and-forth required * to check the dependencies and download any missing attachments. * - * @param otherSide the target party. - * @param stx the [SignedTransaction] being sent to the [otherSideSession]. - * @property txnMetadata transaction recovery metadata (eg. used by Two Phase Finality). + * @param stx the [SignedTransaction] being sent to the [otherSessions]. + * @param participantSessions the target parties which are participants to the transaction. + * @param observerSessions the target parties which are observers to the transaction. + * @param senderStatesToRecord the [StatesToRecord] relevancy information of the sender. */ -open class SendTransactionFlow(otherSide: FlowSession, stx: SignedTransaction, txnMetadata: TransactionMetadata) : DataVendingFlow(otherSide, stx, txnMetadata) { - constructor(otherSide: FlowSession, stx: SignedTransaction) : this(otherSide, stx, - TransactionMetadata(DUMMY_PARTICIPANT_NAME, DistributionList(StatesToRecord.NONE, mapOf(otherSide.counterparty.name to StatesToRecord.ALL_VISIBLE)))) - // Note: DUMMY_PARTICIPANT_NAME to be substituted with actual "ourIdentity.name" in flow call() +open class SendTransactionFlow(val stx: SignedTransaction, + val participantSessions: Set, + val observerSessions: Set, + val senderStatesToRecord: StatesToRecord) : DataVendingFlow(participantSessions + observerSessions, stx, + TransactionMetadata(DUMMY_PARTICIPANT_NAME, + DistributionList(senderStatesToRecord, + (participantSessions.map { it.counterparty.name to StatesToRecord.ONLY_RELEVANT}).toMap() + + (observerSessions.map { it.counterparty.name to StatesToRecord.ALL_VISIBLE}).toMap() + ))) { + constructor(otherSide: FlowSession, stx: SignedTransaction) : this(stx, setOf(otherSide), emptySet(), StatesToRecord.NONE) + // Note: DUMMY_PARTICIPANT_NAME to be substituted with actual "ourIdentity.name" in flow call() companion object { val DUMMY_PARTICIPANT_NAME = CordaX500Name("Transaction Participant", "London", "GB") } @@ -98,7 +106,8 @@ open class SendTransactionFlow(otherSide: FlowSession, stx: SignedTransaction, t */ open class SendStateAndRefFlow(otherSideSession: FlowSession, stateAndRefs: List>) : DataVendingFlow(otherSideSession, stateAndRefs) -open class DataVendingFlow(val otherSideSession: FlowSession, val payload: Any, val txnMetadata: TransactionMetadata? = null) : FlowLogic() { +open class DataVendingFlow(val otherSessions: Set, val payload: Any, private val txnMetadata: TransactionMetadata? = null) : FlowLogic() { + constructor(otherSideSession: FlowSession, payload: Any, txnMetadata: TransactionMetadata? = null) : this(setOf(otherSideSession), payload, txnMetadata) constructor(otherSideSession: FlowSession, payload: Any) : this(otherSideSession, payload, null) @Suspendable @@ -109,7 +118,7 @@ open class DataVendingFlow(val otherSideSession: FlowSession, val payload: Any, // User can override this method to perform custom request verification. } - @Suppress("ComplexCondition", "ComplexMethod") + @Suppress("ComplexCondition", "ComplexMethod", "LongMethod") @Suspendable override fun call(): Void? { val networkMaxMessageSize = serviceHub.networkParameters.maxMessageSize @@ -140,115 +149,126 @@ open class DataVendingFlow(val otherSideSession: FlowSession, val payload: Any, // store and share transaction recovery metadata if required val useTwoPhaseFinality = serviceHub.myInfo.platformVersion >= PlatformVersionSwitches.TWO_PHASE_FINALITY - val toTwoPhaseFinalityNode = serviceHub.networkMapCache.getNodeByLegalIdentity(otherSideSession.counterparty)?.platformVersion!! >= PlatformVersionSwitches.TWO_PHASE_FINALITY - if (txnMetadata != null && toTwoPhaseFinalityNode && useTwoPhaseFinality && payload is SignedTransaction) { - val encryptedDistributionList = (serviceHub as ServiceHubCoreInternal).recordSenderTransactionRecoveryMetadata(payload.id, txnMetadata.copy(initiator = ourIdentity.name)) - payload = SignedTransactionWithDistributionList(payload, encryptedDistributionList!!) + val toTwoPhaseFinalityNode = otherSessions.any { otherSideSession -> + serviceHub.networkMapCache.getNodeByLegalIdentity(otherSideSession.counterparty)?.platformVersion!! >= PlatformVersionSwitches.TWO_PHASE_FINALITY } + // record transaction recovery metadata once + val payloadWithMetadata = + if (txnMetadata != null && toTwoPhaseFinalityNode && useTwoPhaseFinality && payload is SignedTransaction) { + val encryptedDistributionList = (serviceHub as ServiceHubCoreInternal).recordSenderTransactionRecoveryMetadata(payload.id, txnMetadata.copy(initiator = ourIdentity.name)) + SignedTransactionWithDistributionList(payload, encryptedDistributionList!!) + } else null - // This loop will receive [FetchDataFlow.Request] continuously until the `otherSideSession` has all the data they need - // to resolve the transaction, a [FetchDataFlow.EndRequest] will be sent from the `otherSideSession` to indicate end of - // data request. - var loopCount = 0 - while (true) { - val loopCnt = loopCount++ - logger.trace { "DataVendingFlow: Main While [$loopCnt]..." } - val dataRequest = sendPayloadAndReceiveDataRequest(otherSideSession, payload).unwrap { request -> - logger.trace { "sendPayloadAndReceiveDataRequest(): ${request.javaClass.name}" } - when (request) { - is FetchDataFlow.Request.Data -> { - // Security TODO: Check for abnormally large or malformed data requests - verifyDataRequest(request) - request - } - FetchDataFlow.Request.End -> { - logger.trace { "DataVendingFlow: END" } - return null + otherSessions.forEachIndexed { idx, otherSideSession -> + if (payloadWithMetadata != null) + payload = payloadWithMetadata + // This loop will receive [FetchDataFlow.Request] continuously until the `otherSideSession` has all the data they need + // to resolve the transaction, a [FetchDataFlow.EndRequest] will be sent from the `otherSideSession` to indicate end of + // data request. + var loopCount = 0 + while (true) { + val loopCnt = loopCount++ + logger.trace { "DataVendingFlow: Main While [$loopCnt]..." } + val dataRequest = sendPayloadAndReceiveDataRequest(otherSideSession, payload).unwrap { request -> + logger.trace { "sendPayloadAndReceiveDataRequest(): ${request.javaClass.name}" } + when (request) { + is FetchDataFlow.Request.Data -> { + // Security TODO: Check for abnormally large or malformed data requests + verifyDataRequest(request) + request + } + FetchDataFlow.Request.End -> { + logger.trace { "DataVendingFlow: END" } + return@forEachIndexed + } } } - } - logger.trace { "Sending data (Type = ${dataRequest.dataType.name})" } - var totalByteCount = 0 - var firstItem = true - var batchFetchCountExceeded = false - var numSent = 0 - payload = when (dataRequest.dataType) { - FetchDataFlow.DataType.TRANSACTION -> dataRequest.hashes.map { txId -> - logger.trace { "Sending: TRANSACTION (dataRequest.hashes.size=${dataRequest.hashes.size})" } - if (!authorisedTransactions.isAuthorised(txId)) { - throw FetchDataFlow.IllegalTransactionRequest(txId) - } - val tx = serviceHub.validatedTransactions.getTransaction(txId) - ?: throw FetchDataFlow.HashNotFound(txId) - authorisedTransactions.removeAuthorised(tx.id) - authorisedTransactions.addAuthorised(getInputTransactions(tx)) - totalByteCount += tx.txBits.size - numSent++ - tx - } - // Loop on all items returned using dataRequest.hashes.map: - FetchDataFlow.DataType.BATCH_TRANSACTION -> dataRequest.hashes.map { txId -> - if (!authorisedTransactions.isAuthorised(txId)) { - throw FetchDataFlow.IllegalTransactionRequest(txId) - } - // Maybe we should not just throw here as it's not recoverable on the client side. Might be better to send a reason code or - // remove the restriction on sending once. - logger.trace { "Transaction authorised OK: '$txId'" } - var serialized: SerializedBytes? = null - if (!batchFetchCountExceeded) { - // Only fetch and serialize if we have not already exceeded the maximum byte count. Once we have, no more fetching - // is required, just reject all additional items. + logger.trace { "Sending data (Type = ${dataRequest.dataType.name})" } + var totalByteCount = 0 + var firstItem = true + var batchFetchCountExceeded = false + var numSent = 0 + payload = when (dataRequest.dataType) { + FetchDataFlow.DataType.TRANSACTION -> dataRequest.hashes.map { txId -> + logger.trace { "Sending: TRANSACTION (dataRequest.hashes.size=${dataRequest.hashes.size})" } + if (!authorisedTransactions.isAuthorised(txId)) { + throw FetchDataFlow.IllegalTransactionRequest(txId) + } val tx = serviceHub.validatedTransactions.getTransaction(txId) ?: throw FetchDataFlow.HashNotFound(txId) - logger.trace { "Transaction get OK: '$txId'" } - serialized = tx.serialize() - - val itemByteCount = serialized.size - logger.trace { "Batch-Send '$txId': first = $firstItem, Total bytes = $totalByteCount, Item byte count = $itemByteCount, Maximum = $maxPayloadSize" } - if (firstItem || (totalByteCount + itemByteCount) < maxPayloadSize) { - totalByteCount += itemByteCount - numSent++ - // Always include at least one item else if the max is set too low nothing will ever get returned. - // Splitting items will be a separate Jira if need be + if (idx == otherSessions.size - 1) authorisedTransactions.removeAuthorised(tx.id) - authorisedTransactions.addAuthorised(getInputTransactions(tx)) - logger.trace { "Adding item to return set: '$txId'" } - } else { - logger.trace { "Fetch block size EXCEEDED at '$txId'." } - batchFetchCountExceeded = true - } - } // end - - if (batchFetchCountExceeded) { - logger.trace { "Excluding '$txId' from return set due to exceeded count." } + authorisedTransactions.addAuthorised(getInputTransactions(tx)) + totalByteCount += tx.txBits.size + numSent++ + tx } + // Loop on all items returned using dataRequest.hashes.map: + FetchDataFlow.DataType.BATCH_TRANSACTION -> dataRequest.hashes.map { txId -> + if (!authorisedTransactions.isAuthorised(txId)) { + throw FetchDataFlow.IllegalTransactionRequest(txId) + } + // Maybe we should not just throw here as it's not recoverable on the client side. Might be better to send a reason code or + // remove the restriction on sending once. + logger.trace { "Transaction authorised OK: '$txId'" } + var serialized: SerializedBytes? = null + if (!batchFetchCountExceeded) { + // Only fetch and serialize if we have not already exceeded the maximum byte count. Once we have, no more fetching + // is required, just reject all additional items. + val tx = serviceHub.validatedTransactions.getTransaction(txId) + ?: throw FetchDataFlow.HashNotFound(txId) + logger.trace { "Transaction get OK: '$txId'" } + serialized = tx.serialize() - // Send null if limit is exceeded - val maybeserialized = MaybeSerializedSignedTransaction(txId, if (batchFetchCountExceeded) { - null - } else { - serialized - }, null) - firstItem = false - maybeserialized - } // Batch response loop end - FetchDataFlow.DataType.ATTACHMENT -> dataRequest.hashes.map { - logger.trace { "Sending: Attachments for '$it'" } - serviceHub.attachments.openAttachment(it)?.open()?.readFully() - ?: throw FetchDataFlow.HashNotFound(it) - } - FetchDataFlow.DataType.PARAMETERS -> dataRequest.hashes.map { - logger.trace { "Sending: Parameters for '$it'" } - (serviceHub.networkParametersService as NetworkParametersStorage).lookupSigned(it) - ?: throw FetchDataFlow.MissingNetworkParameters(it) - } - FetchDataFlow.DataType.UNKNOWN -> dataRequest.hashes.map { - logger.warn("Message from from a future version of Corda with UNKNOWN enum value for FetchDataFlow.DataType: ID='$it'") + val itemByteCount = serialized.size + logger.trace { "Batch-Send '$txId': first = $firstItem, Total bytes = $totalByteCount, Item byte count = $itemByteCount, Maximum = $maxPayloadSize" } + if (firstItem || (totalByteCount + itemByteCount) < maxPayloadSize) { + totalByteCount += itemByteCount + numSent++ + // Always include at least one item else if the max is set too low nothing will ever get returned. + // Splitting items will be a separate Jira if need be + if (idx == otherSessions.size - 1) + authorisedTransactions.removeAuthorised(tx.id) + authorisedTransactions.addAuthorised(getInputTransactions(tx)) + logger.trace { "Adding item to return set: '$txId'" } + } else { + logger.trace { "Fetch block size EXCEEDED at '$txId'." } + batchFetchCountExceeded = true + } + } // end + + if (batchFetchCountExceeded) { + logger.trace { "Excluding '$txId' from return set due to exceeded count." } + } + + // Send null if limit is exceeded + val maybeserialized = MaybeSerializedSignedTransaction(txId, if (batchFetchCountExceeded) { + null + } else { + serialized + }, null) + firstItem = false + maybeserialized + } // Batch response loop end + FetchDataFlow.DataType.ATTACHMENT -> dataRequest.hashes.map { + logger.trace { "Sending: Attachments for '$it'" } + serviceHub.attachments.openAttachment(it)?.open()?.readFully() + ?: throw FetchDataFlow.HashNotFound(it) + } + FetchDataFlow.DataType.PARAMETERS -> dataRequest.hashes.map { + logger.trace { "Sending: Parameters for '$it'" } + (serviceHub.networkParametersService as NetworkParametersStorage).lookupSigned(it) + ?: throw FetchDataFlow.MissingNetworkParameters(it) + } + FetchDataFlow.DataType.UNKNOWN -> dataRequest.hashes.map { + logger.warn("Message from from a future version of Corda with UNKNOWN enum value for FetchDataFlow.DataType: ID='$it'") + } } + logger.trace { "Block total size = $totalByteCount: Num Items = ($numSent of ${dataRequest.hashes.size} total)" } } - logger.trace { "Block total size = $totalByteCount: Num Items = ($numSent of ${dataRequest.hashes.size} total)" } } + return null } @Suspendable diff --git a/node/src/integration-test/kotlin/net/corda/node/services/DeterministicContractWithCustomSerializerTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/DeterministicContractWithCustomSerializerTest.kt index 7aef99a62f..a6a8e4221e 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/DeterministicContractWithCustomSerializerTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/DeterministicContractWithCustomSerializerTest.kt @@ -26,6 +26,7 @@ import org.junit.jupiter.api.assertDoesNotThrow import org.junit.jupiter.api.assertThrows @Suppress("FunctionName") +@Ignore("flaky tests in CI") class DeterministicContractWithCustomSerializerTest { companion object { val logger = loggerFor() @@ -61,7 +62,6 @@ class DeterministicContractWithCustomSerializerTest { } @Test(timeout=300_000) - @Ignore("Flaky test in CI: org.opentest4j.AssertionFailedError: Unexpected exception thrown: net.corda.client.rpc.RPCException: Class \"class net.corda.contracts.serialization.custom.Currantsy\" is not on the whitelist or annotated with @CordaSerializable.") fun `test DJVM can verify using custom serializer`() { driver(parametersFor(djvmSources, listOf(flowCordapp, contractCordapp))) { val alice = startNode(providedName = ALICE_NAME).getOrThrow() diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index 5fbbde1296..1c12dc61b3 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -38,11 +38,14 @@ import net.corda.core.internal.VisibleForTesting import net.corda.core.internal.concurrent.flatMap import net.corda.core.internal.concurrent.map import net.corda.core.internal.concurrent.openFuture -import net.corda.core.internal.concurrent.thenMatch import net.corda.core.internal.div import net.corda.core.internal.messaging.AttachmentTrustInfoRPCOps import net.corda.core.internal.notary.NotaryService import net.corda.core.internal.rootMessage +import net.corda.core.internal.telemetry.OpenTelemetryComponent +import net.corda.core.internal.telemetry.SimpleLogTelemetryComponent +import net.corda.core.internal.telemetry.TelemetryComponent +import net.corda.core.internal.telemetry.TelemetryServiceImpl import net.corda.core.internal.uncheckedCast import net.corda.core.messaging.ClientRpcSslOptions import net.corda.core.messaging.CordaRPCOps @@ -57,10 +60,6 @@ import net.corda.core.node.services.ContractUpgradeService import net.corda.core.node.services.CordaService import net.corda.core.node.services.IdentityService import net.corda.core.node.services.KeyManagementService -import net.corda.core.internal.telemetry.SimpleLogTelemetryComponent -import net.corda.core.internal.telemetry.TelemetryComponent -import net.corda.core.internal.telemetry.OpenTelemetryComponent -import net.corda.core.internal.telemetry.TelemetryServiceImpl import net.corda.core.node.services.TelemetryService import net.corda.core.node.services.TransactionVerifierService import net.corda.core.node.services.diagnostics.DiagnosticsService @@ -659,6 +658,8 @@ abstract class AbstractNode(val configuration: NodeConfiguration, tokenizableServices = null verifyCheckpointsCompatible(frozenTokenizableServices) + partyInfoCache.start() + /* Note the .get() at the end of the distributeEvent call, below. This will block until all Corda Services have returned from processing the event, allowing a service to prevent the state machine manager from starting (just below this) until the service is ready. @@ -697,9 +698,6 @@ abstract class AbstractNode(val configuration: NodeConfiguration, log.warn("Not distributing events as NetworkMap is not ready") } } - nodeReadyFuture.thenMatch({ - partyInfoCache.start() - }, { th -> log.error("Unexpected exception during cache initialisation", th) }) setNodeStatus(NodeStatus.STARTED) return resultingNodeInfo diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecovery.kt b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecovery.kt index 159fe1383a..ae1c066ab2 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecovery.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecovery.kt @@ -141,14 +141,13 @@ class DBTransactionStorageLedgerRecovery(private val database: CordaPersistence, override fun addSenderTransactionRecoveryMetadata(id: SecureHash, metadata: TransactionMetadata): ByteArray { return database.transaction { - if (metadata.persist) - metadata.distributionList.peersToStatesToRecord.map { (peer, _) -> - val senderDistributionRecord = DBSenderDistributionRecord(PersistentKey(Key(clock.instant())), - id.toString(), - partyInfoCache.getPartyIdByCordaX500Name(peer), - metadata.distributionList.senderStatesToRecord) - session.save(senderDistributionRecord) - } + metadata.distributionList.peersToStatesToRecord.map { (peer, _) -> + val senderDistributionRecord = DBSenderDistributionRecord(PersistentKey(Key(clock.instant())), + id.toString(), + partyInfoCache.getPartyIdByCordaX500Name(peer), + metadata.distributionList.senderStatesToRecord) + session.save(senderDistributionRecord) + } val hashedPeersToStatesToRecord = metadata.distributionList.peersToStatesToRecord.map { (peer, statesToRecord) -> partyInfoCache.getPartyIdByCordaX500Name(peer) to statesToRecord }.toMap() val hashedDistributionList = HashedDistributionList(metadata.distributionList.senderStatesToRecord, hashedPeersToStatesToRecord) From 49d5b6a4bfe577c7970bae6d022222ad498a3f33 Mon Sep 17 00:00:00 2001 From: Jose Coll Date: Thu, 29 Jun 2023 11:19:59 +0100 Subject: [PATCH 034/126] ENT-4973 Introduce explicit constructors to evolve ReceiveFinalityFlow for binary backwards compatibility (#7404) * Introduce explicit constructors to evolve ReceiveFinalityFlow for binary backwards compatibility. --- .../kotlin/net/corda/core/flows/FinalityFlow.kt | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/flows/FinalityFlow.kt b/core/src/main/kotlin/net/corda/core/flows/FinalityFlow.kt index a3c1d518ef..46902c19b2 100644 --- a/core/src/main/kotlin/net/corda/core/flows/FinalityFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/FinalityFlow.kt @@ -16,6 +16,7 @@ import net.corda.core.internal.telemetry.telemetryServiceInternal import net.corda.core.internal.warnOnce import net.corda.core.node.StatesToRecord import net.corda.core.node.StatesToRecord.ONLY_RELEVANT +import net.corda.core.serialization.DeprecatedConstructorForDeserialization import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.ProgressTracker @@ -483,10 +484,16 @@ object NotarySigCheck { * @param statesToRecord Which states to commit to the vault. Defaults to [StatesToRecord.ONLY_RELEVANT]. * @param handlePropagatedNotaryError Whether to catch and propagate Double Spend exception to peers. */ -class ReceiveFinalityFlow @JvmOverloads constructor(private val otherSideSession: FlowSession, - private val expectedTxId: SecureHash? = null, - private val statesToRecord: StatesToRecord = ONLY_RELEVANT, - private val handlePropagatedNotaryError: Boolean? = null) : FlowLogic() { +class ReceiveFinalityFlow(private val otherSideSession: FlowSession, + private val expectedTxId: SecureHash? = null, + private val statesToRecord: StatesToRecord = ONLY_RELEVANT, + private val handlePropagatedNotaryError: Boolean? = null) : FlowLogic() { + + @DeprecatedConstructorForDeserialization(version = 1) + @JvmOverloads constructor(otherSideSession: FlowSession, + expectedTxId: SecureHash? = null, + statesToRecord: StatesToRecord = ONLY_RELEVANT) : this(otherSideSession, expectedTxId, statesToRecord, null) + @Suppress("ComplexMethod", "NestedBlockDepth") @Suspendable override fun call(): SignedTransaction { From 97d8549d4f4f1eb1c0b884224ef91585c52b1cbf Mon Sep 17 00:00:00 2001 From: Jose Coll Date: Mon, 3 Jul 2023 10:02:34 +0100 Subject: [PATCH 035/126] ENT-9927 Ledger Recovery tweaks. (#7407) --- .../corda/core/flows/ReceiveTransactionFlow.kt | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/flows/ReceiveTransactionFlow.kt b/core/src/main/kotlin/net/corda/core/flows/ReceiveTransactionFlow.kt index 435b14605b..a4f2defa3a 100644 --- a/core/src/main/kotlin/net/corda/core/flows/ReceiveTransactionFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/ReceiveTransactionFlow.kt @@ -60,11 +60,7 @@ open class ReceiveTransactionFlow constructor(private val otherSideSession: Flow } val payload = otherSideSession.receive().unwrap { it } - val stx = - if (payload is SignedTransactionWithDistributionList) { - (serviceHub as ServiceHubCoreInternal).recordReceiverTransactionRecoveryMetadata(payload.stx.id, otherSideSession.counterparty.name, ourIdentity.name, statesToRecord, payload.distributionList) - payload.stx - } else payload as SignedTransaction + val stx = resolvePayload(payload) stx.pushToLoggingContext() logger.info("Received transaction acknowledgement request from party ${otherSideSession.counterparty}.") checkParameterHash(stx.networkParametersHash) @@ -87,6 +83,15 @@ open class ReceiveTransactionFlow constructor(private val otherSideSession: Flow return stx } + open fun resolvePayload(payload: Any): SignedTransaction { + return if (payload is SignedTransactionWithDistributionList) { + if (checkSufficientSignatures || deferredAck) { + (serviceHub as ServiceHubCoreInternal).recordReceiverTransactionRecoveryMetadata(payload.stx.id, otherSideSession.counterparty.name, ourIdentity.name, statesToRecord, payload.distributionList) + payload.stx + } else payload.stx + } else payload as SignedTransaction + } + /** * Hook to perform extra checks on the received transaction just before it's recorded. The transaction has already * been resolved and verified at this point. From 0f2312a2010034450d373c9b78e78f77e150b57a Mon Sep 17 00:00:00 2001 From: Chris Cochrane <78791827+chriscochrane@users.noreply.github.com> Date: Fri, 7 Jul 2023 13:29:35 +0100 Subject: [PATCH 036/126] ENT-8983 - Postgres migration failure (#7408) * Set DB transaction isolation level only if its going to change; upgraded dependencies * Removed duplicate changeset --- .../kotlin/net/corda/common/logging/Constants.kt | 2 +- constants.properties | 6 +++--- .../internal/persistence/DatabaseTransaction.kt | 5 ++++- ...tary.changelog-committed-transactions-table.xml | 14 -------------- .../migration/node-notary.changelog-master.xml | 1 - 5 files changed, 8 insertions(+), 20 deletions(-) delete mode 100644 node/src/main/resources/migration/node-notary.changelog-committed-transactions-table.xml diff --git a/common/logging/src/main/kotlin/net/corda/common/logging/Constants.kt b/common/logging/src/main/kotlin/net/corda/common/logging/Constants.kt index 569cf90441..cc86ed31ab 100644 --- a/common/logging/src/main/kotlin/net/corda/common/logging/Constants.kt +++ b/common/logging/src/main/kotlin/net/corda/common/logging/Constants.kt @@ -9,4 +9,4 @@ package net.corda.common.logging * (originally added to source control for ease of use) */ -internal const val CURRENT_MAJOR_RELEASE = "4.11-SNAPSHOT" +internal const val CURRENT_MAJOR_RELEASE = "4.11-SNAPSHOT" \ No newline at end of file diff --git a/constants.properties b/constants.properties index aa67284600..bc90310d62 100644 --- a/constants.properties +++ b/constants.properties @@ -78,9 +78,9 @@ mockitoKotlinVersion=1.6.0 hamkrestVersion=1.7.0.0 joptSimpleVersion=5.0.2 jansiVersion=1.18 -hibernateVersion=5.6.5.Final +hibernateVersion=5.6.14.Final # h2Version - Update docs if renamed or removed. -h2Version=2.1.212 +h2Version=2.1.214 rxjavaVersion=1.3.8 dokkaVersion=0.10.1 eddsaVersion=0.3.0 @@ -89,7 +89,7 @@ commonsCollectionsVersion=4.3 beanutilsVersion=1.9.4 shiroVersion=1.10.0 hikariVersion=3.3.1 -liquibaseVersion=4.18.0 +liquibaseVersion=4.20.0 dockerComposeRuleVersion=1.5.0 seleniumVersion=3.141.59 ghostdriverVersion=2.1.0 diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/DatabaseTransaction.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/DatabaseTransaction.kt index 0d69706eb1..f4482c702c 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/DatabaseTransaction.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/DatabaseTransaction.kt @@ -29,7 +29,10 @@ class DatabaseTransaction( val connection: Connection by lazy(LazyThreadSafetyMode.NONE) { database.dataSource.connection.apply { autoCommit = false - transactionIsolation = isolation + // only set the transaction isolation level if it's actually changed - setting isn't free. + if (transactionIsolation != isolation) { + transactionIsolation = isolation + } } } diff --git a/node/src/main/resources/migration/node-notary.changelog-committed-transactions-table.xml b/node/src/main/resources/migration/node-notary.changelog-committed-transactions-table.xml deleted file mode 100644 index e4fa2cc68e..0000000000 --- a/node/src/main/resources/migration/node-notary.changelog-committed-transactions-table.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - diff --git a/node/src/main/resources/migration/node-notary.changelog-master.xml b/node/src/main/resources/migration/node-notary.changelog-master.xml index 860fd7ddff..9c342362e1 100644 --- a/node/src/main/resources/migration/node-notary.changelog-master.xml +++ b/node/src/main/resources/migration/node-notary.changelog-master.xml @@ -8,7 +8,6 @@ - From 58ecce1713d5f4fc62f5ff63b022aca574ce94e1 Mon Sep 17 00:00:00 2001 From: Tom Stark <47384103+tomstark99@users.noreply.github.com> Date: Fri, 14 Jul 2023 12:02:21 +0100 Subject: [PATCH 037/126] ENT-9875: New network parameters (#7398) * ENT-9875: Added new network parameters - Added `transactionRecoveryPeriod` - Added `confidentialIdentityPreGenerationPeriod` These new parameters are currently set to be nullable meaning they can be ignored and the duration if not specified will be null rather than, e.g., 0. This currently allows for nothing changing/breaking in the node-api _Note: if these params can stay as nullable then the deprecated constructor might not even be needed (since the existing one will still work), needs to be discussed._ --- .../coretests/node/NetworkParametersTest.kt | 28 ++++++ .../net/corda/core/node/NetworkParameters.kt | 91 +++++++++++++++++-- .../node/services/network/NetworkMapTest.kt | 7 +- .../common/internal/ParametersUtilities.kt | 9 +- 4 files changed, 125 insertions(+), 10 deletions(-) diff --git a/core-tests/src/test/kotlin/net/corda/coretests/node/NetworkParametersTest.kt b/core-tests/src/test/kotlin/net/corda/coretests/node/NetworkParametersTest.kt index a9da9eada4..aabda7d94d 100644 --- a/core-tests/src/test/kotlin/net/corda/coretests/node/NetworkParametersTest.kt +++ b/core-tests/src/test/kotlin/net/corda/coretests/node/NetworkParametersTest.kt @@ -30,6 +30,8 @@ import java.time.Duration import java.time.Instant import kotlin.test.assertEquals import kotlin.test.assertFails +import kotlin.test.assertNotEquals +import kotlin.test.assertNull class NetworkParametersTest { private val mockNet = InternalMockNetwork( @@ -93,6 +95,32 @@ class NetworkParametersTest { assertEquals(twoDays, nm2.eventHorizon) } + @Test(timeout=300_000) + fun `that transactionRecoveryPeriod and confidentialIdentityPreGenerationPeriod aren't required`() { + // this is defensive tests in response to CORDA-2769 + val aliceNotaryParty = TestIdentity(ALICE_NAME).party + val aliceNotaryInfo = NotaryInfo(aliceNotaryParty, false) + val nm1 = NetworkParameters( + minimumPlatformVersion = 1, + notaries = listOf(aliceNotaryInfo), + maxMessageSize = Int.MAX_VALUE, + maxTransactionSize = Int.MAX_VALUE, + modifiedTime = Instant.now(), + epoch = 1, + whitelistedContractImplementations = mapOf("MyClass" to listOf(AttachmentId.allOnesHash)), + eventHorizon = Duration.ofDays(1) + ) + + assertNull(nm1.recoveryMaximumBackupInterval) + assertNull(nm1.confidentialIdentityMinimumBackupInterval) + + val nm2 = nm1.copy(recoveryMaximumBackupInterval = 10.days, confidentialIdentityMinimumBackupInterval = 10.days) + + assertNotEquals(nm1.recoveryMaximumBackupInterval, nm2.recoveryMaximumBackupInterval) + assertNotEquals(nm1.confidentialIdentityMinimumBackupInterval, nm2.confidentialIdentityMinimumBackupInterval) + + } + // Notaries tests @Test(timeout=300_000) fun `choosing notary not specified in network parameters will fail`() { diff --git a/core/src/main/kotlin/net/corda/core/node/NetworkParameters.kt b/core/src/main/kotlin/net/corda/core/node/NetworkParameters.kt index 50fe5eb258..4495a483ef 100644 --- a/core/src/main/kotlin/net/corda/core/node/NetworkParameters.kt +++ b/core/src/main/kotlin/net/corda/core/node/NetworkParameters.kt @@ -34,10 +34,19 @@ import java.util.Collections.unmodifiableMap * @property packageOwnership ([AutoAcceptable]) List of the network-wide java packages that were successfully claimed by their owners. * Any CorDapp JAR that offers contracts and states in any of these packages must be signed by the owner. * @property eventHorizon Time after which nodes will be removed from the network map if they have not been seen - * during this period + * during this period. + * @property recoveryMaximumBackupInterval A default value, that will be used by the Ledger Recovery flows to set how far back in time to + * consider for recovery. The expectation is that a node will restore to a database backup that is no older than this, by default, when + * attempting a recovery. This value can be overridden by specifying an override to the flow. It can also be overridden if the same parameter + * is specified, per-node in the node configuration. An override to the flow takes priority in terms of overrides. It is optional in both + * the network parameters and the node configuration however if no values are set then it needs to be specified in the flow. + * @property confidentialIdentityMinimumBackupInterval A default value for the minimum age of a generated confidential identity key before + * it can be used. This can be overridden in the node configuration or if a more recent database backup is indicated via RPC / shell. It is + * optional in both the network parameters and the node configuration and if no value is set for either then it is assumed to be zero. */ @KeepForDJVM @CordaSerializable +@Suppress("LongParameterList") data class NetworkParameters( val minimumPlatformVersion: Int, val notaries: List, @@ -47,10 +56,12 @@ data class NetworkParameters( @AutoAcceptable val epoch: Int, @AutoAcceptable val whitelistedContractImplementations: Map>, val eventHorizon: Duration, - @AutoAcceptable val packageOwnership: Map + @AutoAcceptable val packageOwnership: Map, + val recoveryMaximumBackupInterval: Duration? = null, + val confidentialIdentityMinimumBackupInterval: Duration? = null ) { // DOCEND 1 - @DeprecatedConstructorForDeserialization(1) + @DeprecatedConstructorForDeserialization(version = 1) constructor(minimumPlatformVersion: Int, notaries: List, maxMessageSize: Int, @@ -69,7 +80,7 @@ data class NetworkParameters( emptyMap() ) - @DeprecatedConstructorForDeserialization(2) + @DeprecatedConstructorForDeserialization(version = 2) constructor(minimumPlatformVersion: Int, notaries: List, maxMessageSize: Int, @@ -89,6 +100,29 @@ data class NetworkParameters( emptyMap() ) + @DeprecatedConstructorForDeserialization(version = 3) + constructor(minimumPlatformVersion: Int, + notaries: List, + maxMessageSize: Int, + maxTransactionSize: Int, + modifiedTime: Instant, + epoch: Int, + whitelistedContractImplementations: Map>, + eventHorizon: Duration, + packageOwnership: Map + ) : this(minimumPlatformVersion, + notaries, + maxMessageSize, + maxTransactionSize, + modifiedTime, + epoch, + whitelistedContractImplementations, + eventHorizon, + packageOwnership, + recoveryMaximumBackupInterval = null, + confidentialIdentityMinimumBackupInterval = null + ) + init { require(minimumPlatformVersion > 0) { "Minimum platform level must be at least 1" } require(notaries.distinctBy { it.identity } == notaries) { "Duplicate notary identities" } @@ -98,6 +132,41 @@ data class NetworkParameters( require(!eventHorizon.isNegative) { "Event Horizon must be a positive value" } packageOwnership.keys.forEach(::requirePackageValid) require(noPackageOverlap(packageOwnership.keys)) { "Multiple packages added to the packageOwnership overlap." } + require(recoveryMaximumBackupInterval == null || !recoveryMaximumBackupInterval.isNegative) { + "Recovery maximum backup interval must be a positive value" + } + require(confidentialIdentityMinimumBackupInterval == null || !confidentialIdentityMinimumBackupInterval.isNegative) { + "Confidential Identities maximum backup interval must be a positive value" + } + } + + /** + * This is to address backwards compatibility of the API, invariant to package ownership + * addresses bug CORDA-2769 + */ + fun copy(minimumPlatformVersion: Int = this.minimumPlatformVersion, + notaries: List = this.notaries, + maxMessageSize: Int = this.maxMessageSize, + maxTransactionSize: Int = this.maxTransactionSize, + modifiedTime: Instant = this.modifiedTime, + epoch: Int = this.epoch, + whitelistedContractImplementations: Map> = this.whitelistedContractImplementations, + eventHorizon: Duration = this.eventHorizon, + packageOwnership: Map = this.packageOwnership + ): NetworkParameters { + return NetworkParameters( + minimumPlatformVersion = minimumPlatformVersion, + notaries = notaries, + maxMessageSize = maxMessageSize, + maxTransactionSize = maxTransactionSize, + modifiedTime = modifiedTime, + epoch = epoch, + whitelistedContractImplementations = whitelistedContractImplementations, + eventHorizon = eventHorizon, + packageOwnership = packageOwnership, + recoveryMaximumBackupInterval = recoveryMaximumBackupInterval, + confidentialIdentityMinimumBackupInterval = confidentialIdentityMinimumBackupInterval + ) } /** @@ -122,7 +191,9 @@ data class NetworkParameters( epoch = epoch, whitelistedContractImplementations = whitelistedContractImplementations, eventHorizon = eventHorizon, - packageOwnership = packageOwnership + packageOwnership = packageOwnership, + recoveryMaximumBackupInterval = recoveryMaximumBackupInterval, + confidentialIdentityMinimumBackupInterval = confidentialIdentityMinimumBackupInterval ) } @@ -147,7 +218,9 @@ data class NetworkParameters( epoch = epoch, whitelistedContractImplementations = whitelistedContractImplementations, eventHorizon = eventHorizon, - packageOwnership = packageOwnership + packageOwnership = packageOwnership, + recoveryMaximumBackupInterval = recoveryMaximumBackupInterval, + confidentialIdentityMinimumBackupInterval = confidentialIdentityMinimumBackupInterval ) } @@ -166,6 +239,8 @@ data class NetworkParameters( } modifiedTime=$modifiedTime epoch=$epoch + transactionRecoveryPeriod=$recoveryMaximumBackupInterval + confidentialIdentityPreGenerationPeriod=$confidentialIdentityMinimumBackupInterval }""" } @@ -181,7 +256,9 @@ data class NetworkParameters( unmodifiableList(entry.value) }, eventHorizon = eventHorizon, - packageOwnership = unmodifiable(packageOwnership) + packageOwnership = unmodifiable(packageOwnership), + recoveryMaximumBackupInterval = recoveryMaximumBackupInterval, + confidentialIdentityMinimumBackupInterval = confidentialIdentityMinimumBackupInterval ) } } diff --git a/node/src/integration-test/kotlin/net/corda/node/services/network/NetworkMapTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/network/NetworkMapTest.kt index 6a690004d4..a3738df972 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/network/NetworkMapTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/network/NetworkMapTest.kt @@ -7,6 +7,7 @@ import net.corda.core.messaging.ParametersUpdateInfo import net.corda.core.node.NetworkParameters import net.corda.core.node.NodeInfo import net.corda.core.serialization.serialize +import net.corda.core.utilities.days import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.seconds import net.corda.nodeapi.internal.SignedNodeInfo @@ -76,7 +77,11 @@ class NetworkMapTest { val nextParams = networkMapServer.networkParameters.copy( epoch = 3, modifiedTime = Instant.ofEpochMilli(random63BitValue()), - maxMessageSize = networkMapServer.networkParameters.maxMessageSize + 1) + maxMessageSize = networkMapServer.networkParameters.maxMessageSize + 1, + recoveryMaximumBackupInterval = networkMapServer.networkParameters + .recoveryMaximumBackupInterval?.minus(10.days) ?: 10.days, + confidentialIdentityMinimumBackupInterval = networkMapServer.networkParameters + .confidentialIdentityMinimumBackupInterval?.minus(10.days) ?: 10.days) val nextHash = nextParams.serialize().hash val snapshot = alice.rpc.networkParametersFeed().snapshot val updates = alice.rpc.networkParametersFeed().updates.bufferUntilSubscribed() diff --git a/testing/test-common/src/main/kotlin/net/corda/testing/common/internal/ParametersUtilities.kt b/testing/test-common/src/main/kotlin/net/corda/testing/common/internal/ParametersUtilities.kt index 27d61381f4..9b0935f471 100644 --- a/testing/test-common/src/main/kotlin/net/corda/testing/common/internal/ParametersUtilities.kt +++ b/testing/test-common/src/main/kotlin/net/corda/testing/common/internal/ParametersUtilities.kt @@ -10,6 +10,7 @@ import java.time.Duration import java.time.Instant @JvmOverloads +@Suppress("LongParameterList") fun testNetworkParameters( notaries: List = emptyList(), minimumPlatformVersion: Int = 1, @@ -20,7 +21,9 @@ fun testNetworkParameters( whitelistedContractImplementations: Map> = emptyMap(), epoch: Int = 1, eventHorizon: Duration = 30.days, - packageOwnership: Map = emptyMap() + packageOwnership: Map = emptyMap(), + recoveryMaximumBackupInterval: Duration = 30.days, + confidentialIdentityMinimumBackupInterval: Duration = 30.days ): NetworkParameters { return NetworkParameters( minimumPlatformVersion = minimumPlatformVersion, @@ -31,7 +34,9 @@ fun testNetworkParameters( epoch = epoch, whitelistedContractImplementations = whitelistedContractImplementations, eventHorizon = eventHorizon, - packageOwnership = packageOwnership + packageOwnership = packageOwnership, + recoveryMaximumBackupInterval = recoveryMaximumBackupInterval, + confidentialIdentityMinimumBackupInterval = confidentialIdentityMinimumBackupInterval ) } From c3e8284f280713b758b4456155aaf15fd41943f6 Mon Sep 17 00:00:00 2001 From: Jose Coll Date: Mon, 17 Jul 2023 09:48:37 +0100 Subject: [PATCH 038/126] ENT-9927 Ledger Recovery tweaks (#7409) --- .../net/corda/core/flows/FinalityFlow.kt | 4 +-- .../corda/core/flows/SendTransactionFlow.kt | 30 +++++++++++++++---- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/flows/FinalityFlow.kt b/core/src/main/kotlin/net/corda/core/flows/FinalityFlow.kt index 46902c19b2..36b5174ad8 100644 --- a/core/src/main/kotlin/net/corda/core/flows/FinalityFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/FinalityFlow.kt @@ -283,7 +283,7 @@ class FinalityFlow private constructor(val transaction: SignedTransaction, try { logger.debug { "Sending transaction to party sessions: $sessions." } val (participantSessions, observerSessions) = deriveSessions(sessions) - subFlow(SendTransactionFlow(tx, participantSessions, observerSessions, statesToRecord)) + subFlow(SendTransactionFlow(tx, participantSessions, observerSessions, statesToRecord, true)) } catch (e: UnexpectedFlowEndException) { throw UnexpectedFlowEndException( "One of the sessions ${sessions.map { it.counterparty }} has finished prematurely and we're trying to send them a transaction." + @@ -367,7 +367,7 @@ class FinalityFlow private constructor(val transaction: SignedTransaction, oldV3Broadcast(tx, oldParticipants.toSet()) try { logger.debug { "Sending transaction to party sessions $sessions." } - subFlow(SendTransactionFlow(tx, sessions.toSet(), emptySet(), statesToRecord)) + subFlow(SendTransactionFlow(tx, sessions.toSet(), emptySet(), statesToRecord, true)) } catch (e: UnexpectedFlowEndException) { throw UnexpectedFlowEndException( "One of the sessions ${sessions.map { it.counterparty }} has finished prematurely and we're trying to send them the finalised transaction. " + diff --git a/core/src/main/kotlin/net/corda/core/flows/SendTransactionFlow.kt b/core/src/main/kotlin/net/corda/core/flows/SendTransactionFlow.kt index 0daa48633f..93ed7d0c97 100644 --- a/core/src/main/kotlin/net/corda/core/flows/SendTransactionFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/SendTransactionFlow.kt @@ -6,6 +6,7 @@ import net.corda.core.contracts.StateAndRef import net.corda.core.crypto.SecureHash import net.corda.core.identity.CordaX500Name import net.corda.core.internal.* +import net.corda.core.node.ServicesForResolution import net.corda.core.node.StatesToRecord import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.SerializedBytes @@ -78,20 +79,37 @@ class MaybeSerializedSignedTransaction(override val id: SecureHash, val serializ * @param participantSessions the target parties which are participants to the transaction. * @param observerSessions the target parties which are observers to the transaction. * @param senderStatesToRecord the [StatesToRecord] relevancy information of the sender. + * @param recordMetaDataEvenIfNotFullySigned whether to store recovery metadata when a txn is not fully signed. */ open class SendTransactionFlow(val stx: SignedTransaction, val participantSessions: Set, val observerSessions: Set, - val senderStatesToRecord: StatesToRecord) : DataVendingFlow(participantSessions + observerSessions, stx, - TransactionMetadata(DUMMY_PARTICIPANT_NAME, - DistributionList(senderStatesToRecord, - (participantSessions.map { it.counterparty.name to StatesToRecord.ONLY_RELEVANT}).toMap() + - (observerSessions.map { it.counterparty.name to StatesToRecord.ALL_VISIBLE}).toMap() - ))) { + val senderStatesToRecord: StatesToRecord, + private val recordMetaDataEvenIfNotFullySigned: Boolean = false) + : DataVendingFlow(participantSessions + observerSessions, stx, + makeMetaData(stx, recordMetaDataEvenIfNotFullySigned, senderStatesToRecord, participantSessions, observerSessions)) { + constructor(otherSide: FlowSession, stx: SignedTransaction) : this(stx, setOf(otherSide), emptySet(), StatesToRecord.NONE) + // Note: DUMMY_PARTICIPANT_NAME to be substituted with actual "ourIdentity.name" in flow call() companion object { val DUMMY_PARTICIPANT_NAME = CordaX500Name("Transaction Participant", "London", "GB") + + fun makeMetaData(stx: SignedTransaction, recordMetaDataEvenIfNotFullySigned: Boolean, senderStatesToRecord: StatesToRecord, participantSessions: Set, observerSessions: Set): TransactionMetadata? { + return if (recordMetaDataEvenIfNotFullySigned || isFullySigned(stx)) + TransactionMetadata(DUMMY_PARTICIPANT_NAME, + DistributionList(senderStatesToRecord, + (participantSessions.map { it.counterparty.name to StatesToRecord.ONLY_RELEVANT}).toMap() + + (observerSessions.map { it.counterparty.name to StatesToRecord.ALL_VISIBLE}).toMap())) + else null + } + + private fun isFullySigned(stx: SignedTransaction): Boolean { + val serviceHub = (currentTopLevel?.serviceHub as? ServicesForResolution) + return if (serviceHub != null) + stx.resolveTransactionWithSignatures(serviceHub).getMissingSigners().isEmpty() + else false + } } } From 669d6590af92e7006530e02a76e803e1d7b2bd4c Mon Sep 17 00:00:00 2001 From: Adel El-Beik Date: Mon, 17 Jul 2023 17:58:31 +0100 Subject: [PATCH 039/126] ENT-10122: Add consuming transaction id to vault states table. --- .../corda/core/node/services/VaultService.kt | 28 +++++++++++++++++-- .../node/services/vault/NodeVaultService.kt | 5 ++-- .../corda/node/services/vault/VaultSchema.kt | 6 +++- .../vault-schema.changelog-master.xml | 1 + 4 files changed, 34 insertions(+), 6 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt b/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt index 1913e6bf84..b73673c3a3 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt @@ -68,7 +68,7 @@ class Vault(val states: Iterable>) { * other transactions observed, then the changes are observed "net" of those. */ @CordaSerializable - data class Update @JvmOverloads constructor( + data class Update constructor( val consumed: Set>, val produced: Set>, val flowId: UUID? = null, @@ -78,8 +78,20 @@ class Vault(val states: Iterable>) { * differently. */ val type: UpdateType = UpdateType.GENERAL, - val references: Set> = emptySet() + val references: Set> = emptySet(), + val consumingTxIds: Map = emptyMap() ) { + @JvmOverloads constructor( consumed: Set>, + produced: Set>, + flowId: UUID? = null, + /** + * Specifies the type of update, currently supported types are general and, contract upgrade and notary change. + * Notary change transactions only modify the notary field on states, and potentially need to be handled + * differently. + */ + type: UpdateType = UpdateType.GENERAL, + references: Set> = emptySet()) : this(consumed, produced, flowId, type, references, consumingTxIds = emptyMap()) + /** Checks whether the update contains a state of the specified type. */ inline fun containsType() = consumed.any { it.state.data is T } || produced.any { it.state.data is T } || references.any { it.state.data is T } @@ -105,7 +117,7 @@ class Vault(val states: Iterable>) { val combinedConsumed = consumed + (rhs.consumed - produced) // The ordering below matters to preserve ordering of consumed/produced Sets when they are insertion order dependent implementations. val combinedProduced = produced.filter { it !in rhs.consumed }.toSet() + rhs.produced - return copy(consumed = combinedConsumed, produced = combinedProduced, references = references + rhs.references) + return copy(consumed = combinedConsumed, produced = combinedProduced, references = references + rhs.references, consumingTxIds = consumingTxIds + rhs.consumingTxIds) } override fun toString(): String { @@ -138,6 +150,16 @@ class Vault(val states: Iterable>) { return Update(consumed, produced, flowId, type, references) } + /** Additional copy method to maintain backwards compatibility. */ + fun copy( + consumed: Set>, + produced: Set>, + flowId: UUID? = null, + type: UpdateType = UpdateType.GENERAL, + references: Set> = emptySet() + ): Update { + return Update(consumed, produced, flowId, type, references, consumingTxIds) + } } @CordaSerializable diff --git a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt index ec4984ea68..cca094eb34 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt @@ -231,6 +231,7 @@ class NodeVaultService( if (stateStatus != Vault.StateStatus.CONSUMED) { stateStatus = Vault.StateStatus.CONSUMED consumedTime = clock.instant() + consumingTxId = update.consumingTxIds[stateRef]?.toString() ?: "" // remove lock (if held) if (lockId != null) { lockId = null @@ -370,8 +371,8 @@ class NodeVaultService( } } } - - return Vault.Update(consumedStates.toSet(), ourNewStates.toSet(), references = newReferenceStateAndRefs.toSet()) + val consumedTxIds = consumedStates.associate { Pair(it.ref, tx.id) } + return Vault.Update(consumedStates.toSet(), ourNewStates.toSet(), references = newReferenceStateAndRefs.toSet(), consumingTxIds = consumedTxIds) } fun resolveAndMakeUpdate(tx: CoreTransaction): Vault.Update? { diff --git a/node/src/main/kotlin/net/corda/node/services/vault/VaultSchema.kt b/node/src/main/kotlin/net/corda/node/services/vault/VaultSchema.kt index 09c71fe1f7..fc7fc9f9db 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/VaultSchema.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/VaultSchema.kt @@ -91,7 +91,11 @@ object VaultSchemaV1 : MappedSchema( /** associated constraint type data (if any) */ @Column(name = "constraint_data", length = MAX_CONSTRAINT_DATA_SIZE, nullable = true) @Type(type = "corda-wrapper-binary") - var constraintData: ByteArray? = null + var constraintData: ByteArray? = null, + + /** consuming transaction */ + @Column(name = "consuming_tx_id", length = 144, nullable = false) + var consumingTxId: String = "" ) : PersistentState() @Entity diff --git a/node/src/main/resources/migration/vault-schema.changelog-master.xml b/node/src/main/resources/migration/vault-schema.changelog-master.xml index 44684647fa..8fea107366 100644 --- a/node/src/main/resources/migration/vault-schema.changelog-master.xml +++ b/node/src/main/resources/migration/vault-schema.changelog-master.xml @@ -13,4 +13,5 @@ + From 60bb4c58f2946986caac3dc23494cf9382ce822a Mon Sep 17 00:00:00 2001 From: Adel El-Beik Date: Tue, 18 Jul 2023 17:45:53 +0100 Subject: [PATCH 040/126] ENT-10122: Made the consuming tx id field nullable, added missing changelog file. --- .../corda/node/services/vault/NodeVaultService.kt | 2 +- .../net/corda/node/services/vault/VaultSchema.kt | 4 ++-- .../migration/vault-schema.changelog-v14.xml | 15 +++++++++++++++ 3 files changed, 18 insertions(+), 3 deletions(-) create mode 100644 node/src/main/resources/migration/vault-schema.changelog-v14.xml diff --git a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt index cca094eb34..26b20544f6 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt @@ -231,7 +231,7 @@ class NodeVaultService( if (stateStatus != Vault.StateStatus.CONSUMED) { stateStatus = Vault.StateStatus.CONSUMED consumedTime = clock.instant() - consumingTxId = update.consumingTxIds[stateRef]?.toString() ?: "" + consumingTxId = update.consumingTxIds[stateRef]?.toString() // remove lock (if held) if (lockId != null) { lockId = null diff --git a/node/src/main/kotlin/net/corda/node/services/vault/VaultSchema.kt b/node/src/main/kotlin/net/corda/node/services/vault/VaultSchema.kt index fc7fc9f9db..ee59ee170f 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/VaultSchema.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/VaultSchema.kt @@ -94,8 +94,8 @@ object VaultSchemaV1 : MappedSchema( var constraintData: ByteArray? = null, /** consuming transaction */ - @Column(name = "consuming_tx_id", length = 144, nullable = false) - var consumingTxId: String = "" + @Column(name = "consuming_tx_id", length = 144, nullable = true) + var consumingTxId: String? = null ) : PersistentState() @Entity diff --git a/node/src/main/resources/migration/vault-schema.changelog-v14.xml b/node/src/main/resources/migration/vault-schema.changelog-v14.xml new file mode 100644 index 0000000000..75f576f214 --- /dev/null +++ b/node/src/main/resources/migration/vault-schema.changelog-v14.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + \ No newline at end of file From aa9e41c7c26b2573f6bd526ecaa6255d4f2b84f9 Mon Sep 17 00:00:00 2001 From: Adel El-Beik Date: Wed, 19 Jul 2023 16:36:39 +0100 Subject: [PATCH 041/126] ENT-10122: Updated tests to include consuming transaction id in the Vault.Update check. --- .../kotlin/net/corda/core/node/services/VaultService.kt | 6 +++++- .../net/corda/node/services/vault/NodeVaultServiceTest.kt | 7 ++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt b/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt index b73673c3a3..8af0eb2d31 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt @@ -119,7 +119,7 @@ class Vault(val states: Iterable>) { val combinedProduced = produced.filter { it !in rhs.consumed }.toSet() + rhs.produced return copy(consumed = combinedConsumed, produced = combinedProduced, references = references + rhs.references, consumingTxIds = consumingTxIds + rhs.consumingTxIds) } - + //val consumingTxIds: Map = emptyMap() override fun toString(): String { val sb = StringBuilder() sb.appendln("${consumed.size} consumed, ${produced.size} produced") @@ -137,6 +137,10 @@ class Vault(val states: Iterable>) { references.forEach { sb.appendln("${it.ref}: ${it.state}") } + sb.appendln("Consuming TxIds:") + consumingTxIds.forEach { + sb.appendln("${it.key}: ${it.value}") + } return sb.toString() } diff --git a/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt index 682af85bd8..3b01205ba7 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt @@ -664,14 +664,15 @@ class NodeVaultServiceTest { database.transaction { vaultService.notify(StatesToRecord.ONLY_RELEVANT, issueTx) } val expectedIssueUpdate = Vault.Update(emptySet(), setOf(cashState), null) - database.transaction { + val moveTx = database.transaction { val moveBuilder = TransactionBuilder(notary).apply { CashUtils.generateSpend(services, this, Amount(1000, GBP), identity, thirdPartyIdentity) } val moveTx = moveBuilder.toWireTransaction(services) vaultService.notify(StatesToRecord.ONLY_RELEVANT, moveTx) + moveTx } - val expectedMoveUpdate = Vault.Update(setOf(cashState), emptySet(), null) + val expectedMoveUpdate = Vault.Update(setOf(cashState), emptySet(), null, consumingTxIds = mapOf(cashState.ref to moveTx.id)) // ensure transaction contract state is persisted in DBStorage val signedMoveTx = services.signInitialTransaction(issueBuilder) @@ -740,7 +741,7 @@ class NodeVaultServiceTest { val expectedIssueUpdate = Vault.Update(emptySet(), setOf(initialCashState), null) val expectedNotaryChangeUpdate = Vault.Update(setOf(initialCashState), setOf(cashStateWithNewNotary), null, Vault.UpdateType.NOTARY_CHANGE) - val expectedMoveUpdate = Vault.Update(setOf(cashStateWithNewNotary), emptySet(), null) + val expectedMoveUpdate = Vault.Update(setOf(cashStateWithNewNotary), emptySet(), null, consumingTxIds = mapOf(cashStateWithNewNotary.ref to moveTx.id)) val observedUpdates = vaultSubscriber.onNextEvents assertEquals(observedUpdates, listOf(expectedIssueUpdate, expectedNotaryChangeUpdate, expectedMoveUpdate)) From 117d319317c0fe78db709ac05c3f441f3b4a1671 Mon Sep 17 00:00:00 2001 From: Adel El-Beik Date: Thu, 20 Jul 2023 09:31:46 +0100 Subject: [PATCH 042/126] ENT-10122: Removed commented code left in. --- .../main/kotlin/net/corda/core/node/services/VaultService.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt b/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt index 8af0eb2d31..bf8db51be2 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt @@ -119,7 +119,7 @@ class Vault(val states: Iterable>) { val combinedProduced = produced.filter { it !in rhs.consumed }.toSet() + rhs.produced return copy(consumed = combinedConsumed, produced = combinedProduced, references = references + rhs.references, consumingTxIds = consumingTxIds + rhs.consumingTxIds) } - //val consumingTxIds: Map = emptyMap() + override fun toString(): String { val sb = StringBuilder() sb.appendln("${consumed.size} consumed, ${produced.size} produced") From 48213b5f8c6326258e86d829e0a972f1f437a7e6 Mon Sep 17 00:00:00 2001 From: Rick Parker Date: Thu, 20 Jul 2023 09:51:35 +0100 Subject: [PATCH 043/126] ENT-10284 Performance optimise deserialisation (#7425) --- core-deterministic/build.gradle | 1 + .../net/corda/core/internal/LazyPool.kt | 41 ++++++++ .../net/corda/core/internal/LazyPool.kt | 16 ++- .../corda/finance/flows/CompatibilityTest.kt | 41 +++++++- .../internal/amqp/SerializationOutputTests.kt | 90 +++++++++++++---- .../internal/amqp/DeserializationInput.kt | 76 ++++++++++----- .../serialization/internal/amqp/Envelope.kt | 97 +++++++++++-------- .../serialization/internal/amqp/Schema.kt | 8 +- .../internal/amqp/SerializerFactory.kt | 12 ++- .../amqp/JavaSerializationOutputTests.java | 2 +- 10 files changed, 288 insertions(+), 96 deletions(-) create mode 100644 core-deterministic/src/main/kotlin/net/corda/core/internal/LazyPool.kt diff --git a/core-deterministic/build.gradle b/core-deterministic/build.gradle index e322d57401..2c597dbabc 100644 --- a/core-deterministic/build.gradle +++ b/core-deterministic/build.gradle @@ -84,6 +84,7 @@ def patchCore = tasks.register('patchCore', Zip) { exclude 'net/corda/core/internal/utilities/PrivateInterner*.class' exclude 'net/corda/core/crypto/internal/PublicKeyCache*.class' exclude 'net/corda/core/internal/ContractStateClassCache*.class' + exclude 'net/corda/core/internal/LazyPool*.class' } reproducibleFileOrder = true diff --git a/core-deterministic/src/main/kotlin/net/corda/core/internal/LazyPool.kt b/core-deterministic/src/main/kotlin/net/corda/core/internal/LazyPool.kt new file mode 100644 index 0000000000..4878cd373b --- /dev/null +++ b/core-deterministic/src/main/kotlin/net/corda/core/internal/LazyPool.kt @@ -0,0 +1,41 @@ +package net.corda.core.internal + +import net.corda.core.KeepForDJVM + +/** + * A lazy pool of resources [A], modified for DJVM. + * + * @param clear If specified this function will be run on each borrowed instance before handing it over. + * @param shouldReturnToPool If specified this function will be run on each release to determine whether the instance + * should be returned to the pool for reuse. This may be useful for pooled resources that dynamically grow during + * usage, and we may not want to retain them forever. + * @param bound If specified the pool will be bounded. Once all instances are borrowed subsequent borrows will block until an + * instance is released. + * @param newInstance The function to call to lazily newInstance a pooled resource. + */ +@Suppress("unused") +@KeepForDJVM +class LazyPool( + private val clear: ((A) -> Unit)? = null, + private val shouldReturnToPool: ((A) -> Boolean)? = null, + private val bound: Int? = null, + private val newInstance: () -> A +) { + fun borrow(): A { + return newInstance() + } + + @Suppress("unused_parameter") + fun release(instance: A) { + } + + /** + * Closes the pool. Note that all borrowed instances must have been released before calling this function, otherwise + * the returned iterable will be inaccurate. + */ + fun close(): Iterable { + return emptyList() + } + + fun reentrantRun(withInstance: (A) -> R): R = withInstance(borrow()) +} diff --git a/core/src/main/kotlin/net/corda/core/internal/LazyPool.kt b/core/src/main/kotlin/net/corda/core/internal/LazyPool.kt index 2c94dea8df..72140853e9 100644 --- a/core/src/main/kotlin/net/corda/core/internal/LazyPool.kt +++ b/core/src/main/kotlin/net/corda/core/internal/LazyPool.kt @@ -1,6 +1,5 @@ package net.corda.core.internal -import net.corda.core.DeleteForDJVM import java.util.concurrent.ConcurrentLinkedQueue import java.util.concurrent.Semaphore @@ -15,7 +14,6 @@ import java.util.concurrent.Semaphore * instance is released. * @param newInstance The function to call to lazily newInstance a pooled resource. */ -@DeleteForDJVM class LazyPool( private val clear: ((A) -> Unit)? = null, private val shouldReturnToPool: ((A) -> Boolean)? = null, @@ -76,4 +74,18 @@ class LazyPool( release(instance) } } + + private val currentBorrowed = ThreadLocal() + fun reentrantRun(withInstance: (A) -> R): R { + return currentBorrowed.get()?.let { + withInstance(it) + } ?: run { + currentBorrowed.set(it) + try { + withInstance(it) + } finally { + currentBorrowed.set(null) + } + } + } } \ No newline at end of file diff --git a/finance/workflows/src/test/kotlin/net/corda/finance/flows/CompatibilityTest.kt b/finance/workflows/src/test/kotlin/net/corda/finance/flows/CompatibilityTest.kt index 27a6f4324a..626089cace 100644 --- a/finance/workflows/src/test/kotlin/net/corda/finance/flows/CompatibilityTest.kt +++ b/finance/workflows/src/test/kotlin/net/corda/finance/flows/CompatibilityTest.kt @@ -57,17 +57,50 @@ class CompatibilityTest { assertTrue(inByteArray.contentEquals(serializedBytes.bytes)) } + @Test(timeout = 300_000) + fun performanceTest() { + val inputStream = javaClass.classLoader.getResourceAsStream("compatibilityData/v3/node_transaction.dat") + assertNotNull(inputStream) + + val inByteArray: ByteArray = inputStream.readBytes() + val deserializationInput = DeserializationInput(serializerFactory) + + val bytes = SerializedBytes(inByteArray) + val transaction = deserializationInput.deserialize(bytes, SignedTransaction::class.java, SerializationDefaults.STORAGE_CONTEXT) + assertNotNull(transaction) + + val counts = 1000 + val loops = 200 + for (loop in 0 until loops) { + val start = System.nanoTime() + for (count in 0 until counts) { + val stx = deserializationInput.deserialize(bytes, SignedTransaction::class.java, SerializationDefaults.STORAGE_CONTEXT) + for (input in stx.inputs) { + assertNotNull(input) + } + for (output in stx.tx.outputs) { + assertNotNull(output) + } + for (command in stx.tx.commands) { + assertNotNull(command) + } + } + val end = System.nanoTime() + println("Time per transaction deserialize on loop $loop = ${(end - start) / counts} nanoseconds") + } + } + private fun assertSchemasMatch(original: Schema, reserialized: Schema) { if (original.toString() == reserialized.toString()) return original.types.forEach { originalType -> - val reserializedType = reserialized.types.firstOrNull { it.name == originalType.name } ?: - fail("""Schema mismatch between original and re-serialized data. Could not find reserialized schema matching: + val reserializedType = reserialized.types.firstOrNull { it.name == originalType.name } + ?: fail("""Schema mismatch between original and re-serialized data. Could not find reserialized schema matching: $originalType """) - if (originalType.toString() != reserializedType.toString()) - fail("""Schema mismatch between original and re-serialized data. Expected: + if (originalType.toString() != reserializedType.toString()) + fail("""Schema mismatch between original and re-serialized data. Expected: $originalType diff --git a/serialization-tests/src/test/kotlin/net/corda/serialization/internal/amqp/SerializationOutputTests.kt b/serialization-tests/src/test/kotlin/net/corda/serialization/internal/amqp/SerializationOutputTests.kt index 9dd1a398e8..0022030f82 100644 --- a/serialization-tests/src/test/kotlin/net/corda/serialization/internal/amqp/SerializationOutputTests.kt +++ b/serialization-tests/src/test/kotlin/net/corda/serialization/internal/amqp/SerializationOutputTests.kt @@ -7,7 +7,13 @@ import com.nhaarman.mockito_kotlin.whenever import net.corda.client.rpc.RPCException import net.corda.core.CordaException import net.corda.core.CordaRuntimeException -import net.corda.core.contracts.* +import net.corda.core.contracts.Amount +import net.corda.core.contracts.Contract +import net.corda.core.contracts.ContractAttachment +import net.corda.core.contracts.ContractState +import net.corda.core.contracts.PrivacySalt +import net.corda.core.contracts.StateRef +import net.corda.core.contracts.TransactionState import net.corda.core.crypto.Crypto import net.corda.core.crypto.SecureHash import net.corda.core.crypto.secureRandomBytes @@ -15,24 +21,43 @@ import net.corda.core.flows.FlowException import net.corda.core.identity.AbstractParty import net.corda.core.identity.CordaX500Name import net.corda.core.internal.AbstractAttachment -import net.corda.core.serialization.* +import net.corda.core.serialization.ConstructorForDeserialization +import net.corda.core.serialization.CordaSerializable +import net.corda.core.serialization.EncodingWhitelist +import net.corda.core.serialization.MissingAttachmentsException +import net.corda.core.serialization.SerializationContext +import net.corda.core.serialization.SerializationFactory import net.corda.core.transactions.LedgerTransaction import net.corda.core.utilities.OpaqueBytes -import net.corda.nodeapi.internal.serialization.amqp.AMQPServerSerializationScheme +import net.corda.coretesting.internal.rigorousMock import net.corda.nodeapi.internal.crypto.ContentSignerBuilder +import net.corda.nodeapi.internal.serialization.amqp.AMQPServerSerializationScheme import net.corda.serialization.internal.* -import net.corda.serialization.internal.amqp.testutils.* +import net.corda.serialization.internal.amqp.testutils.deserialize +import net.corda.serialization.internal.amqp.testutils.serialize +import net.corda.serialization.internal.amqp.testutils.testDefaultFactory +import net.corda.serialization.internal.amqp.testutils.testDefaultFactoryNoEvolution +import net.corda.serialization.internal.amqp.testutils.testSerializationContext import net.corda.serialization.internal.carpenter.ClassCarpenterImpl import net.corda.testing.contracts.DummyContract import net.corda.testing.core.BOB_NAME import net.corda.testing.core.SerializationEnvironmentRule import net.corda.testing.core.TestIdentity -import net.corda.coretesting.internal.rigorousMock import org.apache.activemq.artemis.api.core.SimpleString -import org.apache.qpid.proton.amqp.* +import org.apache.qpid.proton.amqp.Decimal128 +import org.apache.qpid.proton.amqp.Decimal32 +import org.apache.qpid.proton.amqp.Decimal64 +import org.apache.qpid.proton.amqp.Symbol +import org.apache.qpid.proton.amqp.UnsignedByte +import org.apache.qpid.proton.amqp.UnsignedInteger +import org.apache.qpid.proton.amqp.UnsignedLong +import org.apache.qpid.proton.amqp.UnsignedShort import org.apache.qpid.proton.codec.DecoderImpl import org.apache.qpid.proton.codec.EncoderImpl -import org.assertj.core.api.Assertions.* +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatExceptionOfType +import org.assertj.core.api.Assertions.assertThatThrownBy +import org.assertj.core.api.Assertions.catchThrowable import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.cert.X509v2CRLBuilder import org.bouncycastle.cert.jcajce.JcaX509CRLConverter @@ -49,9 +74,36 @@ import java.io.NotSerializableException import java.math.BigDecimal import java.math.BigInteger import java.security.cert.X509CRL -import java.time.* +import java.time.DayOfWeek +import java.time.Duration +import java.time.Instant +import java.time.LocalDate +import java.time.LocalDateTime +import java.time.LocalTime +import java.time.Month +import java.time.MonthDay +import java.time.OffsetDateTime +import java.time.OffsetTime +import java.time.Period +import java.time.Year +import java.time.YearMonth +import java.time.ZonedDateTime import java.time.temporal.ChronoUnit -import java.util.* +import java.util.ArrayList +import java.util.Arrays +import java.util.BitSet +import java.util.Currency +import java.util.Date +import java.util.EnumMap +import java.util.EnumSet +import java.util.HashMap +import java.util.NavigableMap +import java.util.Objects +import java.util.Random +import java.util.SortedSet +import java.util.TreeMap +import java.util.TreeSet +import java.util.UUID import kotlin.reflect.full.superclasses import kotlin.test.assertEquals import kotlin.test.assertNotNull @@ -222,16 +274,16 @@ class SerializationOutputTests(private val compression: CordaSerializationEncodi val bytes = ser.serialize(obj, compression) val decoder = DecoderImpl().apply { - this.register(Envelope.DESCRIPTOR, Envelope) - this.register(Schema.DESCRIPTOR, Schema) - this.register(Descriptor.DESCRIPTOR, Descriptor) - this.register(Field.DESCRIPTOR, Field) - this.register(CompositeType.DESCRIPTOR, CompositeType) - this.register(Choice.DESCRIPTOR, Choice) - this.register(RestrictedType.DESCRIPTOR, RestrictedType) - this.register(ReferencedObject.DESCRIPTOR, ReferencedObject) - this.register(TransformsSchema.DESCRIPTOR, TransformsSchema) - this.register(TransformTypes.DESCRIPTOR, TransformTypes) + register(Envelope.DESCRIPTOR, Envelope.FastPathConstructor(this)) + register(Schema.DESCRIPTOR, Schema) + register(Descriptor.DESCRIPTOR, Descriptor) + register(Field.DESCRIPTOR, Field) + register(CompositeType.DESCRIPTOR, CompositeType) + register(Choice.DESCRIPTOR, Choice) + register(RestrictedType.DESCRIPTOR, RestrictedType) + register(ReferencedObject.DESCRIPTOR, ReferencedObject) + register(TransformsSchema.DESCRIPTOR, TransformsSchema) + register(TransformTypes.DESCRIPTOR, TransformTypes) } EncoderImpl(decoder) DeserializationInput.withDataBytes(bytes, encodingWhitelist) { diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/DeserializationInput.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/DeserializationInput.kt index 2591c094e8..396befd4ae 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/DeserializationInput.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/DeserializationInput.kt @@ -1,6 +1,7 @@ package net.corda.serialization.internal.amqp import net.corda.core.KeepForDJVM +import net.corda.core.internal.LazyPool import net.corda.core.internal.VisibleForTesting import net.corda.core.serialization.AMQP_ENVELOPE_CACHE_PROPERTY import net.corda.core.serialization.EncodingWhitelist @@ -18,7 +19,8 @@ import net.corda.serialization.internal.model.TypeIdentifier import org.apache.qpid.proton.amqp.Binary import org.apache.qpid.proton.amqp.DescribedType import org.apache.qpid.proton.amqp.UnsignedInteger -import org.apache.qpid.proton.codec.Data +import org.apache.qpid.proton.codec.DecoderImpl +import org.apache.qpid.proton.codec.EncoderImpl import java.io.InputStream import java.io.NotSerializableException import java.lang.reflect.ParameterizedType @@ -72,17 +74,32 @@ class DeserializationInput constructor( } } + private val decoderPool = LazyPool { + val decoder = DecoderImpl().apply { + register(Envelope.DESCRIPTOR, Envelope.FastPathConstructor(this)) + register(Schema.DESCRIPTOR, Schema) + register(Descriptor.DESCRIPTOR, Descriptor) + register(Field.DESCRIPTOR, Field) + register(CompositeType.DESCRIPTOR, CompositeType) + register(Choice.DESCRIPTOR, Choice) + register(RestrictedType.DESCRIPTOR, RestrictedType) + register(ReferencedObject.DESCRIPTOR, ReferencedObject) + register(TransformsSchema.DESCRIPTOR, TransformsSchema) + register(TransformTypes.DESCRIPTOR, TransformTypes) + } + EncoderImpl(decoder) + decoder + } + @Throws(AMQPNoTypeNotSerializableException::class) - fun getEnvelope(byteSequence: ByteSequence, encodingWhitelist: EncodingWhitelist = NullEncodingWhitelist): Envelope { + fun getEnvelope(byteSequence: ByteSequence, encodingWhitelist: EncodingWhitelist = NullEncodingWhitelist, lazy: Boolean = false): Envelope { return withDataBytes(byteSequence, encodingWhitelist) { dataBytes -> - val data = Data.Factory.create() - val expectedSize = dataBytes.remaining() - if (data.decode(dataBytes) != expectedSize.toLong()) { - throw AMQPNoTypeNotSerializableException( - "Unexpected size of data", - "Blob is corrupted!.") + decoderPool.reentrantRun { + it.byteBuffer = dataBytes + (it.readObject() as Envelope).apply { + if (!lazy) this.resolvedSchema + } } - Envelope.get(data) } } } @@ -124,22 +141,29 @@ class DeserializationInput constructor( fun deserialize(bytes: ByteSequence, clazz: Class, context: SerializationContext): T = des { /** - * The cache uses object identity rather than [ByteSequence.equals] and - * [ByteSequence.hashCode]. This is for speed: each [ByteSequence] object - * can potentially be large, and we are optimizing for the case when we - * know we will be deserializing the exact same objects multiple times. - * This also means that the cache MUST be short-lived, as otherwise it - * becomes a memory leak. + * So that the [DecoderImpl] is held whilst we get the [Envelope] and [doReadObject], + * since we are using lazy when getting the [Envelope] which means [Schema] and + * [TransformsSchema] are only parsed out of the [bytes] if demanded by [doReadObject]. */ - @Suppress("unchecked_cast") - val envelope = (context.properties[AMQP_ENVELOPE_CACHE_PROPERTY] as? MutableMap) - ?.computeIfAbsent(IdentityKey(bytes)) { key -> - getEnvelope(key.bytes, context.encodingWhitelist) - } ?: getEnvelope(bytes, context.encodingWhitelist) + decoderPool.reentrantRun { + /** + * The cache uses object identity rather than [ByteSequence.equals] and + * [ByteSequence.hashCode]. This is for speed: each [ByteSequence] object + * can potentially be large, and we are optimizing for the case when we + * know we will be deserializing the exact same objects multiple times. + * This also means that the cache MUST be short-lived, as otherwise it + * becomes a memory leak. + */ + @Suppress("unchecked_cast") + val envelope = (context.properties[AMQP_ENVELOPE_CACHE_PROPERTY] as? MutableMap) + ?.computeIfAbsent(IdentityKey(bytes)) { key -> + getEnvelope(key.bytes, context.encodingWhitelist, true) + } ?: getEnvelope(bytes, context.encodingWhitelist, true) - logger.trace { "deserialize blob scheme=\"${envelope.schema}\"" } + logger.trace { "deserialize blob scheme=\"${envelope.schema}\"" } - doReadObject(envelope, clazz, context) + doReadObject(envelope, clazz, context) + } } @Throws(NotSerializableException::class) @@ -155,10 +179,10 @@ class DeserializationInput constructor( private fun doReadObject(envelope: Envelope, clazz: Class, context: SerializationContext): T { return clazz.cast(readObjectOrNull( - obj = redescribe(envelope.obj, clazz), - schema = SerializationSchemas(envelope.schema, envelope.transformsSchema), - type = clazz, - context = context + obj = redescribe(envelope.obj, clazz), + schema = SerializationSchemas(envelope::resolvedSchema), + type = clazz, + context = context )) } diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/Envelope.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/Envelope.kt index 70c4ac55bd..272c719a91 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/Envelope.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/Envelope.kt @@ -1,70 +1,85 @@ package net.corda.serialization.internal.amqp import net.corda.core.KeepForDJVM +import org.apache.qpid.proton.ProtonException import org.apache.qpid.proton.amqp.DescribedType import org.apache.qpid.proton.codec.Data -import org.apache.qpid.proton.codec.DescribedTypeConstructor +import org.apache.qpid.proton.codec.DecoderImpl +import org.apache.qpid.proton.codec.EncodingCodes +import org.apache.qpid.proton.codec.FastPathDescribedTypeConstructor +import java.nio.Buffer +import java.nio.ByteBuffer /** * This class wraps all serialized data, so that the schema can be carried along with it. We will provide various * internal utilities to decompose and recompose with/without schema etc so that e.g. we can store objects with a * (relationally) normalised out schema to avoid excessive duplication. */ -// TODO: make the schema parsing lazy since mostly schemas will have been seen before and we only need it if we -// TODO: don't recognise a type descriptor. @KeepForDJVM -data class Envelope(val obj: Any?, val schema: Schema, val transformsSchema: TransformsSchema) : DescribedType { - companion object : DescribedTypeConstructor { +class Envelope(val obj: Any?, resolveSchema: () -> Pair) : DescribedType { + + val resolvedSchema: Pair by lazy(resolveSchema) + + val schema: Schema get() = resolvedSchema.first + val transformsSchema: TransformsSchema get() = resolvedSchema.second + + companion object { val DESCRIPTOR = AMQPDescriptorRegistry.ENVELOPE.amqpDescriptor val DESCRIPTOR_OBJECT = Descriptor(null, DESCRIPTOR) // described list should either be two or three elements long private const val ENVELOPE_WITHOUT_TRANSFORMS = 2 private const val ENVELOPE_WITH_TRANSFORMS = 3 + } - private const val BLOB_IDX = 0 - private const val SCHEMA_IDX = 1 - private const val TRANSFORMS_SCHEMA_IDX = 2 + class FastPathConstructor(private val decoder: DecoderImpl) : FastPathDescribedTypeConstructor { - fun get(data: Data): Envelope { - val describedType = data.`object` as DescribedType - if (describedType.descriptor != DESCRIPTOR) { - throw AMQPNoTypeNotSerializableException( - "Unexpected descriptor ${describedType.descriptor}, should be $DESCRIPTOR.") + private val _buffer: ByteBuffer get() = decoder.byteBuffer + + @Suppress("ComplexMethod", "MagicNumber") + private fun readEncodingAndReturnSize(buffer: ByteBuffer, inBytes: Boolean = true): Int { + val encodingCode: Byte = buffer.get() + return when (encodingCode) { + EncodingCodes.LIST8 -> { + (buffer.get().toInt() and 0xff).let { if (inBytes) it else (buffer.get().toInt() and 0xff) } + } + EncodingCodes.LIST32 -> { + buffer.int.let { if (inBytes) it else buffer.int } + } + else -> throw ProtonException("Expected List type but found encoding: $encodingCode") } - val list = describedType.described as List<*> - - // We need to cope with objects serialised without the transforms header element in the - // envelope - val transformSchema: Any? = when (list.size) { - ENVELOPE_WITHOUT_TRANSFORMS -> null - ENVELOPE_WITH_TRANSFORMS -> list[TRANSFORMS_SCHEMA_IDX] - else -> throw AMQPNoTypeNotSerializableException( - "Malformed list, bad length of ${list.size} (should be 2 or 3)") - } - - return newInstance(listOf(list[BLOB_IDX], Schema.get(list[SCHEMA_IDX]!!), - TransformsSchema.newInstance(transformSchema))) } - // This separation of functions is needed as this will be the entry point for the default - // AMQP decoder if one is used (see the unit tests). - override fun newInstance(described: Any?): Envelope { - val list = described as? List<*> ?: throw IllegalStateException("Was expecting a list") - - // We need to cope with objects serialised without the transforms header element in the - // envelope - val transformSchema = when (list.size) { - ENVELOPE_WITHOUT_TRANSFORMS -> TransformsSchema.newInstance(null) - ENVELOPE_WITH_TRANSFORMS -> list[TRANSFORMS_SCHEMA_IDX] as TransformsSchema - else -> throw AMQPNoTypeNotSerializableException( - "Malformed list, bad length of ${list.size} (should be 2 or 3)") + override fun readValue(): Envelope? { + val buffer = _buffer + val size = readEncodingAndReturnSize(buffer, false) + if (size != ENVELOPE_WITHOUT_TRANSFORMS && size != ENVELOPE_WITH_TRANSFORMS) { + throw AMQPNoTypeNotSerializableException("Malformed list, bad length of $size (should be 2 or 3)") } - - return Envelope(list[BLOB_IDX], list[SCHEMA_IDX] as Schema, transformSchema) + val data = Data.Factory.create() + data.decode(buffer) + val obj = data.`object` + val lambda: () -> Pair = { + data.decode(buffer) + val schema = data.`object` + val transformsSchema = if (size > 2) { + data.decode(buffer) + data.`object` + } else null + Schema.get(schema) to TransformsSchema.newInstance(transformsSchema) + } + return Envelope(obj, lambda) } - override fun getTypeClass(): Class<*> = Envelope::class.java + override fun skipValue() { + val buffer = _buffer + val size = readEncodingAndReturnSize(buffer) + (buffer as Buffer).position(buffer.position() + size) + } + + override fun encodesJavaPrimitive(): Boolean = false + + override fun getTypeClass(): Class = Envelope::class.java } override fun getDescriptor(): Any = DESCRIPTOR diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/Schema.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/Schema.kt index 36ac18bfe6..13e0f1bc1a 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/Schema.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/Schema.kt @@ -5,9 +5,13 @@ import net.corda.core.internal.uncheckedCast import net.corda.serialization.internal.CordaSerializationMagic import net.corda.serialization.internal.amqp.AMQPTypeIdentifiers.isPrimitive import net.corda.serialization.internal.model.TypeIdentifier -import net.corda.serialization.internal.model.TypeIdentifier.TopType import net.corda.serialization.internal.model.TypeIdentifier.Companion.forGenericType -import org.apache.qpid.proton.amqp.* +import net.corda.serialization.internal.model.TypeIdentifier.TopType +import org.apache.qpid.proton.amqp.Binary +import org.apache.qpid.proton.amqp.DescribedType +import org.apache.qpid.proton.amqp.Symbol +import org.apache.qpid.proton.amqp.UnsignedInteger +import org.apache.qpid.proton.amqp.UnsignedLong import org.apache.qpid.proton.codec.DescribedTypeConstructor import java.io.NotSerializableException import java.lang.reflect.Type diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializerFactory.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializerFactory.kt index 4bfab6b128..0eaeb7c6e2 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializerFactory.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializerFactory.kt @@ -5,7 +5,17 @@ import java.io.NotSerializableException import javax.annotation.concurrent.ThreadSafe @KeepForDJVM -data class SerializationSchemas(val schema: Schema, val transforms: TransformsSchema) +class SerializationSchemas(resolveSchema: () -> Pair) { + constructor(schema: Schema, transforms: TransformsSchema) : this({ schema to transforms }) + + private val resolvedSchema: Pair by lazy(resolveSchema) + + val schema: Schema get() = resolvedSchema.first + val transforms: TransformsSchema get() = resolvedSchema.second + + operator fun component1(): Schema = schema + operator fun component2(): TransformsSchema = transforms +} /** * Factory of serializers designed to be shared across threads and invocations. diff --git a/serialization/src/test/java/net/corda/serialization/internal/amqp/JavaSerializationOutputTests.java b/serialization/src/test/java/net/corda/serialization/internal/amqp/JavaSerializationOutputTests.java index 37c3afa53f..0194361360 100644 --- a/serialization/src/test/java/net/corda/serialization/internal/amqp/JavaSerializationOutputTests.java +++ b/serialization/src/test/java/net/corda/serialization/internal/amqp/JavaSerializationOutputTests.java @@ -183,7 +183,7 @@ public class JavaSerializationOutputTests { DecoderImpl decoder = new DecoderImpl(); - decoder.register(Envelope.Companion.getDESCRIPTOR(), Envelope.Companion); + decoder.register(Envelope.Companion.getDESCRIPTOR(), new Envelope.FastPathConstructor(decoder)); decoder.register(Schema.Companion.getDESCRIPTOR(), Schema.Companion); decoder.register(Descriptor.Companion.getDESCRIPTOR(), Descriptor.Companion); decoder.register(Field.Companion.getDESCRIPTOR(), Field.Companion); From 7d1d2297e72ecf8ea16fb2c5bd5e2edbe9dfd48b Mon Sep 17 00:00:00 2001 From: Jose Coll Date: Thu, 20 Jul 2023 09:55:44 +0100 Subject: [PATCH 044/126] ENT-10289 Ensure Sender and Receiver Distribution records share the same timestamp (#7437) --- .../DBTransactionStorageLedgerRecovery.kt | 17 +++++++++++------ .../DBTransactionStorageLedgerRecoveryTests.kt | 4 ++-- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecovery.kt b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecovery.kt index ae1c066ab2..0d00344742 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecovery.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecovery.kt @@ -141,8 +141,9 @@ class DBTransactionStorageLedgerRecovery(private val database: CordaPersistence, override fun addSenderTransactionRecoveryMetadata(id: SecureHash, metadata: TransactionMetadata): ByteArray { return database.transaction { - metadata.distributionList.peersToStatesToRecord.map { (peer, _) -> - val senderDistributionRecord = DBSenderDistributionRecord(PersistentKey(Key(clock.instant())), + val senderRecordingTimestamp = clock.instant() + metadata.distributionList.peersToStatesToRecord.forEach { (peer, _) -> + val senderDistributionRecord = DBSenderDistributionRecord(PersistentKey(Key(senderRecordingTimestamp)), id.toString(), partyInfoCache.getPartyIdByCordaX500Name(peer), metadata.distributionList.senderStatesToRecord) @@ -150,15 +151,16 @@ class DBTransactionStorageLedgerRecovery(private val database: CordaPersistence, } val hashedPeersToStatesToRecord = metadata.distributionList.peersToStatesToRecord.map { (peer, statesToRecord) -> partyInfoCache.getPartyIdByCordaX500Name(peer) to statesToRecord }.toMap() - val hashedDistributionList = HashedDistributionList(metadata.distributionList.senderStatesToRecord, hashedPeersToStatesToRecord) + val hashedDistributionList = HashedDistributionList(metadata.distributionList.senderStatesToRecord, hashedPeersToStatesToRecord, senderRecordingTimestamp) cryptoService.encrypt(hashedDistributionList.serialize()) } } override fun addReceiverTransactionRecoveryMetadata(id: SecureHash, sender: CordaX500Name, receiver: CordaX500Name, receiverStatesToRecord: StatesToRecord, encryptedDistributionList: ByteArray) { + val senderRecordedTimestamp = HashedDistributionList.deserialize(cryptoService.decrypt(encryptedDistributionList)).senderRecordedTimestamp database.transaction { val receiverDistributionRecord = - DBReceiverDistributionRecord(Key(clock.instant()), + DBReceiverDistributionRecord(Key(senderRecordedTimestamp), id, partyInfoCache.getPartyIdByCordaX500Name(sender), encryptedDistributionList, @@ -320,7 +322,8 @@ enum class DistributionRecordType { @CordaSerializable data class HashedDistributionList( val senderStatesToRecord: StatesToRecord, - val peerHashToStatesToRecord: Map + val peerHashToStatesToRecord: Map, + val senderRecordedTimestamp: Instant ) { fun serialize(): ByteArray { val baos = ByteArrayOutputStream() @@ -333,6 +336,7 @@ data class HashedDistributionList( out.writeLong(entry.key) out.writeByte(entry.value.ordinal) } + out.writeLong(senderRecordedTimestamp.toEpochMilli()) out.flush() return baos.toByteArray() } @@ -349,7 +353,8 @@ data class HashedDistributionList( repeat (numPeerHashToStatesToRecords) { peerHashToStatesToRecord[input.readLong()] = StatesToRecord.values()[input.readByte().toInt()] } - return HashedDistributionList(senderStatesToRecord, peerHashToStatesToRecord) + val senderRecordedTimestamp = Instant.ofEpochMilli(input.readLong()) + return HashedDistributionList(senderStatesToRecord, peerHashToStatesToRecord, senderRecordedTimestamp) } } } diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecoveryTests.kt b/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecoveryTests.kt index c4ef9b2f48..5f52c0849f 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecoveryTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecoveryTests.kt @@ -279,7 +279,7 @@ class DBTransactionStorageLedgerRecoveryTests { @Test(timeout = 300_000) fun `test lightweight serialization and deserialization of hashed distribution list payload`() { val dl = HashedDistributionList(ALL_VISIBLE, - mapOf(BOB.name.hashCode().toLong() to NONE, CHARLIE_NAME.hashCode().toLong() to ONLY_RELEVANT)) + mapOf(BOB.name.hashCode().toLong() to NONE, CHARLIE_NAME.hashCode().toLong() to ONLY_RELEVANT), now()) assertEquals(dl, dl.serialize().let { HashedDistributionList.deserialize(it) }) } @@ -373,7 +373,7 @@ class DBTransactionStorageLedgerRecoveryTests { private fun DistributionList.toWire(cryptoService: CryptoService = MockCryptoService(emptyMap())): ByteArray { val hashedPeersToStatesToRecord = this.peersToStatesToRecord.map { (peer, statesToRecord) -> partyInfoCache.getPartyIdByCordaX500Name(peer) to statesToRecord }.toMap() - val hashedDistributionList = HashedDistributionList(this.senderStatesToRecord, hashedPeersToStatesToRecord) + val hashedDistributionList = HashedDistributionList(this.senderStatesToRecord, hashedPeersToStatesToRecord, now()) return cryptoService.encrypt(hashedDistributionList.serialize()) } } From 6ec8855c6e36f81cf8cfc8ef011e5ca23d95d757 Mon Sep 17 00:00:00 2001 From: Rick Parker Date: Fri, 21 Jul 2023 08:56:22 +0100 Subject: [PATCH 045/126] Add system property to disable public key caching (#7438) --- .../net/corda/core/crypto/internal/PublicKeyCache.kt | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/crypto/internal/PublicKeyCache.kt b/core/src/main/kotlin/net/corda/core/crypto/internal/PublicKeyCache.kt index bbd322531d..2c648d812a 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/internal/PublicKeyCache.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/internal/PublicKeyCache.kt @@ -7,6 +7,8 @@ import java.security.PublicKey import java.util.concurrent.ConcurrentHashMap object PublicKeyCache { + private val DISABLE = java.lang.Boolean.getBoolean("net.corda.core.pubkeycache.disable") + private val collectedWeakPubKeys = ReferenceQueue() private class WeakPubKey(key: PublicKey, val bytes: ByteSequence? = null) : WeakReference(key, collectedWeakPubKeys) { @@ -14,8 +16,8 @@ object PublicKeyCache { override fun hashCode(): Int = hashCode override fun equals(other: Any?): Boolean { - if(this === other) return true - if(other !is WeakPubKey) return false + if (this === other) return true + if (other !is WeakPubKey) return false if(this.hashCode != other.hashCode) return false val thisGet = this.get() val otherGet = other.get() @@ -36,15 +38,18 @@ object PublicKeyCache { } fun bytesForCachedPublicKey(key: PublicKey): ByteSequence? { + if (DISABLE) return null val weakPubKey = WeakPubKey(key) return pubKeyToBytes[weakPubKey] } fun publicKeyForCachedBytes(bytes: ByteSequence): PublicKey? { + if (DISABLE) return null return bytesToPubKey[bytes]?.get() } fun cachePublicKey(key: PublicKey): PublicKey { + if (DISABLE) return key reapCollectedWeakPubKeys() val weakPubKey = WeakPubKey(key, ByteSequence.of(key.encoded)) pubKeyToBytes.putIfAbsent(weakPubKey, weakPubKey.bytes!!) From de67ab7377451e16b40136a5aa76f17454af94f7 Mon Sep 17 00:00:00 2001 From: Shams Asari Date: Tue, 25 Jul 2023 11:58:32 +0100 Subject: [PATCH 046/126] ENT-9876: Encrypting the ledger recovery participant distribution list --- .../coretests/flows/FinalityFlowTests.kt | 37 +++-- .../core/internal/ServiceHubCoreInternal.kt | 6 +- .../nodeapi/internal/crypto/AesEncryption.kt | 65 ++++++++ .../internal/crypto/AesEncryptionTest.kt | 73 +++++++++ .../net/corda/node/internal/AbstractNode.kt | 5 +- .../corda/node/services/EncryptionService.kt | 42 +++++ .../node/services/api/ServiceHubInternal.kt | 12 +- .../persistence/AesDbEncryptionService.kt | 152 ++++++++++++++++++ .../persistence/DBTransactionStorage.kt | 8 +- .../DBTransactionStorageLedgerRecovery.kt | 142 ++++++---------- .../persistence/HashedDistributionList.kt | 104 ++++++++++++ .../node/services/schema/NodeSchemaService.kt | 6 +- .../migration/node-core.changelog-master.xml | 1 + .../migration/node-core.changelog-v26.xml | 28 ++++ .../node/messaging/TwoPartyTradeFlowTests.kt | 12 +- .../persistence/AesDbEncryptionServiceTest.kt | 134 +++++++++++++++ ...DBTransactionStorageLedgerRecoveryTests.kt | 54 ++++--- .../node/internal/MockEncryptionService.kt | 39 +++++ .../node/internal/MockTransactionStorage.kt | 8 +- 19 files changed, 785 insertions(+), 143 deletions(-) create mode 100644 node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/AesEncryption.kt create mode 100644 node-api/src/test/kotlin/net/corda/nodeapi/internal/crypto/AesEncryptionTest.kt create mode 100644 node/src/main/kotlin/net/corda/node/services/EncryptionService.kt create mode 100644 node/src/main/kotlin/net/corda/node/services/persistence/AesDbEncryptionService.kt create mode 100644 node/src/main/kotlin/net/corda/node/services/persistence/HashedDistributionList.kt create mode 100644 node/src/main/resources/migration/node-core.changelog-v26.xml create mode 100644 node/src/test/kotlin/net/corda/node/services/persistence/AesDbEncryptionServiceTest.kt create mode 100644 testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/MockEncryptionService.kt diff --git a/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt b/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt index c120a1b620..1fa3b1516d 100644 --- a/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt +++ b/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt @@ -66,7 +66,6 @@ import net.corda.testing.node.internal.FINANCE_WORKFLOWS_CORDAPP import net.corda.testing.node.internal.InternalMockNetwork import net.corda.testing.node.internal.InternalMockNodeParameters import net.corda.testing.node.internal.MOCK_VERSION_INFO -import net.corda.testing.node.internal.MockCryptoService import net.corda.testing.node.internal.TestCordappInternal import net.corda.testing.node.internal.TestStartedNode import net.corda.testing.node.internal.cordappWithPackages @@ -75,6 +74,7 @@ import net.corda.testing.node.internal.findCordapp import org.assertj.core.api.Assertions.assertThat import org.junit.After import org.junit.Test +import org.junit.jupiter.api.assertThrows import java.sql.SQLException import java.util.Random import kotlin.test.assertEquals @@ -239,9 +239,9 @@ class FinalityFlowTests : WithFinality { private fun assertTxnRemovedFromDatabase(node: TestStartedNode, stxId: SecureHash) { val fromDb = node.database.transaction { session.createQuery( - "from ${DBTransactionStorage.DBTransaction::class.java.name} where tx_id = :transactionId", + "from ${DBTransactionStorage.DBTransaction::class.java.name} where txId = :transactionId", DBTransactionStorage.DBTransaction::class.java - ).setParameter("transactionId", stxId.toString()).resultList.map { it } + ).setParameter("transactionId", stxId.toString()).resultList } assertEquals(0, fromDb.size) } @@ -357,7 +357,7 @@ class FinalityFlowTests : WithFinality { assertEquals(StatesToRecord.ONLY_RELEVANT, this[0].statesToRecord) assertEquals(BOB_NAME.hashCode().toLong(), this[0].peerPartyId) } - getReceiverRecoveryData(stx.id, bobNode.database).apply { + getReceiverRecoveryData(stx.id, bobNode, aliceNode).apply { assertEquals(StatesToRecord.ALL_VISIBLE, this?.statesToRecord) assertEquals(StatesToRecord.ONLY_RELEVANT, this?.senderStatesToRecord) assertEquals(aliceNode.info.singleIdentity().name.hashCode().toLong(), this?.initiatorPartyId) @@ -390,7 +390,7 @@ class FinalityFlowTests : WithFinality { assertEquals(StatesToRecord.ONLY_RELEVANT, this[1].statesToRecord) assertEquals(CHARLIE_NAME.hashCode().toLong(), this[1].peerPartyId) } - getReceiverRecoveryData(stx.id, bobNode.database).apply { + getReceiverRecoveryData(stx.id, bobNode, aliceNode).apply { assertEquals(StatesToRecord.ONLY_RELEVANT, this?.statesToRecord) assertEquals(StatesToRecord.ONLY_RELEVANT, this?.senderStatesToRecord) assertEquals(aliceNode.info.singleIdentity().name.hashCode().toLong(), this?.initiatorPartyId) @@ -411,8 +411,8 @@ class FinalityFlowTests : WithFinality { assertThat(charlieNode.services.validatedTransactions.getTransaction(stx3.id)).isNotNull assertEquals(2, getSenderRecoveryData(stx3.id, aliceNode.database).size) - assertThat(getReceiverRecoveryData(stx3.id, bobNode.database)).isNotNull - assertThat(getReceiverRecoveryData(stx3.id, charlieNode.database)).isNotNull + assertThat(getReceiverRecoveryData(stx3.id, bobNode, aliceNode)).isNotNull + assertThat(getReceiverRecoveryData(stx3.id, charlieNode, aliceNode)).isNotNull } @Test(timeout=300_000) @@ -433,7 +433,7 @@ class FinalityFlowTests : WithFinality { assertEquals(StatesToRecord.ONLY_RELEVANT, this[0].statesToRecord) assertEquals(BOB_NAME.hashCode().toLong(), this[0].peerPartyId) } - getReceiverRecoveryData(stx.id, bobNode.database).apply { + getReceiverRecoveryData(stx.id, bobNode, aliceNode).apply { assertEquals(StatesToRecord.ONLY_RELEVANT, this?.statesToRecord) assertEquals(StatesToRecord.ONLY_RELEVANT, this?.senderStatesToRecord) assertEquals(aliceNode.info.singleIdentity().name.hashCode().toLong(), this?.initiatorPartyId) @@ -444,21 +444,28 @@ class FinalityFlowTests : WithFinality { private fun getSenderRecoveryData(id: SecureHash, database: CordaPersistence): List { val fromDb = database.transaction { session.createQuery( - "from ${DBTransactionStorageLedgerRecovery.DBSenderDistributionRecord::class.java.name} where tx_id = :transactionId", + "from ${DBTransactionStorageLedgerRecovery.DBSenderDistributionRecord::class.java.name} where txId = :transactionId", DBTransactionStorageLedgerRecovery.DBSenderDistributionRecord::class.java - ).setParameter("transactionId", id.toString()).resultList.map { it } + ).setParameter("transactionId", id.toString()).resultList } return fromDb.map { it.toSenderDistributionRecord() }.also { println("SenderDistributionRecord\n$it") } } - private fun getReceiverRecoveryData(id: SecureHash, database: CordaPersistence): ReceiverDistributionRecord? { - val fromDb = database.transaction { + private fun getReceiverRecoveryData(txId: SecureHash, receiver: TestStartedNode, sender: TestStartedNode): ReceiverDistributionRecord? { + val fromDb = receiver.database.transaction { session.createQuery( - "from ${DBTransactionStorageLedgerRecovery.DBReceiverDistributionRecord::class.java.name} where tx_id = :transactionId", + "from ${DBTransactionStorageLedgerRecovery.DBReceiverDistributionRecord::class.java.name} where txId = :transactionId", DBTransactionStorageLedgerRecovery.DBReceiverDistributionRecord::class.java - ).setParameter("transactionId", id.toString()).resultList.map { it } + ).setParameter("transactionId", txId.toString()).resultList + }.singleOrNull() + + // The receiver should not be able to decrypt the distribution list + assertThrows { + fromDb?.toReceiverDistributionRecord(receiver.internals.encryptionService) } - return fromDb.singleOrNull()?.toReceiverDistributionRecord(MockCryptoService(emptyMap())).also { println("ReceiverDistributionRecord\n$it") } + + // Only the sender can + return fromDb?.toReceiverDistributionRecord(sender.internals.encryptionService) } @StartableByRPC diff --git a/core/src/main/kotlin/net/corda/core/internal/ServiceHubCoreInternal.kt b/core/src/main/kotlin/net/corda/core/internal/ServiceHubCoreInternal.kt index f5febbad97..a880ecb152 100644 --- a/core/src/main/kotlin/net/corda/core/internal/ServiceHubCoreInternal.kt +++ b/core/src/main/kotlin/net/corda/core/internal/ServiceHubCoreInternal.kt @@ -82,7 +82,11 @@ interface ServiceHubCoreInternal : ServiceHub { * @param receiverStatesToRecord The StatesToRecord value of the receiver. * @param encryptedDistributionList encrypted distribution list (hashed peers -> StatesToRecord values) */ - fun recordReceiverTransactionRecoveryMetadata(txnId: SecureHash, sender: CordaX500Name, receiver: CordaX500Name, receiverStatesToRecord: StatesToRecord, encryptedDistributionList: ByteArray) + fun recordReceiverTransactionRecoveryMetadata(txnId: SecureHash, + sender: CordaX500Name, + receiver: CordaX500Name, + receiverStatesToRecord: StatesToRecord, + encryptedDistributionList: ByteArray) } interface TransactionsResolver { diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/AesEncryption.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/AesEncryption.kt new file mode 100644 index 0000000000..f9b36ffd07 --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/AesEncryption.kt @@ -0,0 +1,65 @@ +package net.corda.nodeapi.internal.crypto + +import net.corda.core.crypto.secureRandomBytes +import java.nio.ByteBuffer +import javax.crypto.Cipher +import javax.crypto.SecretKey +import javax.crypto.spec.GCMParameterSpec +import javax.crypto.spec.SecretKeySpec + +object AesEncryption { + const val KEY_SIZE_BYTES = 16 + internal const val IV_SIZE_BYTES = 12 + private const val TAG_SIZE_BYTES = 16 + private const val TAG_SIZE_BITS = TAG_SIZE_BYTES * 8 + + /** + * Generates a random 128-bit AES key. + */ + fun randomKey(): SecretKey { + return SecretKeySpec(secureRandomBytes(KEY_SIZE_BYTES), "AES") + } + + /** + * Encrypt the given [plaintext] with AES using the given [aesKey]. + * + * An optional public [additionalData] bytes can also be provided which will be authenticated alongside the ciphertext but not encrypted. + * This may be metadata for example. The same authenticated data bytes must be provided to [decrypt] to be able to decrypt the + * ciphertext. Typically these bytes are serialised alongside the ciphertext. Since it's authenticated in the ciphertext, it cannot be + * modified undetected. + */ + fun encrypt(aesKey: SecretKey, plaintext: ByteArray, additionalData: ByteArray? = null): ByteArray { + val cipher = Cipher.getInstance("AES/GCM/NoPadding") + val iv = secureRandomBytes(IV_SIZE_BYTES) // Never use the same IV with the same key! + cipher.init(Cipher.ENCRYPT_MODE, aesKey, GCMParameterSpec(TAG_SIZE_BITS, iv)) + val buffer = ByteBuffer.allocate(IV_SIZE_BYTES + plaintext.size + TAG_SIZE_BYTES) + buffer.put(iv) + if (additionalData != null) { + cipher.updateAAD(additionalData) + } + cipher.doFinal(ByteBuffer.wrap(plaintext), buffer) + return buffer.array() + } + + fun encrypt(aesKey: ByteArray, plaintext: ByteArray, additionalData: ByteArray? = null): ByteArray { + return encrypt(SecretKeySpec(aesKey, "AES"), plaintext, additionalData) + } + + /** + * Decrypt ciphertext that was encrypted with the same key using [encrypt]. + * + * If additional data was used for the encryption then it must also be provided. If doesn't match then the decryption will fail. + */ + fun decrypt(aesKey: SecretKey, ciphertext: ByteArray, additionalData: ByteArray? = null): ByteArray { + val cipher = Cipher.getInstance("AES/GCM/NoPadding") + cipher.init(Cipher.DECRYPT_MODE, aesKey, GCMParameterSpec(TAG_SIZE_BITS, ciphertext, 0, IV_SIZE_BYTES)) + if (additionalData != null) { + cipher.updateAAD(additionalData) + } + return cipher.doFinal(ciphertext, IV_SIZE_BYTES, ciphertext.size - IV_SIZE_BYTES) + } + + fun decrypt(aesKey: ByteArray, ciphertext: ByteArray, additionalData: ByteArray? = null): ByteArray { + return decrypt(SecretKeySpec(aesKey, "AES"), ciphertext, additionalData) + } +} diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/crypto/AesEncryptionTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/crypto/AesEncryptionTest.kt new file mode 100644 index 0000000000..d3b1ded638 --- /dev/null +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/crypto/AesEncryptionTest.kt @@ -0,0 +1,73 @@ +package net.corda.nodeapi.internal.crypto + +import net.corda.core.crypto.secureRandomBytes +import net.corda.nodeapi.internal.crypto.AesEncryption.IV_SIZE_BYTES +import net.corda.nodeapi.internal.crypto.AesEncryption.KEY_SIZE_BYTES +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatExceptionOfType +import org.junit.Test +import java.security.GeneralSecurityException + +class AesEncryptionTest { + private val aesKey = secureRandomBytes(KEY_SIZE_BYTES) + private val plaintext = secureRandomBytes(257) // Intentionally not a power of 2 + + @Test(timeout = 300_000) + fun `ciphertext can be decrypted using the same key`() { + val ciphertext = AesEncryption.encrypt(aesKey, plaintext) + assertThat(String(ciphertext)).doesNotContain(String(plaintext)) + val decrypted = AesEncryption.decrypt(aesKey, ciphertext) + assertThat(decrypted).isEqualTo(plaintext) + } + + @Test(timeout = 300_000) + fun `ciphertext with authenticated data can be decrypted using the same key`() { + val ciphertext = AesEncryption.encrypt(aesKey, plaintext, "Extra public data".toByteArray()) + assertThat(String(ciphertext)).doesNotContain(String(plaintext)) + val decrypted = AesEncryption.decrypt(aesKey, ciphertext, "Extra public data".toByteArray()) + assertThat(decrypted).isEqualTo(plaintext) + } + + @Test(timeout = 300_000) + fun `ciphertext cannot be decrypted with different authenticated data`() { + val ciphertext = AesEncryption.encrypt(aesKey, plaintext, "Extra public data".toByteArray()) + assertThat(String(ciphertext)).doesNotContain(String(plaintext)) + assertThatExceptionOfType(GeneralSecurityException::class.java).isThrownBy { + AesEncryption.decrypt(aesKey, ciphertext, "Different public data".toByteArray()) + } + } + + @Test(timeout = 300_000) + fun `ciphertext cannot be decrypted with different key`() { + val ciphertext = AesEncryption.encrypt(aesKey, plaintext) + for (index in aesKey.indices) { + aesKey[index]-- + assertThatExceptionOfType(GeneralSecurityException::class.java).isThrownBy { + AesEncryption.decrypt(aesKey, ciphertext) + } + aesKey[index]++ + } + } + + @Test(timeout = 300_000) + fun `corrupted ciphertext cannot be decrypted`() { + val ciphertext = AesEncryption.encrypt(aesKey, plaintext) + for (index in ciphertext.indices) { + ciphertext[index]-- + assertThatExceptionOfType(GeneralSecurityException::class.java).isThrownBy { + AesEncryption.decrypt(aesKey, ciphertext) + } + ciphertext[index]++ + } + } + + @Test(timeout = 300_000) + fun `encrypting same plainttext twice with same key does not produce same ciphertext`() { + val first = AesEncryption.encrypt(aesKey, plaintext) + val second = AesEncryption.encrypt(aesKey, plaintext) + // The IV should be different + assertThat(first.take(IV_SIZE_BYTES)).isNotEqualTo(second.take(IV_SIZE_BYTES)) + // Which should cause the encrypted bytes to be different as well + assertThat(first.drop(IV_SIZE_BYTES)).isNotEqualTo(second.drop(IV_SIZE_BYTES)) + } +} diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index 198b158d24..cbae60071e 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -128,6 +128,7 @@ import net.corda.node.services.persistence.AbstractPartyToX500NameAsStringConver import net.corda.node.services.persistence.AttachmentStorageInternal import net.corda.node.services.persistence.DBCheckpointPerformanceRecorder import net.corda.node.services.persistence.DBCheckpointStorage +import net.corda.node.services.persistence.AesDbEncryptionService import net.corda.node.services.persistence.DBTransactionMappingStorage import net.corda.node.services.persistence.DBTransactionStorageLedgerRecovery import net.corda.node.services.persistence.NodeAttachmentService @@ -286,6 +287,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, val networkMapCache = PersistentNetworkMapCache(cacheFactory, database, identityService).tokenize() val partyInfoCache = PersistentPartyInfoCache(networkMapCache, cacheFactory, database) + val encryptionService = AesDbEncryptionService(database) @Suppress("LeakingThis") val cryptoService = makeCryptoService() @Suppress("LeakingThis") @@ -658,6 +660,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, verifyCheckpointsCompatible(frozenTokenizableServices) partyInfoCache.start() + encryptionService.start(nodeInfo.legalIdentities[0]) /* Note the .get() at the end of the distributeEvent call, below. This will block until all Corda Services have returned from processing the event, allowing a service to prevent the @@ -1080,7 +1083,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, } protected open fun makeTransactionStorage(transactionCacheSizeBytes: Long): WritableTransactionStorage { - return DBTransactionStorageLedgerRecovery(database, cacheFactory, platformClock, cryptoService, partyInfoCache) + return DBTransactionStorageLedgerRecovery(database, cacheFactory, platformClock, encryptionService, partyInfoCache) } protected open fun makeNetworkParametersStorage(): NetworkParametersStorage { diff --git a/node/src/main/kotlin/net/corda/node/services/EncryptionService.kt b/node/src/main/kotlin/net/corda/node/services/EncryptionService.kt new file mode 100644 index 0000000000..85dea166e0 --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/services/EncryptionService.kt @@ -0,0 +1,42 @@ +package net.corda.node.services + +/** + * A service for encrypting data. This abstraction does not mandate any security properties except the same service instance will be + * able to decrypt ciphertext encrypted by it. Further security properties are defined by the implementations. This includes the encryption + * protocol used. + */ +interface EncryptionService { + /** + * Encrypt the given [plaintext]. The encryption key used is dependent on the implementation. The returned ciphertext can be decrypted + * using [decrypt]. + * + * An optional public [additionalData] bytes can also be provided which will be authenticated (thus tamperproof) alongside the + * ciphertext but not encrypted. It will be incorporated into the returned bytes in an implementation dependent fashion. + */ + fun encrypt(plaintext: ByteArray, additionalData: ByteArray? = null): ByteArray + + /** + * Decrypt ciphertext that was encrypted using [encrypt] and return the original plaintext plus the additional data authenticated (if + * present). The service will select the correct encryption key to use. + */ + fun decrypt(ciphertext: ByteArray): PlaintextAndAAD + + /** + * Extracts the (unauthenticated) additional data, if present, from the given [ciphertext]. This is the public data that would have been + * given at encryption time. + * + * Note, this method does not verify if the data was tampered with, and hence is unauthenticated. To have it authenticated requires + * calling [decrypt]. This is still useful however, as it doesn't require the encryption key, and so a third-party can view the + * additional data without needing access to the key. + */ + fun extractUnauthenticatedAdditionalData(ciphertext: ByteArray): ByteArray? + + + /** + * Represents the decrypted plaintext and the optional authenticated additional data bytes. + */ + class PlaintextAndAAD(val plaintext: ByteArray, val authenticatedAdditionalData: ByteArray?) { + operator fun component1() = plaintext + operator fun component2() = authenticatedAdditionalData + } +} diff --git a/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt b/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt index a5ef5f054a..962f7a0664 100644 --- a/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt +++ b/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt @@ -372,22 +372,26 @@ interface WritableTransactionStorage : TransactionStorage { /** * Records Sender [TransactionMetadata] for a given txnId. * - * @param id The SecureHash of a transaction. + * @param txId The SecureHash of a transaction. * @param metadata The recovery metadata associated with a transaction. * @return encrypted distribution list (hashed peers -> StatesToRecord values). */ - fun addSenderTransactionRecoveryMetadata(id: SecureHash, metadata: TransactionMetadata): ByteArray? + fun addSenderTransactionRecoveryMetadata(txId: SecureHash, metadata: TransactionMetadata): ByteArray? /** * Records Received [TransactionMetadata] for a given txnId. * - * @param id The SecureHash of a transaction. + * @param txId The SecureHash of a transaction. * @param sender The sender of the transaction. * @param receiver The receiver of the transaction. * @param receiverStatesToRecord The StatesToRecord value of the receiver. * @param encryptedDistributionList encrypted distribution list (hashed peers -> StatesToRecord values) */ - fun addReceiverTransactionRecoveryMetadata(id: SecureHash, sender: CordaX500Name, receiver: CordaX500Name, receiverStatesToRecord: StatesToRecord, encryptedDistributionList: ByteArray) + fun addReceiverTransactionRecoveryMetadata(txId: SecureHash, + sender: CordaX500Name, + receiver: CordaX500Name, + receiverStatesToRecord: StatesToRecord, + encryptedDistributionList: ByteArray) /** * Removes an un-notarised transaction (with a status of *MISSING_TRANSACTION_SIG*) from the data store. diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/AesDbEncryptionService.kt b/node/src/main/kotlin/net/corda/node/services/persistence/AesDbEncryptionService.kt new file mode 100644 index 0000000000..924ef48c7f --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/services/persistence/AesDbEncryptionService.kt @@ -0,0 +1,152 @@ +package net.corda.node.services.persistence + +import net.corda.core.crypto.newSecureRandom +import net.corda.core.identity.Party +import net.corda.core.internal.copyBytes +import net.corda.core.serialization.SingletonSerializeAsToken +import net.corda.node.services.EncryptionService +import net.corda.nodeapi.internal.crypto.AesEncryption +import net.corda.nodeapi.internal.persistence.CordaPersistence +import net.corda.nodeapi.internal.persistence.NODE_DATABASE_PREFIX +import org.hibernate.annotations.Type +import java.nio.ByteBuffer +import java.security.Key +import java.security.MessageDigest +import java.util.UUID +import javax.crypto.Cipher +import javax.crypto.SecretKey +import javax.crypto.spec.SecretKeySpec +import javax.persistence.Column +import javax.persistence.Entity +import javax.persistence.Id +import javax.persistence.Table + +/** + * [EncryptionService] which uses AES keys stored in the node database. A random key is chosen for encryption, and the resultant ciphertext + * encodes the key used so that it can be decrypted without needing further information. + * + * **Storing encryption keys in a database is not secure, and so only use this service if the data being encrypted is also stored + * unencrypted in the same database.** + * + * To obfuscate the keys, they are stored wrapped using another AES key (called the wrapping key or key-encryption-key) derived from the + * node's legal identity. This is not a security measure; it's only meant to reduce the impact of accidental leakage. + */ +// TODO Add support for key expiry +class AesDbEncryptionService(private val database: CordaPersistence) : EncryptionService, SingletonSerializeAsToken() { + companion object { + private const val INITIAL_KEY_COUNT = 10 + private const val UUID_BYTES = 16 + } + + private val aesKeys = ArrayList>() + + fun start(ourIdentity: Party) { + database.transaction { + val criteria = session.criteriaBuilder.createQuery(EncryptionKeyRecord::class.java) + criteria.select(criteria.from(EncryptionKeyRecord::class.java)) + val dbKeyRecords = session.createQuery(criteria).resultList + val keyWrapper = Cipher.getInstance("AESWrap") + if (dbKeyRecords.isEmpty()) { + repeat(INITIAL_KEY_COUNT) { + val keyId = UUID.randomUUID() + val aesKey = AesEncryption.randomKey() + aesKeys += Pair(keyId, aesKey) + val wrappedKey = with(keyWrapper) { + init(Cipher.WRAP_MODE, createKEK(ourIdentity, keyId)) + wrap(aesKey) + } + session.save(EncryptionKeyRecord(keyId = keyId, keyMaterial = wrappedKey)) + } + } else { + for (dbKeyRecord in dbKeyRecords) { + val aesKey = with(keyWrapper) { + init(Cipher.UNWRAP_MODE, createKEK(ourIdentity, dbKeyRecord.keyId)) + unwrap(dbKeyRecord.keyMaterial, "AES", Cipher.SECRET_KEY) as SecretKey + } + aesKeys += Pair(dbKeyRecord.keyId, aesKey) + } + } + } + } + + override fun encrypt(plaintext: ByteArray, additionalData: ByteArray?): ByteArray { + val (keyId, aesKey) = aesKeys[newSecureRandom().nextInt(aesKeys.size)] + val ciphertext = AesEncryption.encrypt(aesKey, plaintext, additionalData) + val buffer = ByteBuffer.allocate(1 + UUID_BYTES + Integer.BYTES + (additionalData?.size ?: 0) + ciphertext.size) + buffer.put(1) // Version tag + // Prepend the key ID to the returned ciphertext. It's OK that this is not included in the authenticated additional data because + // changing this value will lead to either an non-existent key or an another key which will not be able decrypt the ciphertext. + buffer.putUUID(keyId) + if (additionalData != null) { + buffer.putInt(additionalData.size) + buffer.put(additionalData) + } else { + buffer.putInt(0) + } + buffer.put(ciphertext) + return buffer.array() + } + + override fun decrypt(ciphertext: ByteArray): EncryptionService.PlaintextAndAAD { + val buffer = ByteBuffer.wrap(ciphertext) + val version = buffer.get().toInt() + require(version == 1) + val keyId = buffer.getUUID() + val aesKey = requireNotNull(aesKeys.find { it.first == keyId }?.second) { "Unable to decrypt" } + val additionalData = buffer.getAdditionaData() + val plaintext = AesEncryption.decrypt(aesKey, buffer.copyBytes(), additionalData) + // Only now is the additional data authenticated + return EncryptionService.PlaintextAndAAD(plaintext, additionalData) + } + + override fun extractUnauthenticatedAdditionalData(ciphertext: ByteArray): ByteArray? { + val buffer = ByteBuffer.wrap(ciphertext) + buffer.position(1 + UUID_BYTES) + return buffer.getAdditionaData() + } + + private fun ByteBuffer.getAdditionaData(): ByteArray? { + val additionalDataSize = getInt() + return if (additionalDataSize > 0) ByteArray(additionalDataSize).also { get(it) } else null + } + + private fun UUID.toByteArray(): ByteArray { + val buffer = ByteBuffer.allocate(UUID_BYTES) + buffer.putUUID(this) + return buffer.array() + } + + /** + * Derive the key-encryption-key (KEK) from the the node's identity and the persisted key's ID. + */ + private fun createKEK(ourIdentity: Party, keyId: UUID): Key { + val digest = MessageDigest.getInstance("SHA-256") + digest.update(ourIdentity.name.x500Principal.encoded) + digest.update(keyId.toByteArray()) + return SecretKeySpec(digest.digest(), 0, AesEncryption.KEY_SIZE_BYTES, "AES") + } + + + @Entity + @Table(name = "${NODE_DATABASE_PREFIX}aes_encryption_keys") + class EncryptionKeyRecord( + @Id + @Type(type = "uuid-char") + @Column(name = "key_id", nullable = false) + val keyId: UUID, + + @Column(name = "key_material", nullable = false) + val keyMaterial: ByteArray + ) +} + +internal fun ByteBuffer.putUUID(uuid: UUID) { + putLong(uuid.mostSignificantBits) + putLong(uuid.leastSignificantBits) +} + +internal fun ByteBuffer.getUUID(): UUID { + val mostSigBits = getLong() + val leastSigBits = getLong() + return UUID(mostSigBits, leastSigBits) +} diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt index 56acdfd61b..1973f9e7c1 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt @@ -215,9 +215,13 @@ open class DBTransactionStorage(private val database: CordaPersistence, cacheFac false } - override fun addSenderTransactionRecoveryMetadata(id: SecureHash, metadata: TransactionMetadata): ByteArray? { return null } + override fun addSenderTransactionRecoveryMetadata(txId: SecureHash, metadata: TransactionMetadata): ByteArray? { return null } - override fun addReceiverTransactionRecoveryMetadata(id: SecureHash, sender: CordaX500Name, receiver: CordaX500Name, receiverStatesToRecord: StatesToRecord, encryptedDistributionList: ByteArray) { } + override fun addReceiverTransactionRecoveryMetadata(txId: SecureHash, + sender: CordaX500Name, + receiver: CordaX500Name, + receiverStatesToRecord: StatesToRecord, + encryptedDistributionList: ByteArray) { } override fun finalizeTransaction(transaction: SignedTransaction) = addTransaction(transaction) { diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecovery.kt b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecovery.kt index 0d00344742..6c101a8401 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecovery.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecovery.kt @@ -9,15 +9,11 @@ import net.corda.core.node.StatesToRecord import net.corda.core.node.services.vault.Sort import net.corda.core.serialization.CordaSerializable import net.corda.node.CordaClock +import net.corda.node.services.EncryptionService import net.corda.node.services.network.PersistentPartyInfoCache -import net.corda.nodeapi.internal.cryptoservice.CryptoService import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.NODE_DATABASE_PREFIX import org.hibernate.annotations.Immutable -import java.io.ByteArrayInputStream -import java.io.ByteArrayOutputStream -import java.io.DataInputStream -import java.io.DataOutputStream import java.io.Serializable import java.time.Instant import java.util.concurrent.atomic.AtomicLong @@ -31,9 +27,10 @@ import javax.persistence.Table import javax.persistence.criteria.Predicate import kotlin.streams.toList -class DBTransactionStorageLedgerRecovery(private val database: CordaPersistence, cacheFactory: NamedCacheFactory, +class DBTransactionStorageLedgerRecovery(private val database: CordaPersistence, + cacheFactory: NamedCacheFactory, val clock: CordaClock, - private val cryptoService: CryptoService, + private val encryptionService: EncryptionService, private val partyInfoCache: PersistentPartyInfoCache) : DBTransactionStorage(database, cacheFactory, clock) { @Embeddable @Immutable @@ -63,7 +60,6 @@ class DBTransactionStorageLedgerRecovery(private val database: CordaPersistence, /** states to record: NONE, ALL_VISIBLE, ONLY_RELEVANT */ @Column(name = "states_to_record", nullable = false) var statesToRecord: StatesToRecord - ) { fun toSenderDistributionRecord() = SenderDistributionRecord( @@ -76,7 +72,7 @@ class DBTransactionStorageLedgerRecovery(private val database: CordaPersistence, @Entity @Table(name = "${NODE_DATABASE_PREFIX}receiver_distribution_records") - data class DBReceiverDistributionRecord( + class DBReceiverDistributionRecord( @EmbeddedId var compositeKey: PersistentKey, @@ -95,17 +91,18 @@ class DBTransactionStorageLedgerRecovery(private val database: CordaPersistence, /** states to record: NONE, ALL_VISIBLE, ONLY_RELEVANT */ @Column(name = "receiver_states_to_record", nullable = false) val receiverStatesToRecord: StatesToRecord -) { + ) { constructor(key: Key, txId: SecureHash, initiatorPartyId: Long, encryptedDistributionList: ByteArray, receiverStatesToRecord: StatesToRecord) : - this(PersistentKey(key), - txId = txId.toString(), - senderPartyId = initiatorPartyId, - distributionList = encryptedDistributionList, - receiverStatesToRecord = receiverStatesToRecord - ) + this( + PersistentKey(key), + txId = txId.toString(), + senderPartyId = initiatorPartyId, + distributionList = encryptedDistributionList, + receiverStatesToRecord = receiverStatesToRecord + ) - fun toReceiverDistributionRecord(cryptoService: CryptoService): ReceiverDistributionRecord { - val hashedDL = HashedDistributionList.deserialize(cryptoService.decrypt(this.distributionList)) + fun toReceiverDistributionRecord(encryptionService: EncryptionService): ReceiverDistributionRecord { + val hashedDL = HashedDistributionList.decrypt(this.distributionList, encryptionService) return ReceiverDistributionRecord( SecureHash.parse(this.txId), this.senderPartyId, @@ -139,32 +136,45 @@ class DBTransactionStorageLedgerRecovery(private val database: CordaPersistence, } } - override fun addSenderTransactionRecoveryMetadata(id: SecureHash, metadata: TransactionMetadata): ByteArray { + override fun addSenderTransactionRecoveryMetadata(txId: SecureHash, metadata: TransactionMetadata): ByteArray { return database.transaction { val senderRecordingTimestamp = clock.instant() - metadata.distributionList.peersToStatesToRecord.forEach { (peer, _) -> - val senderDistributionRecord = DBSenderDistributionRecord(PersistentKey(Key(senderRecordingTimestamp)), - id.toString(), + for (peer in metadata.distributionList.peersToStatesToRecord.keys) { + val senderDistributionRecord = DBSenderDistributionRecord( + PersistentKey(Key(senderRecordingTimestamp)), + txId.toString(), partyInfoCache.getPartyIdByCordaX500Name(peer), - metadata.distributionList.senderStatesToRecord) + metadata.distributionList.senderStatesToRecord + ) session.save(senderDistributionRecord) } - val hashedPeersToStatesToRecord = metadata.distributionList.peersToStatesToRecord.map { (peer, statesToRecord) -> - partyInfoCache.getPartyIdByCordaX500Name(peer) to statesToRecord }.toMap() - val hashedDistributionList = HashedDistributionList(metadata.distributionList.senderStatesToRecord, hashedPeersToStatesToRecord, senderRecordingTimestamp) - cryptoService.encrypt(hashedDistributionList.serialize()) + + val hashedPeersToStatesToRecord = metadata.distributionList.peersToStatesToRecord.mapKeys { (peer) -> + partyInfoCache.getPartyIdByCordaX500Name(peer) + } + val hashedDistributionList = HashedDistributionList( + metadata.distributionList.senderStatesToRecord, + hashedPeersToStatesToRecord, + HashedDistributionList.PublicHeader(senderRecordingTimestamp) + ) + hashedDistributionList.encrypt(encryptionService) } } - override fun addReceiverTransactionRecoveryMetadata(id: SecureHash, sender: CordaX500Name, receiver: CordaX500Name, receiverStatesToRecord: StatesToRecord, encryptedDistributionList: ByteArray) { - val senderRecordedTimestamp = HashedDistributionList.deserialize(cryptoService.decrypt(encryptedDistributionList)).senderRecordedTimestamp + override fun addReceiverTransactionRecoveryMetadata(txId: SecureHash, + sender: CordaX500Name, + receiver: CordaX500Name, + receiverStatesToRecord: StatesToRecord, + encryptedDistributionList: ByteArray) { + val publicHeader = HashedDistributionList.PublicHeader.unauthenticatedDeserialise(encryptedDistributionList, encryptionService) database.transaction { - val receiverDistributionRecord = - DBReceiverDistributionRecord(Key(senderRecordedTimestamp), - id, - partyInfoCache.getPartyIdByCordaX500Name(sender), - encryptedDistributionList, - receiverStatesToRecord) + val receiverDistributionRecord = DBReceiverDistributionRecord( + Key(publicHeader.senderRecordedTimestamp), + txId, + partyInfoCache.getPartyIdByCordaX500Name(sender), + encryptedDistributionList, + receiverStatesToRecord + ) session.save(receiverDistributionRecord) } } @@ -235,8 +245,9 @@ class DBTransactionStorageLedgerRecovery(private val database: CordaPersistence, } criteriaQuery.orderBy(orderCriteria) } - val results = session.createQuery(criteriaQuery).stream() - results.map { it.toSenderDistributionRecord() }.toList() + session.createQuery(criteriaQuery).stream().use { results -> + results.map { it.toSenderDistributionRecord() }.toList() + } } } @@ -273,21 +284,13 @@ class DBTransactionStorageLedgerRecovery(private val database: CordaPersistence, } criteriaQuery.orderBy(orderCriteria) } - val results = session.createQuery(criteriaQuery).stream() - results.map { it.toReceiverDistributionRecord(cryptoService) }.toList() + session.createQuery(criteriaQuery).stream().use { results -> + results.map { it.toReceiverDistributionRecord(encryptionService) }.toList() + } } } } -// TO DO: https://r3-cev.atlassian.net/browse/ENT-9876 -private fun CryptoService.decrypt(bytes: ByteArray): ByteArray { - return bytes -} - -// TO DO: https://r3-cev.atlassian.net/browse/ENT-9876 -fun CryptoService.encrypt(bytes: ByteArray): ByteArray { - return bytes -} @CordaSerializable open class DistributionRecord( @@ -318,46 +321,3 @@ data class ReceiverDistributionRecord( enum class DistributionRecordType { SENDER, RECEIVER, ALL } - -@CordaSerializable -data class HashedDistributionList( - val senderStatesToRecord: StatesToRecord, - val peerHashToStatesToRecord: Map, - val senderRecordedTimestamp: Instant -) { - fun serialize(): ByteArray { - val baos = ByteArrayOutputStream() - val out = DataOutputStream(baos) - out.use { - out.writeByte(SERIALIZER_VERSION_ID) - out.writeByte(senderStatesToRecord.ordinal) - out.writeInt(peerHashToStatesToRecord.size) - for(entry in peerHashToStatesToRecord) { - out.writeLong(entry.key) - out.writeByte(entry.value.ordinal) - } - out.writeLong(senderRecordedTimestamp.toEpochMilli()) - out.flush() - return baos.toByteArray() - } - } - companion object { - const val SERIALIZER_VERSION_ID = 1 - fun deserialize(bytes: ByteArray): HashedDistributionList { - val input = DataInputStream(ByteArrayInputStream(bytes)) - input.use { - assert(input.readByte().toInt() == SERIALIZER_VERSION_ID) { "Serialization version conflict." } - val senderStatesToRecord = StatesToRecord.values()[input.readByte().toInt()] - val numPeerHashToStatesToRecords = input.readInt() - val peerHashToStatesToRecord = mutableMapOf() - repeat (numPeerHashToStatesToRecords) { - peerHashToStatesToRecord[input.readLong()] = StatesToRecord.values()[input.readByte().toInt()] - } - val senderRecordedTimestamp = Instant.ofEpochMilli(input.readLong()) - return HashedDistributionList(senderStatesToRecord, peerHashToStatesToRecord, senderRecordedTimestamp) - } - } - } -} - - diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/HashedDistributionList.kt b/node/src/main/kotlin/net/corda/node/services/persistence/HashedDistributionList.kt new file mode 100644 index 0000000000..910a00ce74 --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/services/persistence/HashedDistributionList.kt @@ -0,0 +1,104 @@ +package net.corda.node.services.persistence + +import net.corda.core.node.StatesToRecord +import net.corda.core.serialization.CordaSerializable +import net.corda.node.services.EncryptionService +import java.io.ByteArrayOutputStream +import java.io.DataInputStream +import java.io.DataOutputStream +import java.nio.ByteBuffer +import java.time.Instant + +@Suppress("TooGenericExceptionCaught") +@CordaSerializable +data class HashedDistributionList( + val senderStatesToRecord: StatesToRecord, + val peerHashToStatesToRecord: Map, + val publicHeader: PublicHeader +) { + /** + * Encrypt this hashed distribution list using the given [EncryptionService]. The [publicHeader] is not encrypted but is instead + * authenticated so that it is tamperproof. + * + * The same [EncryptionService] instance needs to be used with [decrypt] for decryption. + */ + fun encrypt(encryptionService: EncryptionService): ByteArray { + val baos = ByteArrayOutputStream() + val out = DataOutputStream(baos) + out.writeByte(senderStatesToRecord.ordinal) + out.writeInt(peerHashToStatesToRecord.size) + for (entry in peerHashToStatesToRecord) { + out.writeLong(entry.key) + out.writeByte(entry.value.ordinal) + } + return encryptionService.encrypt(baos.toByteArray(), publicHeader.serialise()) + } + + + @CordaSerializable + data class PublicHeader( + val senderRecordedTimestamp: Instant + ) { + fun serialise(): ByteArray { + val buffer = ByteBuffer.allocate(1 + java.lang.Long.BYTES) + buffer.put(VERSION_TAG.toByte()) + buffer.putLong(senderRecordedTimestamp.toEpochMilli()) + return buffer.array() + } + + companion object { + /** + * Deserialise a [PublicHeader] from the given [encryptedBytes]. The bytes is expected is to be a valid encrypted blob that can + * be decrypted by [HashedDistributionList.decrypt] using the same [EncryptionService]. + * + * Because this method does not actually decrypt the bytes, the header returned is not authenticated and any modifications to it + * will not be detected. That can only be done by the encrypting party with [HashedDistributionList.decrypt]. + */ + fun unauthenticatedDeserialise(encryptedBytes: ByteArray, encryptionService: EncryptionService): PublicHeader { + val additionalData = encryptionService.extractUnauthenticatedAdditionalData(encryptedBytes) + requireNotNull(additionalData) { "Missing additional data field" } + return deserialise(additionalData!!) + } + + fun deserialise(bytes: ByteArray): PublicHeader { + val buffer = ByteBuffer.wrap(bytes) + try { + val version = buffer.get().toInt() + require(version == VERSION_TAG) { "Unknown distribution list format $version" } + val senderRecordedTimestamp = Instant.ofEpochMilli(buffer.getLong()) + return PublicHeader(senderRecordedTimestamp) + } catch (e: Exception) { + throw IllegalArgumentException("Corrupt or not a distribution list header", e) + } + } + } + } + + companion object { + // The version tag is serialised in the header, even though it is separate from the encrypted main body of the distribution list. + // This is because the header and the dist list are cryptographically coupled and we want to avoid declaring the version field twice. + private const val VERSION_TAG = 1 + private val statesToRecordValues = StatesToRecord.values() // Cache the enum values since .values() returns a new array each time. + + /** + * Decrypt a [HashedDistributionList] from the given [encryptedBytes] using the same [EncryptionService] that was used in [encrypt]. + */ + fun decrypt(encryptedBytes: ByteArray, encryptionService: EncryptionService): HashedDistributionList { + val (plaintext, authenticatedAdditionalData) = encryptionService.decrypt(encryptedBytes) + requireNotNull(authenticatedAdditionalData) { "Missing authenticated header" } + val publicHeader = PublicHeader.deserialise(authenticatedAdditionalData!!) + val input = DataInputStream(plaintext.inputStream()) + try { + val senderStatesToRecord = statesToRecordValues[input.readByte().toInt()] + val numPeerHashToStatesToRecords = input.readInt() + val peerHashToStatesToRecord = mutableMapOf() + repeat(numPeerHashToStatesToRecords) { + peerHashToStatesToRecord[input.readLong()] = statesToRecordValues[input.readByte().toInt()] + } + return HashedDistributionList(senderStatesToRecord, peerHashToStatesToRecord, publicHeader) + } catch (e: Exception) { + throw IllegalArgumentException("Corrupt or not a distribution list", e) + } + } + } +} diff --git a/node/src/main/kotlin/net/corda/node/services/schema/NodeSchemaService.kt b/node/src/main/kotlin/net/corda/node/services/schema/NodeSchemaService.kt index 760544758d..68dc445e29 100644 --- a/node/src/main/kotlin/net/corda/node/services/schema/NodeSchemaService.kt +++ b/node/src/main/kotlin/net/corda/node/services/schema/NodeSchemaService.kt @@ -16,6 +16,7 @@ import net.corda.node.services.keys.BasicHSMKeyManagementService import net.corda.node.services.messaging.P2PMessageDeduplicator import net.corda.node.services.network.PersistentNetworkMapCache import net.corda.node.services.persistence.DBCheckpointStorage +import net.corda.node.services.persistence.AesDbEncryptionService import net.corda.node.services.persistence.DBTransactionStorageLedgerRecovery import net.corda.node.services.persistence.DBTransactionStorage import net.corda.node.services.persistence.NodeAttachmentService @@ -30,7 +31,7 @@ import net.corda.node.services.vault.VaultSchemaV1 * TODO: support plugins for schema version upgrading or custom mapping not supported by original [QueryableState]. * TODO: create whitelisted tables when a CorDapp is first installed */ -class NodeSchemaService(private val extraSchemas: Set = emptySet()) : SchemaService, SingletonSerializeAsToken() { +class NodeSchemaService(extraSchemas: Set = emptySet()) : SchemaService, SingletonSerializeAsToken() { // Core Entities used by a Node object NodeCore @@ -55,7 +56,8 @@ class NodeSchemaService(private val extraSchemas: Set = emptySet() PersistentNetworkMapCache.PersistentPartyToPublicKeyHash::class.java, DBTransactionStorageLedgerRecovery.DBSenderDistributionRecord::class.java, DBTransactionStorageLedgerRecovery.DBReceiverDistributionRecord::class.java, - DBTransactionStorageLedgerRecovery.DBRecoveryPartyInfo::class.java + DBTransactionStorageLedgerRecovery.DBRecoveryPartyInfo::class.java, + AesDbEncryptionService.EncryptionKeyRecord::class.java )) { override val migrationResource = "node-core.changelog-master" } diff --git a/node/src/main/resources/migration/node-core.changelog-master.xml b/node/src/main/resources/migration/node-core.changelog-master.xml index 0ebf26bdc1..ef9116aade 100644 --- a/node/src/main/resources/migration/node-core.changelog-master.xml +++ b/node/src/main/resources/migration/node-core.changelog-master.xml @@ -31,6 +31,7 @@ + diff --git a/node/src/main/resources/migration/node-core.changelog-v26.xml b/node/src/main/resources/migration/node-core.changelog-v26.xml new file mode 100644 index 0000000000..b0d4925c7a --- /dev/null +++ b/node/src/main/resources/migration/node-core.changelog-v26.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt b/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt index fa516073fb..d0f0de3d60 100644 --- a/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt +++ b/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt @@ -810,15 +810,19 @@ class TwoPartyTradeFlowTests(private val anonymous: Boolean) { return true } - override fun addSenderTransactionRecoveryMetadata(id: SecureHash, metadata: TransactionMetadata): ByteArray? { + override fun addSenderTransactionRecoveryMetadata(txId: SecureHash, metadata: TransactionMetadata): ByteArray? { return database.transaction { - delegate.addSenderTransactionRecoveryMetadata(id, metadata) + delegate.addSenderTransactionRecoveryMetadata(txId, metadata) } } - override fun addReceiverTransactionRecoveryMetadata(id: SecureHash, sender: CordaX500Name, receiver: CordaX500Name, receiverStatesToRecord: StatesToRecord, encryptedDistributionList: ByteArray) { + override fun addReceiverTransactionRecoveryMetadata(txId: SecureHash, + sender: CordaX500Name, + receiver: CordaX500Name, + receiverStatesToRecord: StatesToRecord, + encryptedDistributionList: ByteArray) { database.transaction { - delegate.addReceiverTransactionRecoveryMetadata(id, sender, receiver, receiverStatesToRecord, encryptedDistributionList) + delegate.addReceiverTransactionRecoveryMetadata(txId, sender, receiver, receiverStatesToRecord, encryptedDistributionList) } } diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/AesDbEncryptionServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/persistence/AesDbEncryptionServiceTest.kt new file mode 100644 index 0000000000..806357627a --- /dev/null +++ b/node/src/test/kotlin/net/corda/node/services/persistence/AesDbEncryptionServiceTest.kt @@ -0,0 +1,134 @@ +package net.corda.node.services.persistence + +import net.corda.node.services.persistence.AesDbEncryptionService.EncryptionKeyRecord +import net.corda.nodeapi.internal.persistence.CordaPersistence +import net.corda.nodeapi.internal.persistence.DatabaseConfig +import net.corda.testing.core.TestIdentity +import net.corda.testing.internal.configureDatabase +import net.corda.testing.node.MockServices +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatExceptionOfType +import org.assertj.core.api.Assertions.assertThatIllegalArgumentException +import org.junit.After +import org.junit.Before +import org.junit.Test +import java.nio.ByteBuffer +import java.security.GeneralSecurityException +import java.util.UUID + +class AesDbEncryptionServiceTest { + private val identity = TestIdentity.fresh("me").party + private lateinit var database: CordaPersistence + private lateinit var encryptionService: AesDbEncryptionService + + @Before + fun setUp() { + val dataSourceProps = MockServices.makeTestDataSourceProperties() + database = configureDatabase(dataSourceProps, DatabaseConfig(), { null }, { null }) + encryptionService = AesDbEncryptionService(database) + encryptionService.start(identity) + } + + @After + fun cleanUp() { + database.close() + } + + @Test(timeout = 300_000) + fun `same instance can decrypt ciphertext`() { + val ciphertext = encryptionService.encrypt("Hello World".toByteArray()) + val (plaintext, authenticatedData) = encryptionService.decrypt(ciphertext) + assertThat(String(plaintext)).isEqualTo("Hello World") + assertThat(authenticatedData).isNull() + } + + @Test(timeout = 300_000) + fun `encypting twice produces different ciphertext`() { + val plaintext = "Hello".toByteArray() + assertThat(encryptionService.encrypt(plaintext)).isNotEqualTo(encryptionService.encrypt(plaintext)) + } + + @Test(timeout = 300_000) + fun `ciphertext can be decrypted after restart`() { + val ciphertext = encryptionService.encrypt("Hello World".toByteArray()) + encryptionService = AesDbEncryptionService(database) + encryptionService.start(identity) + val plaintext = encryptionService.decrypt(ciphertext).plaintext + assertThat(String(plaintext)).isEqualTo("Hello World") + } + + @Test(timeout = 300_000) + fun `encrypting with authenticated data`() { + val ciphertext = encryptionService.encrypt("Hello World".toByteArray(), "Additional data".toByteArray()) + val (plaintext, authenticatedData) = encryptionService.decrypt(ciphertext) + assertThat(String(plaintext)).isEqualTo("Hello World") + assertThat(authenticatedData?.let { String(it) }).isEqualTo("Additional data") + } + + @Test(timeout = 300_000) + fun extractUnauthenticatedAdditionalData() { + val ciphertext = encryptionService.encrypt("Hello World".toByteArray(), "Additional data".toByteArray()) + val additionalData = encryptionService.extractUnauthenticatedAdditionalData(ciphertext) + assertThat(additionalData?.let { String(it) }).isEqualTo("Additional data") + } + + @Test(timeout = 300_000) + fun `ciphertext cannot be decrypted if the authenticated data is modified`() { + val ciphertext = ByteBuffer.wrap(encryptionService.encrypt("Hello World".toByteArray(), "1234".toByteArray())) + + ciphertext.position(21) + ciphertext.put("4321".toByteArray()) // Use same length for the modified AAD + + assertThatExceptionOfType(GeneralSecurityException::class.java).isThrownBy { + encryptionService.decrypt(ciphertext.array()) + } + } + + @Test(timeout = 300_000) + fun `ciphertext cannot be decrypted if the key used is deleted`() { + val ciphertext = encryptionService.encrypt("Hello World".toByteArray()) + val keyId = ByteBuffer.wrap(ciphertext).getKeyId() + val deletedCount = database.transaction { + session.createQuery("DELETE FROM ${EncryptionKeyRecord::class.java.name} k WHERE k.keyId = :keyId") + .setParameter("keyId", keyId) + .executeUpdate() + } + assertThat(deletedCount).isEqualTo(1) + + encryptionService = AesDbEncryptionService(database) + encryptionService.start(identity) + assertThatIllegalArgumentException().isThrownBy { + encryptionService.decrypt(ciphertext) + } + } + + @Test(timeout = 300_000) + fun `ciphertext cannot be decrypted if forced to use a different key`() { + val ciphertext = ByteBuffer.wrap(encryptionService.encrypt("Hello World".toByteArray())) + val keyId = ciphertext.getKeyId() + val anotherKeyId = database.transaction { + session.createQuery("SELECT keyId FROM ${EncryptionKeyRecord::class.java.name} k WHERE k.keyId != :keyId", UUID::class.java) + .setParameter("keyId", keyId) + .setMaxResults(1) + .singleResult + } + + ciphertext.putKeyId(anotherKeyId) + + encryptionService = AesDbEncryptionService(database) + encryptionService.start(identity) + assertThatExceptionOfType(GeneralSecurityException::class.java).isThrownBy { + encryptionService.decrypt(ciphertext.array()) + } + } + + private fun ByteBuffer.getKeyId(): UUID { + position(1) + return getUUID() + } + + private fun ByteBuffer.putKeyId(keyId: UUID) { + position(1) + putUUID(keyId) + } +} diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecoveryTests.kt b/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecoveryTests.kt index 5f52c0849f..e38079536d 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecoveryTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecoveryTests.kt @@ -24,7 +24,6 @@ import net.corda.node.services.network.PersistentPartyInfoCache import net.corda.node.services.persistence.DBTransactionStorage.TransactionStatus.IN_FLIGHT import net.corda.node.services.persistence.DBTransactionStorage.TransactionStatus.VERIFIED import net.corda.nodeapi.internal.DEV_ROOT_CA -import net.corda.nodeapi.internal.cryptoservice.CryptoService import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.testing.core.ALICE_NAME @@ -38,7 +37,8 @@ import net.corda.testing.internal.TestingNamedCacheFactory import net.corda.testing.internal.configureDatabase import net.corda.testing.internal.createWireTransaction import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties -import net.corda.testing.node.internal.MockCryptoService +import net.corda.testing.node.internal.MockEncryptionService +import org.assertj.core.api.Assertions.assertThat import org.junit.After import org.junit.Before import org.junit.Rule @@ -67,6 +67,8 @@ class DBTransactionStorageLedgerRecoveryTests { private lateinit var transactionRecovery: DBTransactionStorageLedgerRecovery private lateinit var partyInfoCache: PersistentPartyInfoCache + private val encryptionService = MockEncryptionService() + @Before fun setUp() { val dataSourceProps = makeTestDataSourceProperties() @@ -278,17 +280,21 @@ class DBTransactionStorageLedgerRecoveryTests { @Test(timeout = 300_000) fun `test lightweight serialization and deserialization of hashed distribution list payload`() { - val dl = HashedDistributionList(ALL_VISIBLE, - mapOf(BOB.name.hashCode().toLong() to NONE, CHARLIE_NAME.hashCode().toLong() to ONLY_RELEVANT), now()) - assertEquals(dl, dl.serialize().let { HashedDistributionList.deserialize(it) }) + val hashedDistList = HashedDistributionList( + ALL_VISIBLE, + mapOf(BOB.name.hashCode().toLong() to NONE, CHARLIE_NAME.hashCode().toLong() to ONLY_RELEVANT), + HashedDistributionList.PublicHeader(now()) + ) + val roundtrip = HashedDistributionList.decrypt(hashedDistList.encrypt(encryptionService), encryptionService) + assertThat(roundtrip).isEqualTo(hashedDistList) } private fun readTransactionFromDB(id: SecureHash): DBTransactionStorage.DBTransaction { val fromDb = database.transaction { session.createQuery( - "from ${DBTransactionStorage.DBTransaction::class.java.name} where tx_id = :transactionId", + "from ${DBTransactionStorage.DBTransaction::class.java.name} where txId = :transactionId", DBTransactionStorage.DBTransaction::class.java - ).setParameter("transactionId", id.toString()).resultList.map { it } + ).setParameter("transactionId", id.toString()).resultList } assertEquals(1, fromDb.size) return fromDb[0] @@ -298,7 +304,7 @@ class DBTransactionStorageLedgerRecoveryTests { return database.transaction { if (id != null) session.createQuery( - "from ${DBTransactionStorageLedgerRecovery.DBSenderDistributionRecord::class.java.name} where tx_id = :transactionId", + "from ${DBTransactionStorageLedgerRecovery.DBSenderDistributionRecord::class.java.name} where txId = :transactionId", DBTransactionStorageLedgerRecovery.DBSenderDistributionRecord::class.java ).setParameter("transactionId", id.toString()).resultList.map { it.toSenderDistributionRecord() } else @@ -312,17 +318,15 @@ class DBTransactionStorageLedgerRecoveryTests { private fun readReceiverDistributionRecordFromDB(id: SecureHash): ReceiverDistributionRecord { val fromDb = database.transaction { session.createQuery( - "from ${DBTransactionStorageLedgerRecovery.DBReceiverDistributionRecord::class.java.name} where tx_id = :transactionId", + "from ${DBTransactionStorageLedgerRecovery.DBReceiverDistributionRecord::class.java.name} where txId = :transactionId", DBTransactionStorageLedgerRecovery.DBReceiverDistributionRecord::class.java - ).setParameter("transactionId", id.toString()).resultList.map { it } + ).setParameter("transactionId", id.toString()).resultList } assertEquals(1, fromDb.size) - return fromDb[0].toReceiverDistributionRecord(MockCryptoService(emptyMap())) + return fromDb[0].toReceiverDistributionRecord(encryptionService) } - private fun newTransactionRecovery(cacheSizeBytesOverride: Long? = null, clock: CordaClock = SimpleClock(Clock.systemUTC()), - cryptoService: CryptoService = MockCryptoService(emptyMap())) { - + private fun newTransactionRecovery(cacheSizeBytesOverride: Long? = null, clock: CordaClock = SimpleClock(Clock.systemUTC())) { val networkMapCache = PersistentNetworkMapCache(TestingNamedCacheFactory(), database, InMemoryIdentityService(trustRoot = DEV_ROOT_CA.certificate)) val alice = createNodeInfo(listOf(ALICE)) val bob = createNodeInfo(listOf(BOB)) @@ -330,8 +334,13 @@ class DBTransactionStorageLedgerRecoveryTests { networkMapCache.addOrUpdateNodes(listOf(alice, bob, charlie)) partyInfoCache = PersistentPartyInfoCache(networkMapCache, TestingNamedCacheFactory(), database) partyInfoCache.start() - transactionRecovery = DBTransactionStorageLedgerRecovery(database, TestingNamedCacheFactory(cacheSizeBytesOverride - ?: 1024), clock, cryptoService, partyInfoCache) + transactionRecovery = DBTransactionStorageLedgerRecovery( + database, + TestingNamedCacheFactory(cacheSizeBytesOverride ?: 1024), + clock, + encryptionService, + partyInfoCache + ) } private var portCounter = 1000 @@ -370,10 +379,13 @@ class DBTransactionStorageLedgerRecoveryTests { private fun notarySig(txId: SecureHash) = DUMMY_NOTARY.keyPair.sign(SignableData(txId, SignatureMetadata(1, Crypto.findSignatureScheme(DUMMY_NOTARY.publicKey).schemeNumberID))) - private fun DistributionList.toWire(cryptoService: CryptoService = MockCryptoService(emptyMap())): ByteArray { - val hashedPeersToStatesToRecord = this.peersToStatesToRecord.map { (peer, statesToRecord) -> - partyInfoCache.getPartyIdByCordaX500Name(peer) to statesToRecord }.toMap() - val hashedDistributionList = HashedDistributionList(this.senderStatesToRecord, hashedPeersToStatesToRecord, now()) - return cryptoService.encrypt(hashedDistributionList.serialize()) + private fun DistributionList.toWire(): ByteArray { + val hashedPeersToStatesToRecord = this.peersToStatesToRecord.mapKeys { (peer) -> partyInfoCache.getPartyIdByCordaX500Name(peer) } + val hashedDistributionList = HashedDistributionList( + this.senderStatesToRecord, + hashedPeersToStatesToRecord, + HashedDistributionList.PublicHeader(now()) + ) + return hashedDistributionList.encrypt(encryptionService) } } diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/MockEncryptionService.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/MockEncryptionService.kt new file mode 100644 index 0000000000..1c3875191c --- /dev/null +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/MockEncryptionService.kt @@ -0,0 +1,39 @@ +package net.corda.testing.node.internal + +import net.corda.core.internal.copyBytes +import net.corda.node.services.EncryptionService +import net.corda.nodeapi.internal.crypto.AesEncryption +import java.nio.ByteBuffer +import javax.crypto.SecretKey + +class MockEncryptionService(private val aesKey: SecretKey = AesEncryption.randomKey()) : EncryptionService { + override fun encrypt(plaintext: ByteArray, additionalData: ByteArray?): ByteArray { + val ciphertext = AesEncryption.encrypt(aesKey, plaintext, additionalData) + val buffer = ByteBuffer.allocate(Integer.BYTES + (additionalData?.size ?: 0) + ciphertext.size) + if (additionalData != null) { + buffer.putInt(additionalData.size) + buffer.put(additionalData) + } else { + buffer.putInt(0) + } + buffer.put(ciphertext) + return buffer.array() + } + + override fun decrypt(ciphertext: ByteArray): EncryptionService.PlaintextAndAAD { + val buffer = ByteBuffer.wrap(ciphertext) + val additionalData = buffer.getAdditionaData() + val plaintext = AesEncryption.decrypt(aesKey, buffer.copyBytes(), additionalData) + // Only now is the additional data authenticated + return EncryptionService.PlaintextAndAAD(plaintext, additionalData) + } + + override fun extractUnauthenticatedAdditionalData(ciphertext: ByteArray): ByteArray? { + return ByteBuffer.wrap(ciphertext).getAdditionaData() + } + + private fun ByteBuffer.getAdditionaData(): ByteArray? { + val additionalDataSize = getInt() + return if (additionalDataSize > 0) ByteArray(additionalDataSize).also { get(it) } else null + } +} diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/MockTransactionStorage.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/MockTransactionStorage.kt index f850aab58b..9f23bf6beb 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/MockTransactionStorage.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/MockTransactionStorage.kt @@ -61,9 +61,13 @@ open class MockTransactionStorage : WritableTransactionStorage, SingletonSeriali return txns.putIfAbsent(transaction.id, TxHolder(transaction, status = TransactionStatus.IN_FLIGHT)) == null } - override fun addSenderTransactionRecoveryMetadata(id: SecureHash, metadata: TransactionMetadata): ByteArray? { return null } + override fun addSenderTransactionRecoveryMetadata(txId: SecureHash, metadata: TransactionMetadata): ByteArray? { return null } - override fun addReceiverTransactionRecoveryMetadata(id: SecureHash, sender: CordaX500Name, receiver: CordaX500Name, receiverStatesToRecord: StatesToRecord, encryptedDistributionList: ByteArray) { } + override fun addReceiverTransactionRecoveryMetadata(txId: SecureHash, + sender: CordaX500Name, + receiver: CordaX500Name, + receiverStatesToRecord: StatesToRecord, + encryptedDistributionList: ByteArray) { } override fun removeUnnotarisedTransaction(id: SecureHash): Boolean { return txns.remove(id) != null From 106ccd7fe8a72701708e29ebc065e68237bd9965 Mon Sep 17 00:00:00 2001 From: Adel El-Beik <48713346+adelel1@users.noreply.github.com> Date: Mon, 31 Jul 2023 10:42:28 +0100 Subject: [PATCH 047/126] ENT-10273: Upgrade BC to 1.75. (#7422) * ENT-10273: Upgrade BC to 1.75. * Use BC 1.70 for core-deterministic avoid issue with primality checking done in 1.70+ which uses random numbers --------- Co-authored-by: Shams Asari --- constants.properties | 2 +- core-deterministic/build.gradle | 5 +++-- core/build.gradle | 4 ++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/constants.properties b/constants.properties index bc90310d62..bf4617968f 100644 --- a/constants.properties +++ b/constants.properties @@ -24,7 +24,7 @@ jdkClassifier11=jdk11 dockerJavaVersion=3.2.5 proguardVersion=6.1.1 // bouncy castle version must not be changed on a patch release. Needs a full release test cycle to flush out any issues. -bouncycastleVersion=1.70 +bouncycastleVersion=1.75 classgraphVersion=4.8.135 disruptorVersion=3.4.2 typesafeConfigVersion=1.3.4 diff --git a/core-deterministic/build.gradle b/core-deterministic/build.gradle index 2c597dbabc..3f84bc5e1c 100644 --- a/core-deterministic/build.gradle +++ b/core-deterministic/build.gradle @@ -49,9 +49,10 @@ dependencies { // These dependencies will become "runtime" scoped in our published POM. // See publish.dependenciesFrom.defaultScope. - deterministicLibraries "org.bouncycastle:bcprov-jdk15on:$bouncycastle_version" - deterministicLibraries "org.bouncycastle:bcpkix-jdk15on:$bouncycastle_version" deterministicLibraries "net.i2p.crypto:eddsa:$eddsa_version" + // From 1.71 BC validates the RSA modulus is prime using random numbers, which is not supported inside the DJVM + deterministicLibraries "org.bouncycastle:bcprov-jdk15on:1.70" + deterministicLibraries "org.bouncycastle:bcpkix-jdk15on:1.70" } tasks.named('jar', Jar) { diff --git a/core/build.gradle b/core/build.gradle index 0ae956a805..fc022ad5e9 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -73,8 +73,8 @@ dependencies { compile "net.i2p.crypto:eddsa:$eddsa_version" // Bouncy castle support needed for X509 certificate manipulation - compile "org.bouncycastle:bcprov-jdk15on:${bouncycastle_version}" - compile "org.bouncycastle:bcpkix-jdk15on:${bouncycastle_version}" + compile "org.bouncycastle:bcprov-jdk18on:${bouncycastle_version}" + compile "org.bouncycastle:bcpkix-jdk18on:${bouncycastle_version}" // JPA 2.2 annotations. compile "javax.persistence:javax.persistence-api:2.2" From c614b21a2a21a35fb3010e25ba4b9343f8fd8c19 Mon Sep 17 00:00:00 2001 From: Adel El-Beik Date: Tue, 1 Aug 2023 15:11:21 +0100 Subject: [PATCH 048/126] ENT-10122: Added annotation for backwards compatibility and added test. --- .../corda/core/node/services/VaultService.kt | 4 +- .../net/corda/node/CashIssueAndPaymentTest.kt | 40 +++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt b/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt index bf8db51be2..a6b42b9541 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt @@ -37,6 +37,7 @@ import net.corda.core.node.services.vault.PageSpecification import net.corda.core.node.services.vault.QueryCriteria import net.corda.core.node.services.vault.Sort import net.corda.core.serialization.CordaSerializable +import net.corda.core.serialization.DeprecatedConstructorForDeserialization import net.corda.core.toFuture import net.corda.core.transactions.LedgerTransaction import net.corda.core.utilities.NonEmptySet @@ -81,6 +82,7 @@ class Vault(val states: Iterable>) { val references: Set> = emptySet(), val consumingTxIds: Map = emptyMap() ) { + @DeprecatedConstructorForDeserialization(1) @JvmOverloads constructor( consumed: Set>, produced: Set>, flowId: UUID? = null, @@ -151,7 +153,7 @@ class Vault(val states: Iterable>) { flowId: UUID? = null, type: UpdateType = UpdateType.GENERAL ): Update { - return Update(consumed, produced, flowId, type, references) + return Update(consumed, produced, flowId, type, references, consumingTxIds) } /** Additional copy method to maintain backwards compatibility. */ diff --git a/node/src/integration-test/kotlin/net/corda/node/CashIssueAndPaymentTest.kt b/node/src/integration-test/kotlin/net/corda/node/CashIssueAndPaymentTest.kt index 2da38e1509..59d4ddbdb7 100644 --- a/node/src/integration-test/kotlin/net/corda/node/CashIssueAndPaymentTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/CashIssueAndPaymentTest.kt @@ -1,10 +1,16 @@ package net.corda.node import net.corda.core.messaging.startFlow +import net.corda.core.messaging.vaultTrackBy +import net.corda.core.node.services.Vault +import net.corda.core.node.services.vault.DEFAULT_PAGE_NUM +import net.corda.core.node.services.vault.PageSpecification +import net.corda.core.node.services.vault.QueryCriteria import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.loggerFor import net.corda.finance.DOLLARS +import net.corda.finance.contracts.asset.Cash import net.corda.finance.flows.CashIssueAndPaymentFlow import net.corda.node.services.config.NodeConfiguration import net.corda.testing.core.ALICE_NAME @@ -17,6 +23,8 @@ import net.corda.testing.node.NotarySpec import net.corda.testing.node.internal.findCordapp import org.junit.Test import org.junit.jupiter.api.assertDoesNotThrow +import java.util.concurrent.CountDownLatch +import kotlin.test.assertEquals /** * Execute a flow with sub-flows, including the finality flow. @@ -65,4 +73,36 @@ class CashIssueAndPaymentTest { logger.info("TXN={}, recipient={}", result.stx, result.recipient) } } + + @Test(timeout = 300_000) + fun `test can issue cash and see consumming transaction id in rpc client`() { + driver(parametersFor()) { + val alice = startNode(providedName = ALICE_NAME, customOverrides = configOverrides).getOrThrow() + val aliceParty = alice.nodeInfo.singleIdentity() + val notaryParty = notaryHandles.single().identity + val result = assertDoesNotThrow { + + val criteria = QueryCriteria.VaultQueryCriteria(Vault.StateStatus.CONSUMED) + val (vault, vaultUpdates) = alice.rpc.vaultTrackBy(criteria = criteria, paging = PageSpecification(DEFAULT_PAGE_NUM)) + val updateLatch = CountDownLatch(1) + vaultUpdates.subscribe { update -> + val consumedRef = update.consumed.single().ref + assertEquals( update.produced.single().ref.txhash, update.consumingTxIds[consumedRef] ) + updateLatch.countDown() + } + val flowRet = alice.rpc.startFlow(::CashIssueAndPaymentFlow, + CASH_AMOUNT, + OpaqueBytes.of(0x01), + aliceParty, + false, + notaryParty + ).use { flowHandle -> + flowHandle.returnValue.getOrThrow() + } + updateLatch.await() + flowRet + } + logger.info("TXN={}, recipient={}", result.stx, result.recipient) + } + } } From 3465917a936e4d2cf3d20d8c42976554cd6eb912 Mon Sep 17 00:00:00 2001 From: Adel El-Beik Date: Tue, 1 Aug 2023 15:48:34 +0100 Subject: [PATCH 049/126] ENT-10122: Fixed detekt issue. --- .../kotlin/net/corda/node/CashIssueAndPaymentTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node/src/integration-test/kotlin/net/corda/node/CashIssueAndPaymentTest.kt b/node/src/integration-test/kotlin/net/corda/node/CashIssueAndPaymentTest.kt index 59d4ddbdb7..93b08b7842 100644 --- a/node/src/integration-test/kotlin/net/corda/node/CashIssueAndPaymentTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/CashIssueAndPaymentTest.kt @@ -83,7 +83,7 @@ class CashIssueAndPaymentTest { val result = assertDoesNotThrow { val criteria = QueryCriteria.VaultQueryCriteria(Vault.StateStatus.CONSUMED) - val (vault, vaultUpdates) = alice.rpc.vaultTrackBy(criteria = criteria, paging = PageSpecification(DEFAULT_PAGE_NUM)) + val (_, vaultUpdates) = alice.rpc.vaultTrackBy(criteria = criteria, paging = PageSpecification(DEFAULT_PAGE_NUM)) val updateLatch = CountDownLatch(1) vaultUpdates.subscribe { update -> val consumedRef = update.consumed.single().ref From f543e476522cc54bfc4911c0cc0dc731495ca77d Mon Sep 17 00:00:00 2001 From: Adel El-Beik Date: Wed, 2 Aug 2023 13:39:04 +0100 Subject: [PATCH 050/126] ENT-10371: Fix unit test failure in CordaServiceLifecycleTests. --- .../net/corda/node/services/CordaServiceLifecycleTests.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node/src/integration-test/kotlin/net/corda/node/services/CordaServiceLifecycleTests.kt b/node/src/integration-test/kotlin/net/corda/node/services/CordaServiceLifecycleTests.kt index e615e91a60..93e0f7e10f 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/CordaServiceLifecycleTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/CordaServiceLifecycleTests.kt @@ -156,6 +156,6 @@ class CordaServiceLifecycleTests { /** * Given an event, was the SMM started when the event was received? */ - fun getSmmStartedForEvent(event: ServiceLifecycleEvent) : Int = smmStateAtEvent.getOrDefault(event, STATE_MACHINE_MANAGER_UNKNOWN_STATUS) + fun getSmmStartedForEvent(event: ServiceLifecycleEvent) : Int = smmStateAtEvent[event] ?: checkSmmStarted() } } \ No newline at end of file From 32af6f5c2d3753f52ab109278ab8b169381c15a5 Mon Sep 17 00:00:00 2001 From: Shams Asari Date: Wed, 9 Aug 2023 08:43:21 +0100 Subject: [PATCH 051/126] ENT-10416: Rename ledger recovery tx_id columns to transaction_id (#7444) This is so that the node archiving service, which scans for tables containing "transaction_id" column, can automatically archive the sender and receiver distribution record information with the transaction. --- .../kotlin/net/corda/coretests/flows/FinalityFlowTests.kt | 6 +++--- .../net/corda/node/flows/FinalityFlowErrorHandlingTest.kt | 2 +- .../persistence/DBTransactionStorageLedgerRecovery.kt | 4 ++-- .../main/resources/migration/node-core.changelog-v25.xml | 4 ++-- .../persistence/DBTransactionStorageLedgerRecoveryTests.kt | 6 +++--- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt b/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt index c120a1b620..fefbac09c1 100644 --- a/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt +++ b/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt @@ -239,7 +239,7 @@ class FinalityFlowTests : WithFinality { private fun assertTxnRemovedFromDatabase(node: TestStartedNode, stxId: SecureHash) { val fromDb = node.database.transaction { session.createQuery( - "from ${DBTransactionStorage.DBTransaction::class.java.name} where tx_id = :transactionId", + "from ${DBTransactionStorage.DBTransaction::class.java.name} where txId = :transactionId", DBTransactionStorage.DBTransaction::class.java ).setParameter("transactionId", stxId.toString()).resultList.map { it } } @@ -444,7 +444,7 @@ class FinalityFlowTests : WithFinality { private fun getSenderRecoveryData(id: SecureHash, database: CordaPersistence): List { val fromDb = database.transaction { session.createQuery( - "from ${DBTransactionStorageLedgerRecovery.DBSenderDistributionRecord::class.java.name} where tx_id = :transactionId", + "from ${DBTransactionStorageLedgerRecovery.DBSenderDistributionRecord::class.java.name} where txId = :transactionId", DBTransactionStorageLedgerRecovery.DBSenderDistributionRecord::class.java ).setParameter("transactionId", id.toString()).resultList.map { it } } @@ -454,7 +454,7 @@ class FinalityFlowTests : WithFinality { private fun getReceiverRecoveryData(id: SecureHash, database: CordaPersistence): ReceiverDistributionRecord? { val fromDb = database.transaction { session.createQuery( - "from ${DBTransactionStorageLedgerRecovery.DBReceiverDistributionRecord::class.java.name} where tx_id = :transactionId", + "from ${DBTransactionStorageLedgerRecovery.DBReceiverDistributionRecord::class.java.name} where txId = :transactionId", DBTransactionStorageLedgerRecovery.DBReceiverDistributionRecord::class.java ).setParameter("transactionId", id.toString()).resultList.map { it } } diff --git a/node/src/integration-test-slow/kotlin/net/corda/node/flows/FinalityFlowErrorHandlingTest.kt b/node/src/integration-test-slow/kotlin/net/corda/node/flows/FinalityFlowErrorHandlingTest.kt index 7d800de41a..9617c21fb3 100644 --- a/node/src/integration-test-slow/kotlin/net/corda/node/flows/FinalityFlowErrorHandlingTest.kt +++ b/node/src/integration-test-slow/kotlin/net/corda/node/flows/FinalityFlowErrorHandlingTest.kt @@ -94,7 +94,7 @@ class GetFlowTransaction(private val txId: SecureHash) : FlowLogic ps.executeQuery().use { rs -> diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecovery.kt b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecovery.kt index 0d00344742..8548dc0d80 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecovery.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecovery.kt @@ -53,7 +53,7 @@ class DBTransactionStorageLedgerRecovery(private val database: CordaPersistence, @EmbeddedId var compositeKey: PersistentKey, - @Column(name = "tx_id", length = 144, nullable = false) + @Column(name = "transaction_id", length = 144, nullable = false) var txId: String, /** PartyId of flow peer **/ @@ -80,7 +80,7 @@ class DBTransactionStorageLedgerRecovery(private val database: CordaPersistence, @EmbeddedId var compositeKey: PersistentKey, - @Column(name = "tx_id", length = 144, nullable = false) + @Column(name = "transaction_id", length = 144, nullable = false) var txId: String, /** PartyId of flow initiator **/ diff --git a/node/src/main/resources/migration/node-core.changelog-v25.xml b/node/src/main/resources/migration/node-core.changelog-v25.xml index d926460ba3..0a81edd870 100644 --- a/node/src/main/resources/migration/node-core.changelog-v25.xml +++ b/node/src/main/resources/migration/node-core.changelog-v25.xml @@ -18,7 +18,7 @@ - + @@ -51,7 +51,7 @@ - + diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecoveryTests.kt b/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecoveryTests.kt index 5f52c0849f..2bc406601c 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecoveryTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecoveryTests.kt @@ -286,7 +286,7 @@ class DBTransactionStorageLedgerRecoveryTests { private fun readTransactionFromDB(id: SecureHash): DBTransactionStorage.DBTransaction { val fromDb = database.transaction { session.createQuery( - "from ${DBTransactionStorage.DBTransaction::class.java.name} where tx_id = :transactionId", + "from ${DBTransactionStorage.DBTransaction::class.java.name} where txId = :transactionId", DBTransactionStorage.DBTransaction::class.java ).setParameter("transactionId", id.toString()).resultList.map { it } } @@ -298,7 +298,7 @@ class DBTransactionStorageLedgerRecoveryTests { return database.transaction { if (id != null) session.createQuery( - "from ${DBTransactionStorageLedgerRecovery.DBSenderDistributionRecord::class.java.name} where tx_id = :transactionId", + "from ${DBTransactionStorageLedgerRecovery.DBSenderDistributionRecord::class.java.name} where txId = :transactionId", DBTransactionStorageLedgerRecovery.DBSenderDistributionRecord::class.java ).setParameter("transactionId", id.toString()).resultList.map { it.toSenderDistributionRecord() } else @@ -312,7 +312,7 @@ class DBTransactionStorageLedgerRecoveryTests { private fun readReceiverDistributionRecordFromDB(id: SecureHash): ReceiverDistributionRecord { val fromDb = database.transaction { session.createQuery( - "from ${DBTransactionStorageLedgerRecovery.DBReceiverDistributionRecord::class.java.name} where tx_id = :transactionId", + "from ${DBTransactionStorageLedgerRecovery.DBReceiverDistributionRecord::class.java.name} where txId = :transactionId", DBTransactionStorageLedgerRecovery.DBReceiverDistributionRecord::class.java ).setParameter("transactionId", id.toString()).resultList.map { it } } From e0e4f51ba2a0fdaff84393b8f9d4a1fa99dfd3f8 Mon Sep 17 00:00:00 2001 From: Shams Asari Date: Wed, 9 Aug 2023 08:44:32 +0100 Subject: [PATCH 052/126] ENT-10285: Remove experimental DJVM support (#7442) --- .ci/api-current.txt | 38 +-- .github/CODEOWNERS | 8 - build.gradle | 37 --- constants.properties | 2 - core-deterministic/README.md | 2 - core-deterministic/build.gradle | 249 --------------- .../crypto/DelegatingSecureRandomService.kt | 19 -- .../net/corda/core/crypto/DigestSupplier.kt | 10 - .../core/crypto/internal/PublicKeyCache.kt | 21 -- .../core/internal/ContractStateClassCache.kt | 16 - .../net/corda/core/internal/LazyPool.kt | 41 --- .../net/corda/core/internal/ToggleField.kt | 62 ---- .../rules/TargetVersionDependentRules.kt | 12 - .../internal/utilities/PrivateInterner.kt | 16 - .../serialization/SerializationFactory.kt | 99 ------ .../internal/AttachmentsHolderImpl.kt | 23 -- .../src/main/resources/META-INF/DJVM-preload | 0 core-deterministic/testing/build.gradle | 29 -- core-deterministic/testing/data/build.gradle | 44 --- .../corda/deterministic/data/GenerateData.kt | 92 ------ .../deterministic/data/KeyStoreGenerator.kt | 50 --- .../data/TransactionGenerator.kt | 115 ------- .../CheatingSecurityProvider.java | 61 ---- .../corda/core/internal/ClassLoadingUtils.kt | 9 - .../corda/deterministic/CordaExceptionTest.kt | 70 ---- .../corda/deterministic/KeyStoreProvider.kt | 44 --- .../net/corda/deterministic/Utilities.kt | 14 - .../deterministic/contracts/AttachmentTest.kt | 79 ----- .../contracts/PrivacySaltTest.kt | 28 -- .../contracts/UniqueIdentifierTest.kt | 37 --- .../deterministic/crypto/CryptoSignUtils.kt | 68 ---- .../deterministic/crypto/MerkleTreeTest.kt | 50 --- .../deterministic/crypto/SecureHashTest.kt | 42 --- .../deterministic/crypto/SecureRandomTest.kt | 22 -- .../crypto/TransactionSignatureTest.kt | 143 --------- .../TransactionWithSignaturesTest.kt | 30 -- .../txverify/VerifyTransactionTest.kt | 29 -- .../src/test/resources/log4j2-test.xml | 14 - .../testing/verifier/build.gradle | 56 ---- .../verifier/LocalSerializationRule.kt | 82 ----- .../verifier/MockContractAttachment.kt | 19 -- .../deterministic/verifier/SampleData.kt | 6 - .../TransactionVerificationRequest.kt | 35 -- .../corda/deterministic/verifier/Verifier.kt | 21 -- core/build.gradle | 1 - .../java/net/corda/core/crypto/Base58.java | 3 - .../core/flows/IdentifiableException.java | 3 - .../net/corda/core/ClientRelevantError.kt | 1 - .../kotlin/net/corda/core/CordaException.kt | 3 - .../main/kotlin/net/corda/core/CordaOID.kt | 1 - .../kotlin/net/corda/core/DeleteForDJVM.kt | 24 -- .../main/kotlin/net/corda/core/KeepForDJVM.kt | 18 -- .../kotlin/net/corda/core/StubOutForDJVM.kt | 22 -- .../corda/core/context/InvocationContext.kt | 14 +- .../kotlin/net/corda/core/context/Trace.kt | 6 +- .../kotlin/net/corda/core/contracts/Amount.kt | 2 - .../net/corda/core/contracts/Attachment.kt | 2 - .../core/contracts/AttachmentConstraint.kt | 7 - .../core/contracts/ContractAttachment.kt | 2 - .../net/corda/core/contracts/ContractState.kt | 2 - .../net/corda/core/contracts/ContractsDSL.kt | 3 - .../net/corda/core/contracts/FungibleAsset.kt | 2 - .../net/corda/core/contracts/FungibleState.kt | 3 - .../net/corda/core/contracts/StatePointer.kt | 8 - .../net/corda/core/contracts/Structures.kt | 27 -- .../net/corda/core/contracts/TimeWindow.kt | 4 - .../corda/core/contracts/TransactionState.kt | 2 - .../TransactionVerificationException.kt | 33 -- .../corda/core/contracts/UniqueIdentifier.kt | 5 +- .../kotlin/net/corda/core/cordapp/Cordapp.kt | 2 - .../net/corda/core/cordapp/CordappContext.kt | 2 - .../net/corda/core/cordapp/CordappProvider.kt | 2 - .../net/corda/core/crypto/CompositeKey.kt | 4 - .../corda/core/crypto/CompositeKeyFactory.kt | 2 - .../corda/core/crypto/CompositeSignature.kt | 2 - .../crypto/CompositeSignaturesWithKeys.kt | 2 - .../core/crypto/CordaSecurityProvider.kt | 41 +-- .../kotlin/net/corda/core/crypto/Crypto.kt | 16 - .../net/corda/core/crypto/CryptoUtils.kt | 13 - .../net/corda/core/crypto/DigestAlgorithm.kt | 3 - .../net/corda/core/crypto/DigestService.kt | 5 - .../net/corda/core/crypto/DigitalSignature.kt | 3 - .../net/corda/core/crypto/MerkleTree.kt | 6 +- .../kotlin/net/corda/core/crypto/NullKeys.kt | 2 - .../corda/core/crypto/PartialMerkleTree.kt | 9 +- .../net/corda/core/crypto/SecureHash.kt | 6 - .../net/corda/core/crypto/SignableData.kt | 2 - .../corda/core/crypto/SignatureMetadata.kt | 2 - .../net/corda/core/crypto/SignatureScheme.kt | 2 - .../net/corda/core/crypto/SignedData.kt | 2 - .../corda/core/crypto/TransactionSignature.kt | 2 - .../corda/core/crypto/internal/Instances.kt | 16 +- .../crypto/internal/PlatformSecureRandom.kt | 5 - .../corda/core/crypto/internal/ProviderMap.kt | 2 - .../kotlin/net/corda/core/flows/FlowLogic.kt | 3 - .../net/corda/core/flows/FlowLogicRef.kt | 7 - .../net/corda/core/flows/FlowSession.kt | 2 - .../net/corda/core/flows/StateMachineRunId.kt | 2 - .../net/corda/core/identity/AnonymousParty.kt | 2 - .../net/corda/core/identity/CordaX500Name.kt | 2 - .../kotlin/net/corda/core/identity/Party.kt | 2 - .../core/identity/PartyAndCertificate.kt | 2 - .../corda/core/internal/AbstractAttachment.kt | 8 +- .../corda/core/internal/ClassGraphUtils.kt | 3 - .../corda/core/internal/ClassLoadingUtils.kt | 3 - .../corda/core/internal/ConstraintsUtils.kt | 9 +- .../core/internal/ContractUpgradeUtils.kt | 2 - .../net/corda/core/internal/CordaUtils.kt | 3 - .../core/internal/CordappFixupInternal.kt | 2 - .../net/corda/core/internal/DjvmUtils.kt | 20 -- .../kotlin/net/corda/core/internal/Emoji.kt | 3 - .../net/corda/core/internal/FlowIORequest.kt | 2 - .../corda/core/internal/FlowStateMachine.kt | 3 - .../net/corda/core/internal/InternalUtils.kt | 39 +-- .../net/corda/core/internal/LazyStickyPool.kt | 2 - .../corda/core/internal/LegalNameValidator.kt | 9 - .../net/corda/core/internal/LifeCycle.kt | 2 - .../net/corda/core/internal/PathUtils.kt | 2 - .../core/internal/ResolveTransactionsFlow.kt | 2 - .../core/internal/ServiceHubCoreInternal.kt | 2 - .../net/corda/core/internal/ThreadBox.kt | 2 - .../corda/core/internal/TransactionUtils.kt | 2 - .../TransactionVerifierServiceInternal.kt | 19 +- .../net/corda/core/internal/X500Utils.kt | 3 - .../corda/core/internal/X509EdDSAEngine.kt | 3 +- .../core/internal/cordapp/CordappImpl.kt | 2 - .../core/internal/notary/NotaryService.kt | 2 - .../rules/TargetVersionDependentRules.kt | 2 - .../core/internal/utilities/Internable.kt | 4 - .../net/corda/core/node/AppServiceHub.kt | 2 - .../net/corda/core/node/NetworkParameters.kt | 3 - .../kotlin/net/corda/core/node/ServiceHub.kt | 3 - .../core/node/services/AttachmentStorage.kt | 3 +- .../core/node/services/NetworkMapCache.kt | 3 +- .../core/node/services/TimeWindowChecker.kt | 2 - .../core/node/services/TransactionStorage.kt | 2 - .../services/TransactionVerifierService.kt | 2 - .../corda/core/node/services/VaultService.kt | 2 - .../net/corda/core/schemas/PersistentTypes.kt | 6 - .../MissingAttachmentsException.kt | 2 - .../MissingAttachmentsRuntimeException.kt | 2 - .../core/serialization/SerializationAPI.kt | 18 +- .../SerializationCustomSerializer.kt | 4 - .../core/serialization/SerializationToken.kt | 6 - .../serialization/SerializationWhitelist.kt | 3 - .../internal/AttachmentsClassLoader.kt | 2 - .../internal/CheckpointSerializationAPI.kt | 6 - .../CustomSerializationSchemeUtils.kt | 2 - .../internal/MissingSerializerException.kt | 2 - .../internal/SerializationEnvironment.kt | 4 - .../core/transactions/BaseTransaction.kt | 2 - .../ContractUpgradeTransactions.kt | 4 - .../core/transactions/LedgerTransaction.kt | 16 +- .../core/transactions/MerkleTransaction.kt | 9 +- .../MissingContractAttachments.kt | 2 - .../transactions/NotaryChangeTransactions.kt | 6 - .../core/transactions/SignedTransaction.kt | 19 +- .../core/transactions/TransactionBuilder.kt | 2 - .../transactions/TransactionWithSignatures.kt | 2 - .../core/transactions/WireTransaction.kt | 25 -- .../net/corda/core/utilities/ByteArrays.kt | 6 - .../net/corda/core/utilities/EncodingUtils.kt | 2 - .../kotlin/net/corda/core/utilities/Id.kt | 2 - .../net/corda/core/utilities/KotlinUtils.kt | 5 - .../net/corda/core/utilities/NonEmptySet.kt | 2 - .../corda/core/utilities/ProgressTracker.kt | 6 - .../net/corda/core/utilities/SgxSupport.kt | 3 - .../kotlin/net/corda/core/utilities/Try.kt | 3 - .../corda/core/utilities/UntrustworthyData.kt | 4 - .../net/corda/core/utilities/UuidGenerator.kt | 2 - detekt-baseline.xml | 5 - .../rules/TestWithMissingTimeoutTest.kt | 5 +- deterministic.gradle | 36 --- jdk8u-deterministic/.gitignore | 1 - jdk8u-deterministic/build.gradle | 49 --- .../internal/serialization/kryo/Kryo.kt | 2 - node/build.gradle | 35 -- node/capsule/build.gradle | 34 +- node/capsule/src/main/java/CordaCaplet.java | 68 ---- node/djvm/build.gradle | 25 -- .../net/corda/node/djvm/AttachmentBuilder.kt | 88 ------ .../net/corda/node/djvm/CommandBuilder.kt | 48 --- .../net/corda/node/djvm/ComponentBuilder.kt | 27 -- .../net/corda/node/djvm/LtxSupplierFactory.kt | 73 ----- .../src/main/resources/META-INF/DJVM-preload | 0 .../attachment/SandboxAttachmentContract.kt | 38 --- .../djvm/broken/NonDeterministicContract.kt | 50 --- .../crypto/DeterministicCryptoContract.kt | 39 --- .../DeterministicWhitelistContract.kt | 38 --- .../contracts/djvm/whitelist/WhitelistData.kt | 15 - .../djvm/attachment/SandboxAttachmentFlow.kt | 25 -- .../flows/djvm/broken/NonDeterministicFlow.kt | 25 -- .../djvm/crypto/DeterministicCryptoFlow.kt | 30 -- .../whitelist/DeterministicWhitelistFlow.kt | 26 -- .../corda/node/DeterministicSourcesRule.kt | 31 -- .../node/services/AttachmentLoadingTests.kt | 2 +- .../DeterministicCashIssueAndPaymentTest.kt | 72 ----- ...sticContractCannotMutateTransactionTest.kt | 55 ---- .../DeterministicContractCryptoTest.kt | 73 ----- ...inisticContractWithCustomSerializerTest.kt | 91 ------ ...eterministicContractWithGenericTypeTest.kt | 85 ----- ...cContractWithSerializationWhitelistTest.kt | 91 ------ ...isticEvilContractCannotModifyStatesTest.kt | 71 ----- .../NonDeterministicContractVerifyTest.kt | 133 -------- .../node/services/SandboxAttachmentsTest.kt | 80 ----- .../identity/CertificateRotationTest.kt | 4 +- .../java/sandbox/java/lang/CharSequence.java | 8 - .../java/sandbox/java/lang/Comparable.java | 8 - .../main/java/sandbox/java/lang/Number.java | 8 - .../java/sandbox/java/math/BigInteger.java | 8 - .../main/java/sandbox/java/security/Key.java | 13 - .../java/sandbox/java/security/KeyPair.java | 8 - .../sandbox/java/security/PrivateKey.java | 8 - .../java/sandbox/java/security/PublicKey.java | 8 - .../security/spec/AlgorithmParameterSpec.java | 8 - .../java/sandbox/java/util/ArrayList.java | 16 - .../src/main/java/sandbox/java/util/List.java | 9 - .../sandbox/net/corda/core/crypto/DJVM.java | 62 ---- .../net/corda/core/crypto/DJVMPublicKey.java | 61 ---- .../org/bouncycastle/asn1/ASN1Encodable.java | 9 - .../org/bouncycastle/asn1/ASN1Object.java | 16 - .../asn1/x509/AlgorithmIdentifier.java | 14 - .../asn1/x509/SubjectPublicKeyInfo.java | 10 - .../net/corda/node/internal/AbstractNode.kt | 23 +- .../kotlin/net/corda/node/internal/Node.kt | 84 ----- .../node/internal/djvm/AttachmentFactory.kt | 49 --- .../node/internal/djvm/CommandFactory.kt | 17 - .../node/internal/djvm/ComponentFactory.kt | 43 --- .../internal/djvm/DeterministicVerifier.kt | 147 --------- .../corda/node/internal/djvm/Serializer.kt | 71 ----- .../node/services/config/NodeConfiguration.kt | 8 +- .../config/schema/v1/ConfigSections.kt | 14 +- .../DeterministicVerifierFactoryService.kt | 117 ------- .../sandbox/net/corda/core/crypto/Crypto.kt | 266 ---------------- .../net/corda/core/crypto/SecureHash.kt | 8 - .../net/corda/core/crypto/SignatureScheme.kt | 26 -- .../corda/core/crypto/TransactionSignature.kt | 7 - .../corda/core/crypto/internal/ProviderMap.kt | 8 - serialization-deterministic/README.md | 2 - serialization-deterministic/build.gradle | 228 -------------- .../internal/ByteBufferStreams.kt | 15 - .../internal/DefaultWhitelist.kt | 10 - .../amqp/AMQPSerializationThreadContext.kt | 6 - .../internal/amqp/AMQPSerializerFactories.kt | 34 -- .../internal/amqp/AMQPStreams.kt | 40 --- .../internal/model/DefaultCacheProvider.kt | 9 - .../src/main/resources/META-INF/DJVM-preload | 0 serialization-djvm/build.gradle | 83 ----- serialization-djvm/deserializers/build.gradle | 40 --- .../djvm/deserializers/BitSetDeserializer.kt | 11 - .../deserializers/CertPathDeserializer.kt | 13 - .../djvm/deserializers/CheckEnum.kt | 9 - .../djvm/deserializers/ClassDeserializer.kt | 11 - .../CorDappCustomDeserializer.kt | 10 - .../djvm/deserializers/CreateCollection.kt | 54 ---- .../djvm/deserializers/CreateCurrency.kt | 10 - .../djvm/deserializers/CreateMap.kt | 54 ---- .../deserializers/Decimal128Deserializer.kt | 10 - .../deserializers/Decimal32Deserializer.kt | 10 - .../deserializers/Decimal64Deserializer.kt | 10 - .../djvm/deserializers/DescribeEnum.kt | 9 - .../deserializers/DurationDeserializer.kt | 11 - .../djvm/deserializers/EnumSetDeserializer.kt | 16 - .../djvm/deserializers/GetEnumNames.kt | 9 - .../deserializers/InputStreamDeserializer.kt | 11 - .../djvm/deserializers/InstantDeserializer.kt | 11 - .../djvm/deserializers/JustForCasting.kt | 6 - .../deserializers/LocalDateDeserializer.kt | 11 - .../LocalDateTimeDeserializer.kt | 11 - .../deserializers/LocalTimeDeserializer.kt | 11 - .../djvm/deserializers/MergeWhitelists.kt | 10 - .../deserializers/MonthDayDeserializer.kt | 11 - .../OffsetDateTimeDeserializer.kt | 11 - .../deserializers/OffsetTimeDeserializer.kt | 11 - .../OpaqueBytesSubSequenceDeserializer.kt | 11 - .../deserializers/OptionalDeserializer.kt | 11 - .../djvm/deserializers/PeriodDeserializer.kt | 11 - .../djvm/deserializers/PublicKeyDecoder.kt | 11 - .../djvm/deserializers/SymbolDeserializer.kt | 10 - .../deserializers/UnsignedByteDeserializer.kt | 10 - .../UnsignedIntegerDeserializer.kt | 10 - .../deserializers/UnsignedLongDeserializer.kt | 10 - .../UnsignedShortDeserializer.kt | 10 - .../djvm/deserializers/X509CRLDeserializer.kt | 12 - .../X509CertificateDeserializer.kt | 12 - .../djvm/deserializers/YearDeserializer.kt | 11 - .../deserializers/YearMonthDeserializer.kt | 11 - .../djvm/deserializers/ZoneIdDeserializer.kt | 11 - .../ZonedDateTimeDeserializer.kt | 10 - .../src/main/resources/META-INF/DJVM-preload | 0 .../djvm/serializers/CacheKey.java | 35 -- .../djvm/AMQPSerializationScheme.kt | 34 -- .../djvm/DelegatingClassLoader.kt | 10 - .../djvm/SandboxSerializationSchemeBuilder.kt | 150 --------- .../djvm/SandboxSerializerFactoryFactory.kt | 116 ------- .../serialization/djvm/SandboxWhitelist.kt | 13 - .../corda/serialization/djvm/Serialization.kt | 117 ------- .../djvm/serializers/ExceptionUtils.kt | 39 --- .../djvm/serializers/PrimitiveSerializer.kt | 36 --- .../serializers/SandboxBitSetSerializer.kt | 30 -- .../serializers/SandboxCertPathSerializer.kt | 41 --- .../serializers/SandboxCharacterSerializer.kt | 37 --- .../serializers/SandboxClassSerializer.kt | 47 --- .../SandboxCollectionSerializer.kt | 129 -------- .../SandboxCorDappCustomSerializer.kt | 93 ------ .../serializers/SandboxCurrencySerializer.kt | 40 --- .../SandboxDecimal128Serializer.kt | 35 -- .../serializers/SandboxDecimal32Serializer.kt | 34 -- .../serializers/SandboxDecimal64Serializer.kt | 34 -- .../serializers/SandboxDurationSerializer.kt | 30 -- .../djvm/serializers/SandboxEnumSerializer.kt | 121 ------- .../serializers/SandboxEnumSetSerializer.kt | 35 -- .../SandboxInputStreamSerializer.kt | 37 --- .../serializers/SandboxInstantSerializer.kt | 30 -- .../serializers/SandboxLocalDateSerializer.kt | 30 -- .../SandboxLocalDateTimeSerializer.kt | 30 -- .../serializers/SandboxLocalTimeSerializer.kt | 30 -- .../djvm/serializers/SandboxMapSerializer.kt | 124 -------- .../serializers/SandboxMonthDaySerializer.kt | 30 -- .../SandboxOffsetDateTimeSerializer.kt | 30 -- .../SandboxOffsetTimeSerializer.kt | 30 -- ...SandboxOpaqueBytesSubSequenceSerializer.kt | 30 -- .../serializers/SandboxOptionalSerializer.kt | 30 -- .../serializers/SandboxPeriodSerializer.kt | 30 -- .../serializers/SandboxPrimitiveSerializer.kt | 30 -- .../serializers/SandboxPublicKeySerializer.kt | 41 --- .../serializers/SandboxSymbolSerializer.kt | 39 --- .../serializers/SandboxToStringSerializer.kt | 39 --- .../SandboxUnsignedByteSerializer.kt | 34 -- .../SandboxUnsignedIntegerSerializer.kt | 34 -- .../SandboxUnsignedLongSerializer.kt | 34 -- .../SandboxUnsignedShortSerializer.kt | 34 -- .../serializers/SandboxX509CRLSerializer.kt | 42 --- .../SandboxX509CertificateSerializer.kt | 42 --- .../serializers/SandboxYearMonthSerializer.kt | 30 -- .../djvm/serializers/SandboxYearSerializer.kt | 30 -- .../serializers/SandboxZoneIdSerializer.kt | 32 -- .../SandboxZonedDateTimeSerializer.kt | 47 --- .../djvm/serializers/Serializers.kt | 8 - .../test/java/greymalkin/ExternalData.java | 13 - .../test/java/greymalkin/ExternalEnum.java | 11 - .../serialization/djvm/InnocentData.java | 22 -- .../djvm/MultiConstructorData.java | 53 ---- .../serialization/djvm/VeryEvilData.java | 15 - .../corda/serialization/djvm/Assertions.kt | 14 - .../djvm/DeserializeBigDecimalTest.kt | 49 --- .../djvm/DeserializeBigIntegerTest.kt | 49 --- .../djvm/DeserializeBitSetTest.kt | 39 --- .../djvm/DeserializeCertificatesTest.kt | 134 -------- .../djvm/DeserializeClassTest.kt | 59 ---- .../djvm/DeserializeCollectionsTest.kt | 215 ------------- .../djvm/DeserializeComposedCustomDataTest.kt | 119 ------- .../djvm/DeserializeCurrencyTest.kt | 45 --- .../djvm/DeserializeCustomGenericDataTest.kt | 97 ------ .../djvm/DeserializeCustomisedEnumTest.kt | 60 ---- .../djvm/DeserializeDurationTest.kt | 39 --- .../djvm/DeserializeEnumSetTest.kt | 43 --- .../serialization/djvm/DeserializeEnumTest.kt | 55 ---- .../djvm/DeserializeEnumWithEvolutionTest.kt | 155 --------- .../djvm/DeserializeGenericsTest.kt | 171 ---------- .../djvm/DeserializeInputStreamTest.kt | 43 --- .../djvm/DeserializeInstantTest.kt | 39 --- ...rializeJavaWithMultipleConstructorsTest.kt | 39 --- .../djvm/DeserializeKotlinAliasTest.kt | 69 ---- .../djvm/DeserializeLocalDateTest.kt | 39 --- .../djvm/DeserializeLocalDateTimeTest.kt | 39 --- .../djvm/DeserializeLocalTimeTest.kt | 39 --- .../serialization/djvm/DeserializeMapsTest.kt | 203 ------------ .../djvm/DeserializeMonthDayTest.kt | 39 --- .../djvm/DeserializeObjectArraysTest.kt | 298 ------------------ .../djvm/DeserializeOffsetDateTimeTest.kt | 39 --- .../djvm/DeserializeOffsetTimeTest.kt | 39 --- .../DeserializeOpaqueBytesSubSequenceTest.kt | 49 --- .../djvm/DeserializeOptionalTest.kt | 58 ---- .../djvm/DeserializePeriodTest.kt | 39 --- .../djvm/DeserializePrimitiveArraysTest.kt | 239 -------------- .../djvm/DeserializePrimitivesTest.kt | 76 ----- .../djvm/DeserializeProtonJTest.kt | 245 -------------- .../djvm/DeserializePublicKeyTest.kt | 104 ------ .../DeserializeRemoteCustomisedEnumTest.kt | 144 --------- .../djvm/DeserializeStringBufferTest.kt | 38 --- .../djvm/DeserializeStringTest.kt | 71 ----- .../DeserializeWithCustomSerializerTest.kt | 88 ------ ...serializeWithObjectCustomSerializerTest.kt | 75 ----- ...serializeWithSerializationWhitelistTest.kt | 81 ----- .../djvm/DeserializeYearMonthTest.kt | 39 --- .../serialization/djvm/DeserializeYearTest.kt | 39 --- .../djvm/DeserializeZoneIdTest.kt | 51 --- .../djvm/DeserializeZonedDateTimeTest.kt | 39 --- .../serialization/djvm/LocalSerialization.kt | 73 ----- .../serialization/djvm/LocalTypeModelTest.kt | 210 ------------ .../djvm/SafeDeserialisationTest.kt | 65 ---- .../corda/serialization/djvm/SandboxType.kt | 6 - .../net/corda/serialization/djvm/TestBase.kt | 124 -------- .../corda/serialization/djvm/TestHelpers.kt | 28 -- .../src/test/resources/log4j2-test.xml | 17 - .../src/test/resources/testing.cert | Bin 801 -> 0 bytes .../src/test/scripts/generate-certificate.sh | 11 - serialization/build.gradle | 1 - .../internal/amqp/custom/CacheKey.java | 2 - .../internal/AllButBlacklisted.kt | 2 - .../internal/ByteBufferStreams.kt | 5 - .../internal/CheckpointSerializationScheme.kt | 2 - .../serialization/internal/ClassWhitelists.kt | 6 - .../serialization/internal/ClientContexts.kt | 2 - .../internal/GeneratedAttachment.kt | 2 - .../corda/serialization/internal/OrdinalIO.kt | 4 - .../internal/SerializationFormat.kt | 4 - .../internal/SerializationScheme.kt | 6 - .../internal/SerializeAsTokenContextImpl.kt | 5 +- .../serialization/internal/ServerContexts.kt | 2 - .../serialization/internal/SharedContexts.kt | 4 - .../internal/UseCaseAwareness.kt | 2 - .../internal/amqp/AMQPRemoteTypeModel.kt | 3 +- .../internal/amqp/AMQPSerializationScheme.kt | 29 +- .../internal/amqp/AMQPSerializer.kt | 2 - .../internal/amqp/AMQPStreams.kt | 2 - .../internal/amqp/AccessOrderLinkedHashMap.kt | 2 - .../internal/amqp/ArraySerializer.kt | 2 - .../internal/amqp/CollectionSerializer.kt | 2 - .../internal/amqp/CustomSerializer.kt | 13 - .../internal/amqp/CustomSerializerRegistry.kt | 12 +- .../amqp/DescriptorBasedSerializerRegistry.kt | 6 +- .../internal/amqp/DeserializationInput.kt | 2 - .../serialization/internal/amqp/Envelope.kt | 2 - .../amqp/EvolutionSerializerFactory.kt | 2 - .../internal/amqp/LocalSerializerFactory.kt | 7 +- .../internal/amqp/MapSerializer.kt | 8 - .../internal/amqp/PropertyDescriptor.kt | 2 - .../internal/amqp/RemoteSerializerFactory.kt | 24 +- .../serialization/internal/amqp/Schema.kt | 8 - .../internal/amqp/SerializationOutput.kt | 3 - .../internal/amqp/SerializerFactory.kt | 3 - .../internal/amqp/SerializerFactoryBuilder.kt | 6 - .../internal/amqp/SupportedTransforms.kt | 2 - .../internal/amqp/TransformTypes.kt | 2 - .../internal/amqp/TransformsSchema.kt | 4 - .../internal/amqp/custom/BitSetSerializer.kt | 2 - .../amqp/custom/CertPathSerializer.kt | 2 - .../internal/amqp/custom/ClassSerializer.kt | 2 - .../custom/ContractAttachmentSerializer.kt | 2 - .../amqp/custom/DurationSerializer.kt | 2 - .../internal/amqp/custom/EnumSetSerializer.kt | 2 - .../internal/amqp/custom/InstantSerializer.kt | 2 - .../amqp/custom/LocalDateSerializer.kt | 2 - .../amqp/custom/LocalDateTimeSerializer.kt | 2 - .../amqp/custom/LocalTimeSerializer.kt | 4 +- .../amqp/custom/MonthDaySerializer.kt | 4 +- .../amqp/custom/OffsetDateTimeSerializer.kt | 4 +- .../amqp/custom/OffsetTimeSerializer.kt | 4 +- .../amqp/custom/OptionalSerializer.kt | 4 +- .../internal/amqp/custom/PeriodSerializer.kt | 4 +- .../amqp/custom/PrivateKeySerializer.kt | 2 - .../amqp/custom/SimpleStringSerializer.kt | 2 - .../amqp/custom/ThrowableSerializer.kt | 3 - .../amqp/custom/YearMonthSerializer.kt | 2 - .../internal/amqp/custom/YearSerializer.kt | 2 - .../internal/amqp/custom/ZoneIdSerializer.kt | 2 - .../amqp/custom/ZonedDateTimeSerializer.kt | 2 - .../internal/carpenter/ClassCarpenter.kt | 7 +- .../internal/carpenter/Exceptions.kt | 2 - .../internal/carpenter/Schema.kt | 10 - .../internal/carpenter/SchemaFields.kt | 9 +- .../internal/model/DefaultCacheProvider.kt | 11 - .../internal/model/LocalTypeModel.kt | 5 +- .../internal/model/TypeIdentifier.kt | 6 +- .../internal/model/TypeLoader.kt | 5 +- .../model/TypeModellingFingerPrinter.kt | 3 +- serialization/src/test/README.md | 7 - settings.gradle | 10 - .../kotlin/net/corda/testing/driver/Driver.kt | 31 -- .../testing/node/internal/DriverDSLImpl.kt | 32 +- .../corda/testing/node/internal/RPCDriver.kt | 4 - .../corda/demobench/model/NodeController.kt | 17 - .../net/corda/demobench/views/NodeTabView.kt | 3 - .../demobench/model/NodeControllerTest.kt | 3 +- 476 files changed, 100 insertions(+), 13327 deletions(-) delete mode 100644 core-deterministic/README.md delete mode 100644 core-deterministic/build.gradle delete mode 100644 core-deterministic/src/main/kotlin/net/corda/core/crypto/DelegatingSecureRandomService.kt delete mode 100644 core-deterministic/src/main/kotlin/net/corda/core/crypto/DigestSupplier.kt delete mode 100644 core-deterministic/src/main/kotlin/net/corda/core/crypto/internal/PublicKeyCache.kt delete mode 100644 core-deterministic/src/main/kotlin/net/corda/core/internal/ContractStateClassCache.kt delete mode 100644 core-deterministic/src/main/kotlin/net/corda/core/internal/LazyPool.kt delete mode 100644 core-deterministic/src/main/kotlin/net/corda/core/internal/ToggleField.kt delete mode 100644 core-deterministic/src/main/kotlin/net/corda/core/internal/rules/TargetVersionDependentRules.kt delete mode 100644 core-deterministic/src/main/kotlin/net/corda/core/internal/utilities/PrivateInterner.kt delete mode 100644 core-deterministic/src/main/kotlin/net/corda/core/serialization/SerializationFactory.kt delete mode 100644 core-deterministic/src/main/kotlin/net/corda/core/serialization/internal/AttachmentsHolderImpl.kt delete mode 100644 core-deterministic/src/main/resources/META-INF/DJVM-preload delete mode 100644 core-deterministic/testing/build.gradle delete mode 100644 core-deterministic/testing/data/build.gradle delete mode 100644 core-deterministic/testing/data/src/test/kotlin/net/corda/deterministic/data/GenerateData.kt delete mode 100644 core-deterministic/testing/data/src/test/kotlin/net/corda/deterministic/data/KeyStoreGenerator.kt delete mode 100644 core-deterministic/testing/data/src/test/kotlin/net/corda/deterministic/data/TransactionGenerator.kt delete mode 100644 core-deterministic/testing/src/test/java/net/corda/deterministic/CheatingSecurityProvider.java delete mode 100644 core-deterministic/testing/src/test/kotlin/net/corda/core/internal/ClassLoadingUtils.kt delete mode 100644 core-deterministic/testing/src/test/kotlin/net/corda/deterministic/CordaExceptionTest.kt delete mode 100644 core-deterministic/testing/src/test/kotlin/net/corda/deterministic/KeyStoreProvider.kt delete mode 100644 core-deterministic/testing/src/test/kotlin/net/corda/deterministic/Utilities.kt delete mode 100644 core-deterministic/testing/src/test/kotlin/net/corda/deterministic/contracts/AttachmentTest.kt delete mode 100644 core-deterministic/testing/src/test/kotlin/net/corda/deterministic/contracts/PrivacySaltTest.kt delete mode 100644 core-deterministic/testing/src/test/kotlin/net/corda/deterministic/contracts/UniqueIdentifierTest.kt delete mode 100644 core-deterministic/testing/src/test/kotlin/net/corda/deterministic/crypto/CryptoSignUtils.kt delete mode 100644 core-deterministic/testing/src/test/kotlin/net/corda/deterministic/crypto/MerkleTreeTest.kt delete mode 100644 core-deterministic/testing/src/test/kotlin/net/corda/deterministic/crypto/SecureHashTest.kt delete mode 100644 core-deterministic/testing/src/test/kotlin/net/corda/deterministic/crypto/SecureRandomTest.kt delete mode 100644 core-deterministic/testing/src/test/kotlin/net/corda/deterministic/crypto/TransactionSignatureTest.kt delete mode 100644 core-deterministic/testing/src/test/kotlin/net/corda/deterministic/transactions/TransactionWithSignaturesTest.kt delete mode 100644 core-deterministic/testing/src/test/kotlin/net/corda/deterministic/txverify/VerifyTransactionTest.kt delete mode 100644 core-deterministic/testing/src/test/resources/log4j2-test.xml delete mode 100644 core-deterministic/testing/verifier/build.gradle delete mode 100644 core-deterministic/testing/verifier/src/main/kotlin/net/corda/deterministic/verifier/LocalSerializationRule.kt delete mode 100644 core-deterministic/testing/verifier/src/main/kotlin/net/corda/deterministic/verifier/MockContractAttachment.kt delete mode 100644 core-deterministic/testing/verifier/src/main/kotlin/net/corda/deterministic/verifier/SampleData.kt delete mode 100644 core-deterministic/testing/verifier/src/main/kotlin/net/corda/deterministic/verifier/TransactionVerificationRequest.kt delete mode 100644 core-deterministic/testing/verifier/src/main/kotlin/net/corda/deterministic/verifier/Verifier.kt delete mode 100644 core/src/main/kotlin/net/corda/core/DeleteForDJVM.kt delete mode 100644 core/src/main/kotlin/net/corda/core/KeepForDJVM.kt delete mode 100644 core/src/main/kotlin/net/corda/core/StubOutForDJVM.kt delete mode 100644 core/src/main/kotlin/net/corda/core/internal/DjvmUtils.kt delete mode 100644 deterministic.gradle delete mode 100644 jdk8u-deterministic/.gitignore delete mode 100644 jdk8u-deterministic/build.gradle delete mode 100644 node/djvm/build.gradle delete mode 100644 node/djvm/src/main/kotlin/net/corda/node/djvm/AttachmentBuilder.kt delete mode 100644 node/djvm/src/main/kotlin/net/corda/node/djvm/CommandBuilder.kt delete mode 100644 node/djvm/src/main/kotlin/net/corda/node/djvm/ComponentBuilder.kt delete mode 100644 node/djvm/src/main/kotlin/net/corda/node/djvm/LtxSupplierFactory.kt delete mode 100644 node/djvm/src/main/resources/META-INF/DJVM-preload delete mode 100644 node/src/integration-test/kotlin/net/corda/contracts/djvm/attachment/SandboxAttachmentContract.kt delete mode 100644 node/src/integration-test/kotlin/net/corda/contracts/djvm/broken/NonDeterministicContract.kt delete mode 100644 node/src/integration-test/kotlin/net/corda/contracts/djvm/crypto/DeterministicCryptoContract.kt delete mode 100644 node/src/integration-test/kotlin/net/corda/contracts/djvm/whitelist/DeterministicWhitelistContract.kt delete mode 100644 node/src/integration-test/kotlin/net/corda/contracts/djvm/whitelist/WhitelistData.kt delete mode 100644 node/src/integration-test/kotlin/net/corda/flows/djvm/attachment/SandboxAttachmentFlow.kt delete mode 100644 node/src/integration-test/kotlin/net/corda/flows/djvm/broken/NonDeterministicFlow.kt delete mode 100644 node/src/integration-test/kotlin/net/corda/flows/djvm/crypto/DeterministicCryptoFlow.kt delete mode 100644 node/src/integration-test/kotlin/net/corda/flows/djvm/whitelist/DeterministicWhitelistFlow.kt delete mode 100644 node/src/integration-test/kotlin/net/corda/node/DeterministicSourcesRule.kt delete mode 100644 node/src/integration-test/kotlin/net/corda/node/services/DeterministicCashIssueAndPaymentTest.kt delete mode 100644 node/src/integration-test/kotlin/net/corda/node/services/DeterministicContractCannotMutateTransactionTest.kt delete mode 100644 node/src/integration-test/kotlin/net/corda/node/services/DeterministicContractCryptoTest.kt delete mode 100644 node/src/integration-test/kotlin/net/corda/node/services/DeterministicContractWithCustomSerializerTest.kt delete mode 100644 node/src/integration-test/kotlin/net/corda/node/services/DeterministicContractWithGenericTypeTest.kt delete mode 100644 node/src/integration-test/kotlin/net/corda/node/services/DeterministicContractWithSerializationWhitelistTest.kt delete mode 100644 node/src/integration-test/kotlin/net/corda/node/services/DeterministicEvilContractCannotModifyStatesTest.kt delete mode 100644 node/src/integration-test/kotlin/net/corda/node/services/NonDeterministicContractVerifyTest.kt delete mode 100644 node/src/integration-test/kotlin/net/corda/node/services/SandboxAttachmentsTest.kt delete mode 100644 node/src/main/java/sandbox/java/lang/CharSequence.java delete mode 100644 node/src/main/java/sandbox/java/lang/Comparable.java delete mode 100644 node/src/main/java/sandbox/java/lang/Number.java delete mode 100644 node/src/main/java/sandbox/java/math/BigInteger.java delete mode 100644 node/src/main/java/sandbox/java/security/Key.java delete mode 100644 node/src/main/java/sandbox/java/security/KeyPair.java delete mode 100644 node/src/main/java/sandbox/java/security/PrivateKey.java delete mode 100644 node/src/main/java/sandbox/java/security/PublicKey.java delete mode 100644 node/src/main/java/sandbox/java/security/spec/AlgorithmParameterSpec.java delete mode 100644 node/src/main/java/sandbox/java/util/ArrayList.java delete mode 100644 node/src/main/java/sandbox/java/util/List.java delete mode 100644 node/src/main/java/sandbox/net/corda/core/crypto/DJVM.java delete mode 100644 node/src/main/java/sandbox/net/corda/core/crypto/DJVMPublicKey.java delete mode 100644 node/src/main/java/sandbox/org/bouncycastle/asn1/ASN1Encodable.java delete mode 100644 node/src/main/java/sandbox/org/bouncycastle/asn1/ASN1Object.java delete mode 100644 node/src/main/java/sandbox/org/bouncycastle/asn1/x509/AlgorithmIdentifier.java delete mode 100644 node/src/main/java/sandbox/org/bouncycastle/asn1/x509/SubjectPublicKeyInfo.java delete mode 100644 node/src/main/kotlin/net/corda/node/internal/djvm/AttachmentFactory.kt delete mode 100644 node/src/main/kotlin/net/corda/node/internal/djvm/CommandFactory.kt delete mode 100644 node/src/main/kotlin/net/corda/node/internal/djvm/ComponentFactory.kt delete mode 100644 node/src/main/kotlin/net/corda/node/internal/djvm/DeterministicVerifier.kt delete mode 100644 node/src/main/kotlin/net/corda/node/internal/djvm/Serializer.kt delete mode 100644 node/src/main/kotlin/net/corda/node/services/transactions/DeterministicVerifierFactoryService.kt delete mode 100644 node/src/main/kotlin/sandbox/net/corda/core/crypto/Crypto.kt delete mode 100644 node/src/main/kotlin/sandbox/net/corda/core/crypto/SecureHash.kt delete mode 100644 node/src/main/kotlin/sandbox/net/corda/core/crypto/SignatureScheme.kt delete mode 100644 node/src/main/kotlin/sandbox/net/corda/core/crypto/TransactionSignature.kt delete mode 100644 node/src/main/kotlin/sandbox/net/corda/core/crypto/internal/ProviderMap.kt delete mode 100644 serialization-deterministic/README.md delete mode 100644 serialization-deterministic/build.gradle delete mode 100644 serialization-deterministic/src/main/kotlin/net/corda/serialization/internal/ByteBufferStreams.kt delete mode 100644 serialization-deterministic/src/main/kotlin/net/corda/serialization/internal/DefaultWhitelist.kt delete mode 100644 serialization-deterministic/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPSerializationThreadContext.kt delete mode 100644 serialization-deterministic/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPSerializerFactories.kt delete mode 100644 serialization-deterministic/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPStreams.kt delete mode 100644 serialization-deterministic/src/main/kotlin/net/corda/serialization/internal/model/DefaultCacheProvider.kt delete mode 100644 serialization-deterministic/src/main/resources/META-INF/DJVM-preload delete mode 100644 serialization-djvm/build.gradle delete mode 100644 serialization-djvm/deserializers/build.gradle delete mode 100644 serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/BitSetDeserializer.kt delete mode 100644 serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/CertPathDeserializer.kt delete mode 100644 serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/CheckEnum.kt delete mode 100644 serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/ClassDeserializer.kt delete mode 100644 serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/CorDappCustomDeserializer.kt delete mode 100644 serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/CreateCollection.kt delete mode 100644 serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/CreateCurrency.kt delete mode 100644 serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/CreateMap.kt delete mode 100644 serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/Decimal128Deserializer.kt delete mode 100644 serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/Decimal32Deserializer.kt delete mode 100644 serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/Decimal64Deserializer.kt delete mode 100644 serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/DescribeEnum.kt delete mode 100644 serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/DurationDeserializer.kt delete mode 100644 serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/EnumSetDeserializer.kt delete mode 100644 serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/GetEnumNames.kt delete mode 100644 serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/InputStreamDeserializer.kt delete mode 100644 serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/InstantDeserializer.kt delete mode 100644 serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/JustForCasting.kt delete mode 100644 serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/LocalDateDeserializer.kt delete mode 100644 serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/LocalDateTimeDeserializer.kt delete mode 100644 serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/LocalTimeDeserializer.kt delete mode 100644 serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/MergeWhitelists.kt delete mode 100644 serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/MonthDayDeserializer.kt delete mode 100644 serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/OffsetDateTimeDeserializer.kt delete mode 100644 serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/OffsetTimeDeserializer.kt delete mode 100644 serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/OpaqueBytesSubSequenceDeserializer.kt delete mode 100644 serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/OptionalDeserializer.kt delete mode 100644 serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/PeriodDeserializer.kt delete mode 100644 serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/PublicKeyDecoder.kt delete mode 100644 serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/SymbolDeserializer.kt delete mode 100644 serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/UnsignedByteDeserializer.kt delete mode 100644 serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/UnsignedIntegerDeserializer.kt delete mode 100644 serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/UnsignedLongDeserializer.kt delete mode 100644 serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/UnsignedShortDeserializer.kt delete mode 100644 serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/X509CRLDeserializer.kt delete mode 100644 serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/X509CertificateDeserializer.kt delete mode 100644 serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/YearDeserializer.kt delete mode 100644 serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/YearMonthDeserializer.kt delete mode 100644 serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/ZoneIdDeserializer.kt delete mode 100644 serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/ZonedDateTimeDeserializer.kt delete mode 100644 serialization-djvm/deserializers/src/main/resources/META-INF/DJVM-preload delete mode 100644 serialization-djvm/src/main/java/net/corda/serialization/djvm/serializers/CacheKey.java delete mode 100644 serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/AMQPSerializationScheme.kt delete mode 100644 serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/DelegatingClassLoader.kt delete mode 100644 serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/SandboxSerializationSchemeBuilder.kt delete mode 100644 serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/SandboxSerializerFactoryFactory.kt delete mode 100644 serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/SandboxWhitelist.kt delete mode 100644 serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/Serialization.kt delete mode 100644 serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/ExceptionUtils.kt delete mode 100644 serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/PrimitiveSerializer.kt delete mode 100644 serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxBitSetSerializer.kt delete mode 100644 serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxCertPathSerializer.kt delete mode 100644 serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxCharacterSerializer.kt delete mode 100644 serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxClassSerializer.kt delete mode 100644 serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxCollectionSerializer.kt delete mode 100644 serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxCorDappCustomSerializer.kt delete mode 100644 serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxCurrencySerializer.kt delete mode 100644 serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxDecimal128Serializer.kt delete mode 100644 serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxDecimal32Serializer.kt delete mode 100644 serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxDecimal64Serializer.kt delete mode 100644 serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxDurationSerializer.kt delete mode 100644 serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxEnumSerializer.kt delete mode 100644 serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxEnumSetSerializer.kt delete mode 100644 serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxInputStreamSerializer.kt delete mode 100644 serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxInstantSerializer.kt delete mode 100644 serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxLocalDateSerializer.kt delete mode 100644 serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxLocalDateTimeSerializer.kt delete mode 100644 serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxLocalTimeSerializer.kt delete mode 100644 serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxMapSerializer.kt delete mode 100644 serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxMonthDaySerializer.kt delete mode 100644 serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxOffsetDateTimeSerializer.kt delete mode 100644 serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxOffsetTimeSerializer.kt delete mode 100644 serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxOpaqueBytesSubSequenceSerializer.kt delete mode 100644 serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxOptionalSerializer.kt delete mode 100644 serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxPeriodSerializer.kt delete mode 100644 serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxPrimitiveSerializer.kt delete mode 100644 serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxPublicKeySerializer.kt delete mode 100644 serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxSymbolSerializer.kt delete mode 100644 serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxToStringSerializer.kt delete mode 100644 serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxUnsignedByteSerializer.kt delete mode 100644 serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxUnsignedIntegerSerializer.kt delete mode 100644 serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxUnsignedLongSerializer.kt delete mode 100644 serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxUnsignedShortSerializer.kt delete mode 100644 serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxX509CRLSerializer.kt delete mode 100644 serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxX509CertificateSerializer.kt delete mode 100644 serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxYearMonthSerializer.kt delete mode 100644 serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxYearSerializer.kt delete mode 100644 serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxZoneIdSerializer.kt delete mode 100644 serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxZonedDateTimeSerializer.kt delete mode 100644 serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/Serializers.kt delete mode 100644 serialization-djvm/src/test/java/greymalkin/ExternalData.java delete mode 100644 serialization-djvm/src/test/java/greymalkin/ExternalEnum.java delete mode 100644 serialization-djvm/src/test/java/net/corda/serialization/djvm/InnocentData.java delete mode 100644 serialization-djvm/src/test/java/net/corda/serialization/djvm/MultiConstructorData.java delete mode 100644 serialization-djvm/src/test/java/net/corda/serialization/djvm/VeryEvilData.java delete mode 100644 serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/Assertions.kt delete mode 100644 serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeBigDecimalTest.kt delete mode 100644 serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeBigIntegerTest.kt delete mode 100644 serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeBitSetTest.kt delete mode 100644 serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeCertificatesTest.kt delete mode 100644 serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeClassTest.kt delete mode 100644 serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeCollectionsTest.kt delete mode 100644 serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeComposedCustomDataTest.kt delete mode 100644 serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeCurrencyTest.kt delete mode 100644 serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeCustomGenericDataTest.kt delete mode 100644 serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeCustomisedEnumTest.kt delete mode 100644 serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeDurationTest.kt delete mode 100644 serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeEnumSetTest.kt delete mode 100644 serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeEnumTest.kt delete mode 100644 serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeEnumWithEvolutionTest.kt delete mode 100644 serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeGenericsTest.kt delete mode 100644 serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeInputStreamTest.kt delete mode 100644 serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeInstantTest.kt delete mode 100644 serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeJavaWithMultipleConstructorsTest.kt delete mode 100644 serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeKotlinAliasTest.kt delete mode 100644 serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeLocalDateTest.kt delete mode 100644 serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeLocalDateTimeTest.kt delete mode 100644 serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeLocalTimeTest.kt delete mode 100644 serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeMapsTest.kt delete mode 100644 serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeMonthDayTest.kt delete mode 100644 serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeObjectArraysTest.kt delete mode 100644 serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeOffsetDateTimeTest.kt delete mode 100644 serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeOffsetTimeTest.kt delete mode 100644 serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeOpaqueBytesSubSequenceTest.kt delete mode 100644 serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeOptionalTest.kt delete mode 100644 serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializePeriodTest.kt delete mode 100644 serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializePrimitiveArraysTest.kt delete mode 100644 serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializePrimitivesTest.kt delete mode 100644 serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeProtonJTest.kt delete mode 100644 serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializePublicKeyTest.kt delete mode 100644 serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeRemoteCustomisedEnumTest.kt delete mode 100644 serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeStringBufferTest.kt delete mode 100644 serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeStringTest.kt delete mode 100644 serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeWithCustomSerializerTest.kt delete mode 100644 serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeWithObjectCustomSerializerTest.kt delete mode 100644 serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeWithSerializationWhitelistTest.kt delete mode 100644 serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeYearMonthTest.kt delete mode 100644 serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeYearTest.kt delete mode 100644 serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeZoneIdTest.kt delete mode 100644 serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeZonedDateTimeTest.kt delete mode 100644 serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/LocalSerialization.kt delete mode 100644 serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/LocalTypeModelTest.kt delete mode 100644 serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/SafeDeserialisationTest.kt delete mode 100644 serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/SandboxType.kt delete mode 100644 serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/TestBase.kt delete mode 100644 serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/TestHelpers.kt delete mode 100644 serialization-djvm/src/test/resources/log4j2-test.xml delete mode 100644 serialization-djvm/src/test/resources/testing.cert delete mode 100755 serialization-djvm/src/test/scripts/generate-certificate.sh delete mode 100644 serialization/src/main/kotlin/net/corda/serialization/internal/model/DefaultCacheProvider.kt diff --git a/.ci/api-current.txt b/.ci/api-current.txt index a9499a2158..200b056099 100644 --- a/.ci/api-current.txt +++ b/.ci/api-current.txt @@ -8554,12 +8554,12 @@ public final class net.corda.testing.driver.DriverParameters extends java.lang.O public (boolean, java.nio.file.Path, net.corda.testing.driver.PortAllocation, net.corda.testing.driver.PortAllocation, java.util.Map, boolean, boolean, boolean, java.util.List, java.util.List, net.corda.testing.driver.JmxPolicy, net.corda.core.node.NetworkParameters, java.util.Map, boolean, int, kotlin.jvm.internal.DefaultConstructorMarker) public (boolean, java.nio.file.Path, net.corda.testing.driver.PortAllocation, net.corda.testing.driver.PortAllocation, java.util.Map, boolean, boolean, boolean, java.util.List, java.util.List, net.corda.testing.driver.JmxPolicy, net.corda.core.node.NetworkParameters, java.util.Map, boolean, java.util.Collection) public (boolean, java.nio.file.Path, net.corda.testing.driver.PortAllocation, net.corda.testing.driver.PortAllocation, java.util.Map, boolean, boolean, boolean, java.util.List, java.util.List, net.corda.testing.driver.JmxPolicy, net.corda.core.node.NetworkParameters, java.util.Map, boolean, java.util.Collection, int, kotlin.jvm.internal.DefaultConstructorMarker) - public (boolean, java.nio.file.Path, net.corda.testing.driver.PortAllocation, net.corda.testing.driver.PortAllocation, java.util.Map, boolean, boolean, boolean, java.util.List, java.util.List, net.corda.testing.driver.JmxPolicy, net.corda.core.node.NetworkParameters, java.util.Map, boolean, java.util.Collection, java.nio.file.Path, java.util.List, java.util.Map, boolean) - public (boolean, java.nio.file.Path, net.corda.testing.driver.PortAllocation, net.corda.testing.driver.PortAllocation, java.util.Map, boolean, boolean, boolean, java.util.List, java.util.List, net.corda.testing.driver.JmxPolicy, net.corda.core.node.NetworkParameters, java.util.Map, boolean, java.util.Collection, java.nio.file.Path, java.util.List, java.util.Map, boolean, int, kotlin.jvm.internal.DefaultConstructorMarker) - public (boolean, java.nio.file.Path, net.corda.testing.driver.PortAllocation, net.corda.testing.driver.PortAllocation, java.util.Map, boolean, boolean, boolean, java.util.List, java.util.List, net.corda.testing.driver.JmxPolicy, net.corda.core.node.NetworkParameters, java.util.Map, boolean, java.util.Collection, java.nio.file.Path, java.util.List, java.util.Map, boolean, boolean) - public (boolean, java.nio.file.Path, net.corda.testing.driver.PortAllocation, net.corda.testing.driver.PortAllocation, java.util.Map, boolean, boolean, boolean, java.util.List, java.util.List, net.corda.testing.driver.JmxPolicy, net.corda.core.node.NetworkParameters, java.util.Map, boolean, java.util.Collection, java.nio.file.Path, java.util.List, java.util.Map, boolean, boolean, int, kotlin.jvm.internal.DefaultConstructorMarker) - public (boolean, java.nio.file.Path, net.corda.testing.driver.PortAllocation, net.corda.testing.driver.PortAllocation, java.util.Map, boolean, boolean, boolean, java.util.List, java.util.List, net.corda.testing.driver.JmxPolicy, net.corda.core.node.NetworkParameters, java.util.Map, boolean, java.util.Collection, java.nio.file.Path, java.util.List, java.util.Map, boolean, boolean, java.time.Duration) - public (boolean, java.nio.file.Path, net.corda.testing.driver.PortAllocation, net.corda.testing.driver.PortAllocation, java.util.Map, boolean, boolean, boolean, java.util.List, java.util.List, net.corda.testing.driver.JmxPolicy, net.corda.core.node.NetworkParameters, java.util.Map, boolean, java.util.Collection, java.nio.file.Path, java.util.List, java.util.Map, boolean, boolean, java.time.Duration, int, kotlin.jvm.internal.DefaultConstructorMarker) + public (boolean, java.nio.file.Path, net.corda.testing.driver.PortAllocation, net.corda.testing.driver.PortAllocation, java.util.Map, boolean, boolean, boolean, java.util.List, java.util.List, net.corda.testing.driver.JmxPolicy, net.corda.core.node.NetworkParameters, java.util.Map, boolean, java.util.Collection, java.util.Map, boolean) + public (boolean, java.nio.file.Path, net.corda.testing.driver.PortAllocation, net.corda.testing.driver.PortAllocation, java.util.Map, boolean, boolean, boolean, java.util.List, java.util.List, net.corda.testing.driver.JmxPolicy, net.corda.core.node.NetworkParameters, java.util.Map, boolean, java.util.Collection, java.util.Map, boolean, int, kotlin.jvm.internal.DefaultConstructorMarker) + public (boolean, java.nio.file.Path, net.corda.testing.driver.PortAllocation, net.corda.testing.driver.PortAllocation, java.util.Map, boolean, boolean, boolean, java.util.List, java.util.List, net.corda.testing.driver.JmxPolicy, net.corda.core.node.NetworkParameters, java.util.Map, boolean, java.util.Collection, java.util.Map, boolean, boolean) + public (boolean, java.nio.file.Path, net.corda.testing.driver.PortAllocation, net.corda.testing.driver.PortAllocation, java.util.Map, boolean, boolean, boolean, java.util.List, java.util.List, net.corda.testing.driver.JmxPolicy, net.corda.core.node.NetworkParameters, java.util.Map, boolean, java.util.Collection, java.util.Map, boolean, boolean, int, kotlin.jvm.internal.DefaultConstructorMarker) + public (boolean, java.nio.file.Path, net.corda.testing.driver.PortAllocation, net.corda.testing.driver.PortAllocation, java.util.Map, boolean, boolean, boolean, java.util.List, java.util.List, net.corda.testing.driver.JmxPolicy, net.corda.core.node.NetworkParameters, java.util.Map, boolean, java.util.Collection, java.util.Map, boolean, boolean, java.time.Duration) + public (boolean, java.nio.file.Path, net.corda.testing.driver.PortAllocation, net.corda.testing.driver.PortAllocation, java.util.Map, boolean, boolean, boolean, java.util.List, java.util.List, net.corda.testing.driver.JmxPolicy, net.corda.core.node.NetworkParameters, java.util.Map, boolean, java.util.Collection, java.util.Map, boolean, boolean, java.time.Duration, int, kotlin.jvm.internal.DefaultConstructorMarker) public (boolean, java.nio.file.Path, net.corda.testing.driver.PortAllocation, net.corda.testing.driver.PortAllocation, java.util.Map, boolean, boolean, boolean, java.util.List, java.util.List, net.corda.testing.driver.JmxPolicy, net.corda.core.node.NetworkParameters, boolean) public final boolean component1() @NotNull @@ -8573,18 +8573,14 @@ public final class net.corda.testing.driver.DriverParameters extends java.lang.O public final boolean component14() @Nullable public final java.util.Collection component15() - @Nullable - public final java.nio.file.Path component16() @NotNull - public final java.util.List component17() + public final java.util.Map component16() + public final boolean component17() + public final boolean component18() @NotNull - public final java.util.Map component18() - public final boolean component19() + public final java.time.Duration component19() @NotNull public final java.nio.file.Path component2() - public final boolean component20() - @NotNull - public final java.time.Duration component21() @NotNull public final net.corda.testing.driver.PortAllocation component3() @NotNull @@ -8601,11 +8597,11 @@ public final class net.corda.testing.driver.DriverParameters extends java.lang.O @NotNull public final net.corda.testing.driver.DriverParameters copy(boolean, java.nio.file.Path, net.corda.testing.driver.PortAllocation, net.corda.testing.driver.PortAllocation, java.util.Map, boolean, boolean, boolean, java.util.List, java.util.List, net.corda.testing.driver.JmxPolicy, net.corda.core.node.NetworkParameters, java.util.Map, boolean, java.util.Collection) @NotNull - public final net.corda.testing.driver.DriverParameters copy(boolean, java.nio.file.Path, net.corda.testing.driver.PortAllocation, net.corda.testing.driver.PortAllocation, java.util.Map, boolean, boolean, boolean, java.util.List, java.util.List, net.corda.testing.driver.JmxPolicy, net.corda.core.node.NetworkParameters, java.util.Map, boolean, java.util.Collection, java.nio.file.Path, java.util.List, java.util.Map, boolean) + public final net.corda.testing.driver.DriverParameters copy(boolean, java.nio.file.Path, net.corda.testing.driver.PortAllocation, net.corda.testing.driver.PortAllocation, java.util.Map, boolean, boolean, boolean, java.util.List, java.util.List, net.corda.testing.driver.JmxPolicy, net.corda.core.node.NetworkParameters, java.util.Map, boolean, java.util.Collection, java.util.Map, boolean) @NotNull - public final net.corda.testing.driver.DriverParameters copy(boolean, java.nio.file.Path, net.corda.testing.driver.PortAllocation, net.corda.testing.driver.PortAllocation, java.util.Map, boolean, boolean, boolean, java.util.List, java.util.List, net.corda.testing.driver.JmxPolicy, net.corda.core.node.NetworkParameters, java.util.Map, boolean, java.util.Collection, java.nio.file.Path, java.util.List, java.util.Map, boolean, boolean) + public final net.corda.testing.driver.DriverParameters copy(boolean, java.nio.file.Path, net.corda.testing.driver.PortAllocation, net.corda.testing.driver.PortAllocation, java.util.Map, boolean, boolean, boolean, java.util.List, java.util.List, net.corda.testing.driver.JmxPolicy, net.corda.core.node.NetworkParameters, java.util.Map, boolean, java.util.Collection, java.util.Map, boolean, boolean) @NotNull - public final net.corda.testing.driver.DriverParameters copy(boolean, java.nio.file.Path, net.corda.testing.driver.PortAllocation, net.corda.testing.driver.PortAllocation, java.util.Map, boolean, boolean, boolean, java.util.List, java.util.List, net.corda.testing.driver.JmxPolicy, net.corda.core.node.NetworkParameters, java.util.Map, boolean, java.util.Collection, java.nio.file.Path, java.util.List, java.util.Map, boolean, boolean, java.time.Duration) + public final net.corda.testing.driver.DriverParameters copy(boolean, java.nio.file.Path, net.corda.testing.driver.PortAllocation, net.corda.testing.driver.PortAllocation, java.util.Map, boolean, boolean, boolean, java.util.List, java.util.List, net.corda.testing.driver.JmxPolicy, net.corda.core.node.NetworkParameters, java.util.Map, boolean, java.util.Collection, java.util.Map, boolean, boolean, java.time.Duration) @NotNull public final net.corda.testing.driver.DriverParameters copy(boolean, java.nio.file.Path, net.corda.testing.driver.PortAllocation, net.corda.testing.driver.PortAllocation, java.util.Map, boolean, boolean, boolean, java.util.List, java.util.List, net.corda.testing.driver.JmxPolicy, net.corda.core.node.NetworkParameters, java.util.Set) public boolean equals(Object) @@ -8614,10 +8610,6 @@ public final class net.corda.testing.driver.DriverParameters extends java.lang.O public final java.util.Collection getCordappsForAllNodes() @NotNull public final net.corda.testing.driver.PortAllocation getDebugPortAllocation() - @Nullable - public final java.nio.file.Path getDjvmBootstrapSource() - @NotNull - public final java.util.List getDjvmCordaSource() @NotNull public final java.nio.file.Path getDriverDirectory() @NotNull @@ -8654,10 +8646,6 @@ public final class net.corda.testing.driver.DriverParameters extends java.lang.O @NotNull public final net.corda.testing.driver.DriverParameters withDebugPortAllocation(net.corda.testing.driver.PortAllocation) @NotNull - public final net.corda.testing.driver.DriverParameters withDjvmBootstrapSource(java.nio.file.Path) - @NotNull - public final net.corda.testing.driver.DriverParameters withDjvmCordaSource(java.util.List) - @NotNull public final net.corda.testing.driver.DriverParameters withDriverDirectory(java.nio.file.Path) @NotNull public final net.corda.testing.driver.DriverParameters withEnvironmentVariables(java.util.Map) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 8a8dff99e1..3421d521f7 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -7,14 +7,6 @@ node-api @rick-r3 node/src/main/kotlin/net/corda/node/internal @rick-r3 node/src/main/kotlin/net/corda/node/services @rick-r3 -# Determinstic components -core-deterministic @chrisr3 -jdk8u-deterministic @chrisr3 -node/djvm @chrisr3 -serialization-deterministic @chrisr3 -serialization-djvm @chrisr3 -serialization-tests @chrisr3 - # Demobench defaults to Chris, but Viktor for the main code tools/demobench @chrisr3 diff --git a/build.gradle b/build.gradle index 3d94dc2eaf..ded7cd5189 100644 --- a/build.gradle +++ b/build.gradle @@ -75,8 +75,6 @@ buildscript { ext.disruptor_version = constants.getProperty("disruptorVersion") ext.metrics_version = constants.getProperty("metricsVersion") ext.metrics_new_relic_version = constants.getProperty("metricsNewRelicVersion") - ext.djvm_version = constants.getProperty("djvmVersion") - ext.deterministic_rt_version = constants.getProperty('deterministicRtVersion') ext.okhttp_version = constants.getProperty("okhttpVersion") ext.netty_version = constants.getProperty("nettyVersion") ext.tcnative_version = constants.getProperty("tcnativeVersion") @@ -134,9 +132,6 @@ buildscript { ext.fontawesomefx_fontawesome_version = constants.getProperty("fontawesomefxFontawesomeVersion") } - // Name of the IntelliJ SDK created for the deterministic Java rt.jar. - // ext.deterministic_idea_sdk = '1.8 (Deterministic)' - // Update 121 is required for ObjectInputFilter. // Updates [131, 161] also have zip compression bugs on MacOS (High Sierra). // when the java version in NodeStartup.hasMinimumJavaVersion() changes, so must this check @@ -581,28 +576,6 @@ task jacocoRootReport(type: org.gradle.testing.jacoco.tasks.JacocoReport) { it.exists() }) } - afterEvaluate { - classDirectories = files(classDirectories.files.collect { - fileTree(dir: it, - // these exclusions are necessary because jacoco gets confused by same class names - // which occur due to deterministic versions of non deterministic classes - exclude: ['**/net/corda/core/crypto/DigestSupplier**', - '**/net/corda/core/crypto/DelegatingSecureRandomService', - '**/net/corda/core/internal/ThreadLocalToggleField**', - '**/net/corda/core/internal/InheritableThreadLocalToggleField**', - '**/net/corda/core/internal/ToggleField**', - 'net/corda/core/internal/rules/StateContractValidationEnforcementRule**', - 'net/corda/core/internal/SimpleToggleField**', - 'net/corda/core/serialization/SerializationFactory**', - 'net/corda/serialization/internal/amqp/AMQPStreams**', - 'net/corda/serialization/internal/amqp/AMQPSerializerFactories**', - 'net/corda/serialization/internal/amqp/AMQPSerializationThreadContext**', - 'net/corda/serialization/internal/ByteBufferStreams**', - 'net/corda/serialization/internal/model/DefaultCacheProvider**', - 'net/corda/serialization/internal/DefaultWhitelist**' - ]) - }) - } } tasks.register('detekt', JavaExec) { @@ -655,15 +628,11 @@ bintrayConfig { 'corda-mock', 'corda-rpc', 'corda-core', - 'corda-core-deterministic', - 'corda-deterministic-verifier', - 'corda-deserializers-djvm', 'corda', 'corda-finance-workflows', 'corda-finance-contracts', 'corda-node', 'corda-node-api', - 'corda-node-djvm', 'corda-test-common', 'corda-core-test-utils', 'corda-test-utils', @@ -676,8 +645,6 @@ bintrayConfig { 'corda-shell', 'corda-tools-shell-cli', 'corda-serialization', - 'corda-serialization-deterministic', - 'corda-serialization-djvm', 'corda-tools-blob-inspector', 'corda-tools-explorer', 'corda-tools-network-bootstrapper', @@ -808,8 +775,4 @@ distributedTesting { distribution DistributeTestsBy.METHOD } } - - ignoredTests = [ - ':core-deterministic:testing:data:test' - ] } diff --git a/constants.properties b/constants.properties index bf4617968f..55e9e68db7 100644 --- a/constants.properties +++ b/constants.properties @@ -34,8 +34,6 @@ snakeYamlVersion=1.33 caffeineVersion=2.9.3 metricsVersion=4.1.0 metricsNewRelicVersion=1.1.1 -djvmVersion=1.1.1 -deterministicRtVersion=1.0-RC02 openSourceBranch=https://github.com/corda/corda/blob/release/os/4.4 openSourceSamplesBranch=https://github.com/corda/samples/blob/release-V4 jolokiaAgentVersion=1.6.1 diff --git a/core-deterministic/README.md b/core-deterministic/README.md deleted file mode 100644 index 766d178882..0000000000 --- a/core-deterministic/README.md +++ /dev/null @@ -1,2 +0,0 @@ -## corda-core-deterministic. -This artifact is a deterministic subset of the binary contents of `corda-core`. diff --git a/core-deterministic/build.gradle b/core-deterministic/build.gradle deleted file mode 100644 index 3f84bc5e1c..0000000000 --- a/core-deterministic/build.gradle +++ /dev/null @@ -1,249 +0,0 @@ -import net.corda.gradle.jarfilter.JarFilterTask -import net.corda.gradle.jarfilter.MetaFixerTask -import proguard.gradle.ProGuardTask - -import static org.gradle.api.JavaVersion.VERSION_1_8 - -plugins { - id 'org.jetbrains.kotlin.jvm' - id 'net.corda.plugins.publish-utils' - id 'com.jfrog.artifactory' - id 'java-library' - id 'idea' -} -apply from: "${rootProject.projectDir}/deterministic.gradle" - -description 'Corda core (deterministic)' - -evaluationDependsOn(":core") - -// required by DJVM and Avian JVM (for running inside the SGX enclave) which only supports Java 8. -targetCompatibility = VERSION_1_8 - -def javaHome = System.getProperty('java.home') -def jarBaseName = "corda-${project.name}".toString() - -configurations { - deterministicLibraries { - canBeConsumed = false - extendsFrom api - } - deterministicArtifacts.extendsFrom deterministicLibraries -} - -dependencies { - compileOnly project(':core') - - // Configure these by hand. It should be a minimal subset of core's dependencies, - // and without any obviously non-deterministic ones such as Hibernate. - - // These "api" dependencies will become "compile" scoped in our published POM. - api "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" - api "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" - api "javax.persistence:javax.persistence-api:2.2" - api "com.google.code.findbugs:jsr305:$jsr305_version" - api "org.slf4j:slf4j-api:$slf4j_version" - - compileOnly "io.opentelemetry:opentelemetry-api:${open_telemetry_version}" - compileOnly project(':opentelemetry') - - // These dependencies will become "runtime" scoped in our published POM. - // See publish.dependenciesFrom.defaultScope. - deterministicLibraries "net.i2p.crypto:eddsa:$eddsa_version" - // From 1.71 BC validates the RSA modulus is prime using random numbers, which is not supported inside the DJVM - deterministicLibraries "org.bouncycastle:bcprov-jdk15on:1.70" - deterministicLibraries "org.bouncycastle:bcpkix-jdk15on:1.70" -} - -tasks.named('jar', Jar) { - archiveBaseName = 'DOES-NOT-EXIST' - // Don't build a jar here because it would be the wrong one. - // The jar we really want will be built by the metafix task. - enabled = false -} - -def coreJarTask = project(':core').tasks.named('jar', Jar) -def originalJar = coreJarTask.map { it.outputs.files.singleFile } - -def patchCore = tasks.register('patchCore', Zip) { - dependsOn coreJarTask - destinationDirectory = layout.buildDirectory.dir('source-libs') - metadataCharset 'UTF-8' - archiveClassifier = 'transient' - archiveExtension = 'jar' - - from(compileKotlin) - from(processResources) - from(zipTree(originalJar)) { - exclude 'net/corda/core/crypto/DelegatingSecureRandomService*.class' - exclude 'net/corda/core/crypto/DigestSupplier.class' - exclude 'net/corda/core/internal/*ToggleField*.class' - exclude 'net/corda/core/serialization/*SerializationFactory*.class' - exclude 'net/corda/core/serialization/internal/AttachmentsHolderImpl.class' - exclude 'net/corda/core/serialization/internal/CheckpointSerializationFactory*.class' - exclude 'net/corda/core/internal/rules/*.class' - exclude 'net/corda/core/internal/utilities/PrivateInterner*.class' - exclude 'net/corda/core/crypto/internal/PublicKeyCache*.class' - exclude 'net/corda/core/internal/ContractStateClassCache*.class' - exclude 'net/corda/core/internal/LazyPool*.class' - } - - reproducibleFileOrder = true - includeEmptyDirs = false -} - -def predeterminise = tasks.register('predeterminise', ProGuardTask) { - injars patchCore - outjars file("$buildDir/proguard/pre-deterministic-${project.version}.jar") - - if (JavaVersion.current().isJava9Compatible()) { - libraryjars "$javaHome/jmods" - } else { - libraryjars "$javaHome/lib/rt.jar" - libraryjars "$javaHome/lib/jce.jar" - } - configurations.compileClasspath.forEach { - if (originalJar != it) { - libraryjars it, filter: '!META-INF/versions/**' - } - } - - keepattributes '*' - keepdirectories - dontwarn '**$1$1,org.hibernate.annotations.*' - dontpreverify - dontobfuscate - dontoptimize - dontnote - printseeds - verbose - - keep '@interface net.corda.core.* { *; }' - keep '@interface net.corda.core.contracts.** { *; }' - keep '@interface net.corda.core.serialization.** { *; }' - keep '@net.corda.core.KeepForDJVM class * { *; }', includedescriptorclasses:true - keepclassmembers 'class net.corda.core.** { public synthetic ; }' -} - -def jarFilter = tasks.register('jarFilter', JarFilterTask) { - jars predeterminise - annotations { - forDelete = [ - "net.corda.core.DeleteForDJVM" - ] - forStub = [ - "net.corda.core.StubOutForDJVM" - ] - forRemove = [ - "co.paralleluniverse.fibers.Suspendable", - "org.hibernate.annotations.Immutable" - ] - forSanitise = [ - "net.corda.core.DeleteForDJVM" - ] - } -} - -def determinise = tasks.register('determinise', ProGuardTask) { - injars jarFilter - outjars file("$buildDir/proguard/$jarBaseName-${project.version}.jar") - - if (JavaVersion.current().isJava9Compatible()) { - libraryjars "$javaHome/jmods" - } else { - libraryjars "$javaHome/lib/rt.jar" - libraryjars "$javaHome/lib/jce.jar" - } - configurations.deterministicLibraries.forEach { - libraryjars it, filter: '!META-INF/versions/**' - } - - // Analyse the JAR for dead code, and remove (some of) it. - optimizations 'code/removal/simple,code/removal/advanced' - printconfiguration - - keepattributes '*' - keepdirectories - dontobfuscate - dontnote - printseeds - verbose - - keep '@interface net.corda.core.CordaInternal { *; }' - keep '@interface net.corda.core.DoNotImplement { *; }' - keep '@interface net.corda.core.KeepForDJVM { *; }' - keep '@interface net.corda.core.contracts.** { *; }' - keep '@interface net.corda.core.serialization.** { *; }' - keep '@net.corda.core.KeepForDJVM class * { *; }', includedescriptorclasses:true - keepclassmembers 'class net.corda.core.** { public synthetic ; }' -} - -def checkDeterminism = tasks.register('checkDeterminism', ProGuardTask) - -def metafix = tasks.register('metafix', MetaFixerTask) { - outputDir = layout.buildDirectory.dir('libs') - jars determinise - suffix "" - - // Strip timestamps from the JAR to make it reproducible. - preserveTimestamps = false - finalizedBy checkDeterminism -} - -// DOCSTART 01 -checkDeterminism.configure { - dependsOn jdkTask - injars metafix - - libraryjars deterministic_rt_jar - - configurations.deterministicLibraries.forEach { - libraryjars it, filter: '!META-INF/versions/**' - } - - keepattributes '*' - dontpreverify - dontobfuscate - dontoptimize - verbose - - keep 'class *' -} -// DOCEND 01 - -defaultTasks "determinise" -determinise.configure { - finalizedBy metafix -} -tasks.named('assemble') { - dependsOn checkDeterminism -} - -def deterministicJar = metafix.map { it.outputs.files.singleFile } -artifacts { - deterministicArtifacts deterministicJar - publish deterministicJar -} - -tasks.named('sourceJar', Jar) { - from 'README.md' - include 'README.md' -} - -tasks.named('javadocJar', Jar) { - from 'README.md' - include 'README.md' -} - -publish { - dependenciesFrom configurations.deterministicArtifacts - name jarBaseName -} - -idea { - module { - if (project.hasProperty("deterministic_idea_sdk")) { - jdkName project.property("deterministic_idea_sdk") as String - } - } -} diff --git a/core-deterministic/src/main/kotlin/net/corda/core/crypto/DelegatingSecureRandomService.kt b/core-deterministic/src/main/kotlin/net/corda/core/crypto/DelegatingSecureRandomService.kt deleted file mode 100644 index 34355c4191..0000000000 --- a/core-deterministic/src/main/kotlin/net/corda/core/crypto/DelegatingSecureRandomService.kt +++ /dev/null @@ -1,19 +0,0 @@ -package net.corda.core.crypto - -import java.security.Provider -import java.security.SecureRandomSpi - -@Suppress("unused") -class DelegatingSecureRandomService(provider: CordaSecurityProvider) - : Provider.Service(provider, "SecureRandom", "dummy-algorithm", UnsupportedSecureRandomSpi::javaClass.name, null, null) { - private val instance: SecureRandomSpi = UnsupportedSecureRandomSpi(algorithm) - override fun newInstance(param: Any?) = instance - - private class UnsupportedSecureRandomSpi(private val algorithm: String) : SecureRandomSpi() { - override fun engineSetSeed(seed: ByteArray) = unsupported() - override fun engineNextBytes(bytes: ByteArray) = unsupported() - override fun engineGenerateSeed(numBytes: Int) = unsupported() - - private fun unsupported(): Nothing = throw UnsupportedOperationException("$algorithm not supported") - } -} diff --git a/core-deterministic/src/main/kotlin/net/corda/core/crypto/DigestSupplier.kt b/core-deterministic/src/main/kotlin/net/corda/core/crypto/DigestSupplier.kt deleted file mode 100644 index 18d60effa4..0000000000 --- a/core-deterministic/src/main/kotlin/net/corda/core/crypto/DigestSupplier.kt +++ /dev/null @@ -1,10 +0,0 @@ -package net.corda.core.crypto - -import net.corda.core.crypto.internal.DigestAlgorithmFactory -import java.util.function.Supplier - -@Suppress("unused") -private class DigestSupplier(private val algorithm: String) : Supplier { - override fun get(): DigestAlgorithm = DigestAlgorithmFactory.create(algorithm) - val digestLength: Int by lazy { get().digestLength } -} diff --git a/core-deterministic/src/main/kotlin/net/corda/core/crypto/internal/PublicKeyCache.kt b/core-deterministic/src/main/kotlin/net/corda/core/crypto/internal/PublicKeyCache.kt deleted file mode 100644 index b4b7d440d7..0000000000 --- a/core-deterministic/src/main/kotlin/net/corda/core/crypto/internal/PublicKeyCache.kt +++ /dev/null @@ -1,21 +0,0 @@ -package net.corda.core.crypto.internal - -import net.corda.core.utilities.ByteSequence -import java.security.PublicKey - -@Suppress("unused") -object PublicKeyCache { - @Suppress("UNUSED_PARAMETER") - fun bytesForCachedPublicKey(key: PublicKey): ByteSequence? { - return null - } - - @Suppress("UNUSED_PARAMETER") - fun publicKeyForCachedBytes(bytes: ByteSequence): PublicKey? { - return null - } - - fun cachePublicKey(key: PublicKey): PublicKey { - return key - } -} \ No newline at end of file diff --git a/core-deterministic/src/main/kotlin/net/corda/core/internal/ContractStateClassCache.kt b/core-deterministic/src/main/kotlin/net/corda/core/internal/ContractStateClassCache.kt deleted file mode 100644 index f437cc0938..0000000000 --- a/core-deterministic/src/main/kotlin/net/corda/core/internal/ContractStateClassCache.kt +++ /dev/null @@ -1,16 +0,0 @@ -package net.corda.core.internal - -import net.corda.core.contracts.ContractState - -@Suppress("unused") -object ContractStateClassCache { - @Suppress("UNUSED_PARAMETER") - fun contractClassName(key: Class): String? { - return null - } - - @Suppress("UNUSED_PARAMETER") - fun cacheContractClassName(key: Class, contractClassName: String?): String? { - return contractClassName - } -} \ No newline at end of file diff --git a/core-deterministic/src/main/kotlin/net/corda/core/internal/LazyPool.kt b/core-deterministic/src/main/kotlin/net/corda/core/internal/LazyPool.kt deleted file mode 100644 index 4878cd373b..0000000000 --- a/core-deterministic/src/main/kotlin/net/corda/core/internal/LazyPool.kt +++ /dev/null @@ -1,41 +0,0 @@ -package net.corda.core.internal - -import net.corda.core.KeepForDJVM - -/** - * A lazy pool of resources [A], modified for DJVM. - * - * @param clear If specified this function will be run on each borrowed instance before handing it over. - * @param shouldReturnToPool If specified this function will be run on each release to determine whether the instance - * should be returned to the pool for reuse. This may be useful for pooled resources that dynamically grow during - * usage, and we may not want to retain them forever. - * @param bound If specified the pool will be bounded. Once all instances are borrowed subsequent borrows will block until an - * instance is released. - * @param newInstance The function to call to lazily newInstance a pooled resource. - */ -@Suppress("unused") -@KeepForDJVM -class LazyPool( - private val clear: ((A) -> Unit)? = null, - private val shouldReturnToPool: ((A) -> Boolean)? = null, - private val bound: Int? = null, - private val newInstance: () -> A -) { - fun borrow(): A { - return newInstance() - } - - @Suppress("unused_parameter") - fun release(instance: A) { - } - - /** - * Closes the pool. Note that all borrowed instances must have been released before calling this function, otherwise - * the returned iterable will be inaccurate. - */ - fun close(): Iterable { - return emptyList() - } - - fun reentrantRun(withInstance: (A) -> R): R = withInstance(borrow()) -} diff --git a/core-deterministic/src/main/kotlin/net/corda/core/internal/ToggleField.kt b/core-deterministic/src/main/kotlin/net/corda/core/internal/ToggleField.kt deleted file mode 100644 index 1912bd895e..0000000000 --- a/core-deterministic/src/main/kotlin/net/corda/core/internal/ToggleField.kt +++ /dev/null @@ -1,62 +0,0 @@ -package net.corda.core.internal - -import net.corda.core.KeepForDJVM -import net.corda.core.utilities.contextLogger -import org.slf4j.Logger -import kotlin.reflect.KProperty - -/** May go from null to non-null and vice-versa, and that's it. */ -abstract class ToggleField(val name: String) { - abstract fun get(): T? - fun set(value: T?) { - if (value != null) { - check(get() == null) { "$name already has a value." } - setImpl(value) - } else { - check(get() != null) { "$name is already null." } - clear() - } - } - - protected abstract fun setImpl(value: T) - protected abstract fun clear() - operator fun getValue(thisRef: Any?, property: KProperty<*>) = get() - operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T?) = set(value) -} - -@KeepForDJVM -class SimpleToggleField(name: String, private val once: Boolean = false) : ToggleField(name) { - private var holder: T? = null // Force T? in API for safety. - override fun get() = holder - override fun setImpl(value: T) { holder = value } - override fun clear() { - check(!once) { "Value of $name cannot be changed." } - holder = null - } -} - -@KeepForDJVM -class ThreadLocalToggleField(name: String) : ToggleField(name) { - private var holder: T? = null // Force T? in API for safety. - override fun get() = holder - override fun setImpl(value: T) { holder = value } - override fun clear() { - holder = null - } -} - -@Suppress("UNUSED") -@KeepForDJVM -class InheritableThreadLocalToggleField(name: String, - private val log: Logger = staticLog, - private val isAGlobalThreadBeingCreated: (Array) -> Boolean) : ToggleField(name) { - private companion object { - private val staticLog = contextLogger() - } - private var holder: T? = null // Force T? in API for safety. - override fun get() = holder - override fun setImpl(value: T) { holder = value } - override fun clear() { - holder = null - } -} \ No newline at end of file diff --git a/core-deterministic/src/main/kotlin/net/corda/core/internal/rules/TargetVersionDependentRules.kt b/core-deterministic/src/main/kotlin/net/corda/core/internal/rules/TargetVersionDependentRules.kt deleted file mode 100644 index 528d059e2c..0000000000 --- a/core-deterministic/src/main/kotlin/net/corda/core/internal/rules/TargetVersionDependentRules.kt +++ /dev/null @@ -1,12 +0,0 @@ -package net.corda.core.internal.rules - -import net.corda.core.contracts.ContractState - -// This file provides rules that depend on the targetVersion of the current Contract or Flow. -// In core, this is determined by means which are unavailable in the DJVM, -// so we must provide deterministic alternatives here. - -@Suppress("unused") -object StateContractValidationEnforcementRule { - fun shouldEnforce(@Suppress("UNUSED_PARAMETER") state: ContractState): Boolean = true -} \ No newline at end of file diff --git a/core-deterministic/src/main/kotlin/net/corda/core/internal/utilities/PrivateInterner.kt b/core-deterministic/src/main/kotlin/net/corda/core/internal/utilities/PrivateInterner.kt deleted file mode 100644 index 24aa506c5c..0000000000 --- a/core-deterministic/src/main/kotlin/net/corda/core/internal/utilities/PrivateInterner.kt +++ /dev/null @@ -1,16 +0,0 @@ -package net.corda.core.internal.utilities - -import net.corda.core.KeepForDJVM - -@KeepForDJVM -class PrivateInterner(val verifier: IternabilityVerifier = AlwaysInternableVerifier()) { - // DJVM implementation does not intern and does not use Guava - fun intern(sample: S): S = sample - - @KeepForDJVM - companion object { - @Suppress("UNUSED_PARAMETER") - fun findFor(clazz: Class<*>?): PrivateInterner? = null - } -} - diff --git a/core-deterministic/src/main/kotlin/net/corda/core/serialization/SerializationFactory.kt b/core-deterministic/src/main/kotlin/net/corda/core/serialization/SerializationFactory.kt deleted file mode 100644 index 2d6d8d3b09..0000000000 --- a/core-deterministic/src/main/kotlin/net/corda/core/serialization/SerializationFactory.kt +++ /dev/null @@ -1,99 +0,0 @@ -package net.corda.core.serialization - -import net.corda.core.KeepForDJVM -import net.corda.core.serialization.internal.effectiveSerializationEnv -import net.corda.core.utilities.ByteSequence - -/** - * An abstraction for serializing and deserializing objects, with support for versioning of the wire format via - * a header / prefix in the bytes. - */ -@KeepForDJVM -abstract class SerializationFactory { - /** - * Deserialize the bytes in to an object, using the prefixed bytes to determine the format. - * - * @param byteSequence The bytes to deserialize, including a format header prefix. - * @param clazz The class or superclass or the object to be deserialized, or [Any] or [Object] if unknown. - * @param context A context that configures various parameters to deserialization. - */ - abstract fun deserialize(byteSequence: ByteSequence, clazz: Class, context: SerializationContext): T - - /** - * Deserialize the bytes in to an object, using the prefixed bytes to determine the format. - * - * @param byteSequence The bytes to deserialize, including a format header prefix. - * @param clazz The class or superclass or the object to be deserialized, or [Any] or [Object] if unknown. - * @param context A context that configures various parameters to deserialization. - * @return deserialized object along with [SerializationContext] to identify encoding used. - */ - abstract fun deserializeWithCompatibleContext(byteSequence: ByteSequence, clazz: Class, context: SerializationContext): ObjectWithCompatibleContext - - /** - * Serialize an object to bytes using the preferred serialization format version from the context. - * - * @param obj The object to be serialized. - * @param context A context that configures various parameters to serialization, including the serialization format version. - */ - abstract fun serialize(obj: T, context: SerializationContext): SerializedBytes - - /** - * If there is a need to nest serialization/deserialization with a modified context during serialization or deserialization, - * this will return the current context used to start serialization/deserialization. - */ - val currentContext: SerializationContext? get() = _currentContext - - /** - * A context to use as a default if you do not require a specially configured context. It will be the current context - * if the use is somehow nested (see [currentContext]). - */ - val defaultContext: SerializationContext get() = currentContext ?: effectiveSerializationEnv.p2pContext - - private var _currentContext: SerializationContext? = null - - /** - * Change the current context inside the block to that supplied. - */ - fun withCurrentContext(context: SerializationContext?, block: () -> T): T { - return if (context == null) { - block() - } else { - val priorContext = _currentContext - _currentContext = context - try { - block() - } finally { - _currentContext = priorContext - } - } - } - - /** - * Allow subclasses to temporarily mark themselves as the current factory for the current thread during serialization/deserialization. - * Will restore the prior context on exiting the block. - */ - fun asCurrent(block: SerializationFactory.() -> T): T { - val priorContext = _currentFactory - _currentFactory= this - try { - return this.block() - } finally { - _currentFactory = priorContext - } - } - - companion object { - private var _currentFactory: SerializationFactory? = null - - /** - * A default factory for serialization/deserialization, taking into account the [currentFactory] if set. - */ - val defaultFactory: SerializationFactory get() = currentFactory ?: effectiveSerializationEnv.serializationFactory - - /** - * If there is a need to nest serialization/deserialization with a modified context during serialization or deserialization, - * this will return the current factory used to start serialization/deserialization. - */ - val currentFactory: SerializationFactory? get() = _currentFactory - } -} diff --git a/core-deterministic/src/main/kotlin/net/corda/core/serialization/internal/AttachmentsHolderImpl.kt b/core-deterministic/src/main/kotlin/net/corda/core/serialization/internal/AttachmentsHolderImpl.kt deleted file mode 100644 index a2f7b8ab30..0000000000 --- a/core-deterministic/src/main/kotlin/net/corda/core/serialization/internal/AttachmentsHolderImpl.kt +++ /dev/null @@ -1,23 +0,0 @@ -package net.corda.core.serialization.internal - -import net.corda.core.contracts.Attachment -import java.net.URL - -@Suppress("unused") -private class AttachmentsHolderImpl : AttachmentsHolder { - private val attachments = LinkedHashMap>() - - override val size: Int get() = attachments.size - - override fun getKey(key: URL): URL? { - return attachments[key]?.first - } - - override fun get(key: URL): Attachment? { - return attachments[key]?.second - } - - override fun set(key: URL, value: Attachment) { - attachments[key] = key to value - } -} diff --git a/core-deterministic/src/main/resources/META-INF/DJVM-preload b/core-deterministic/src/main/resources/META-INF/DJVM-preload deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/core-deterministic/testing/build.gradle b/core-deterministic/testing/build.gradle deleted file mode 100644 index 61abf977af..0000000000 --- a/core-deterministic/testing/build.gradle +++ /dev/null @@ -1,29 +0,0 @@ -plugins { - id 'org.jetbrains.kotlin.jvm' -} - -dependencies { - testImplementation project(path: ':core-deterministic', configuration: 'deterministicArtifacts') - testImplementation project(path: ':serialization-deterministic', configuration: 'deterministicArtifacts') - testImplementation project(path: ':core-deterministic:testing:verifier', configuration: 'deterministicArtifacts') - testImplementation project(path: ':core-deterministic:testing:data', configuration: 'testData') - testImplementation(project(':finance:contracts')) { - transitive = false - } - testImplementation(project(':finance:workflows')) { - transitive = false - } - - testImplementation "org.slf4j:slf4j-api:$slf4j_version" - testRuntimeOnly "org.apache.logging.log4j:log4j-slf4j-impl:$log4j_version" - testImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version" - testImplementation "org.assertj:assertj-core:$assertj_version" - testImplementation "junit:junit:$junit_version" - testRuntimeOnly "org.junit.vintage:junit-vintage-engine:${junit_vintage_version}" - testRuntimeOnly "org.junit.platform:junit-platform-launcher:${junit_platform_version}" -} - -// This module has no artifact and only contains tests. -tasks.named('jar', Jar) { - enabled = false -} diff --git a/core-deterministic/testing/data/build.gradle b/core-deterministic/testing/data/build.gradle deleted file mode 100644 index 0141dc3c61..0000000000 --- a/core-deterministic/testing/data/build.gradle +++ /dev/null @@ -1,44 +0,0 @@ -plugins { - id 'org.jetbrains.kotlin.jvm' -} - -configurations { - testData { - canBeResolved = false - } -} - -dependencies { - testImplementation project(':core') - testImplementation project(':finance:workflows') - testImplementation project(':node-driver') - testImplementation project(path: ':core-deterministic:testing:verifier', configuration: 'runtimeArtifacts') - - testImplementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8" - testImplementation "org.jetbrains.kotlin:kotlin-reflect" - - testImplementation "junit:junit:$junit_version" - - testRuntimeOnly "org.junit.vintage:junit-vintage-engine:${junit_vintage_version}" - testRuntimeOnly "org.junit.platform:junit-platform-launcher:${junit_platform_version}" -} - -tasks.named('jar', Jar) { - enabled = false -} - -def test = tasks.named('test', Test) { - filter { - // Running this class is the whole point, so include it explicitly. - includeTestsMatching "net.corda.deterministic.data.GenerateData" - } - // force execution of these tests to generate artifacts required by other module (eg. VerifyTransactionTest) - // note: required by Gradle Build Cache. - outputs.upToDateWhen { false } -} - -def testDataJar = file("$buildDir/test-data.jar") -artifacts { - archives file: testDataJar, type: 'jar', builtBy: test - testData file: testDataJar, type: 'jar', builtBy: test -} diff --git a/core-deterministic/testing/data/src/test/kotlin/net/corda/deterministic/data/GenerateData.kt b/core-deterministic/testing/data/src/test/kotlin/net/corda/deterministic/data/GenerateData.kt deleted file mode 100644 index 4612f19c7a..0000000000 --- a/core-deterministic/testing/data/src/test/kotlin/net/corda/deterministic/data/GenerateData.kt +++ /dev/null @@ -1,92 +0,0 @@ -package net.corda.deterministic.data - -import net.corda.core.serialization.deserialize -import net.corda.deterministic.verifier.LocalSerializationRule -import net.corda.deterministic.verifier.TransactionVerificationRequest -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import java.io.FileNotFoundException -import java.net.URLClassLoader -import java.nio.file.Files -import java.nio.file.Path -import java.nio.file.Paths -import java.nio.file.attribute.FileTime -import java.util.* -import java.util.Calendar.* -import java.util.jar.JarOutputStream -import java.util.zip.Deflater.NO_COMPRESSION -import java.util.zip.ZipEntry -import java.util.zip.ZipEntry.* -import kotlin.reflect.jvm.jvmName - -/** - * Use the JUnit framework to generate a JAR of test data. - */ -class GenerateData { - companion object { - private val CONSTANT_TIME: FileTime = FileTime.fromMillis( - GregorianCalendar(1980, FEBRUARY, 1).apply { timeZone = TimeZone.getTimeZone("UTC") }.timeInMillis - ) - private const val KEYSTORE_ALIAS = "tx" - private val KEYSTORE_PASSWORD = "deterministic".toCharArray() - private val TEST_DATA: Path = Paths.get("build", "test-data.jar") - - private fun compressed(name: String) = ZipEntry(name).apply { - lastModifiedTime = CONSTANT_TIME - method = DEFLATED - } - - private fun directory(name: String) = ZipEntry(name).apply { - lastModifiedTime = CONSTANT_TIME - method = STORED - compressedSize = 0 - size = 0 - crc = 0 - } - } - - @Rule - @JvmField - val testSerialization = LocalSerializationRule(GenerateData::class.jvmName) - - @Before - fun createTransactions() { - JarOutputStream(Files.newOutputStream(TEST_DATA)).use { jar -> - jar.setComment("Test data for Deterministic Corda") - jar.setLevel(NO_COMPRESSION) - - // Serialised transactions for the Enclavelet - jar.putNextEntry(directory("txverify")) - jar.putNextEntry(compressed("txverify/tx-success.bin")) - TransactionGenerator.writeSuccess(jar) - jar.putNextEntry(compressed("txverify/tx-failure.bin")) - TransactionGenerator.writeFailure(jar) - - // KeyStore containing an EC private key. - jar.putNextEntry(directory("keystore")) - jar.putNextEntry(compressed("keystore/txsignature.pfx")) - KeyStoreGenerator.writeKeyStore(jar, KEYSTORE_ALIAS, KEYSTORE_PASSWORD) - } - testSerialization.reset() - } - - @Test(timeout = 300_000) - fun verifyTransactions() { - URLClassLoader(arrayOf(TEST_DATA.toUri().toURL())).use { cl -> - cl.loadResource("txverify/tx-success.bin") - .deserialize() - .toLedgerTransaction() - .verify() - - cl.loadResource("txverify/tx-failure.bin") - .deserialize() - .toLedgerTransaction() - } - } - - private fun ClassLoader.loadResource(resourceName: String): ByteArray { - return getResourceAsStream(resourceName)?.use { it.readBytes() } - ?: throw FileNotFoundException(resourceName) - } -} diff --git a/core-deterministic/testing/data/src/test/kotlin/net/corda/deterministic/data/KeyStoreGenerator.kt b/core-deterministic/testing/data/src/test/kotlin/net/corda/deterministic/data/KeyStoreGenerator.kt deleted file mode 100644 index a1511fbb42..0000000000 --- a/core-deterministic/testing/data/src/test/kotlin/net/corda/deterministic/data/KeyStoreGenerator.kt +++ /dev/null @@ -1,50 +0,0 @@ -package net.corda.deterministic.data - -import org.bouncycastle.asn1.x500.X500Name -import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter -import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder -import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder -import java.io.OutputStream -import java.math.BigInteger.TEN -import java.security.KeyPairGenerator -import java.security.KeyStore -import java.security.spec.ECGenParameterSpec -import java.util.* -import java.util.Calendar.* - -object KeyStoreGenerator { - private val keyPairGenerator: KeyPairGenerator = KeyPairGenerator.getInstance("EC").apply { - initialize(ECGenParameterSpec("secp256k1")) - } - - fun writeKeyStore(output: OutputStream, alias: String, password: CharArray) { - val keyPair = keyPairGenerator.generateKeyPair() - val signer = JcaContentSignerBuilder("SHA256WithECDSA").build(keyPair.private) - val dname = X500Name("CN=Enclavelet") - val startDate = Calendar.getInstance().let { cal -> - cal.time = Date() - cal.add(HOUR, -1) - cal.time - } - val endDate = Calendar.getInstance().let { cal -> - cal.time = startDate - cal.add(YEAR, 1) - cal.time - } - val certificate = JcaX509v3CertificateBuilder( - dname, - TEN, - startDate, - endDate, - dname, - keyPair.public - ).build(signer) - val x509 = JcaX509CertificateConverter().getCertificate(certificate) - - KeyStore.getInstance("PKCS12").apply { - load(null, password) - setKeyEntry(alias, keyPair.private, password, arrayOf(x509)) - store(output, password) - } - } -} \ No newline at end of file diff --git a/core-deterministic/testing/data/src/test/kotlin/net/corda/deterministic/data/TransactionGenerator.kt b/core-deterministic/testing/data/src/test/kotlin/net/corda/deterministic/data/TransactionGenerator.kt deleted file mode 100644 index 87b7dbb019..0000000000 --- a/core-deterministic/testing/data/src/test/kotlin/net/corda/deterministic/data/TransactionGenerator.kt +++ /dev/null @@ -1,115 +0,0 @@ -package net.corda.deterministic.data - -import com.nhaarman.mockito_kotlin.doReturn -import com.nhaarman.mockito_kotlin.mock -import com.nhaarman.mockito_kotlin.whenever -import net.corda.core.crypto.entropyToKeyPair -import net.corda.core.identity.AnonymousParty -import net.corda.core.identity.CordaX500Name -import net.corda.core.identity.Party -import net.corda.core.node.services.IdentityService -import net.corda.core.serialization.serialize -import net.corda.deterministic.verifier.MockContractAttachment -import net.corda.deterministic.verifier.SampleCommandData -import net.corda.deterministic.verifier.TransactionVerificationRequest -import net.corda.finance.POUNDS -import net.corda.finance.`issued by` -import net.corda.finance.contracts.asset.Cash.Commands.Issue -import net.corda.finance.contracts.asset.Cash.Commands.Move -import net.corda.finance.contracts.asset.Cash.Companion.PROGRAM_ID -import net.corda.finance.contracts.asset.Cash.State -import net.corda.testing.core.DUMMY_NOTARY_NAME -import net.corda.testing.core.TestIdentity -import net.corda.testing.core.getTestPartyAndCertificate -import net.corda.testing.node.MockServices -import net.corda.testing.node.ledger -import java.io.OutputStream -import java.math.BigInteger -import java.security.KeyPair -import java.security.PublicKey - -object TransactionGenerator { - private val DUMMY_NOTARY: Party = TestIdentity(DUMMY_NOTARY_NAME, 20).party - - private val DUMMY_CASH_ISSUER_KEY: KeyPair = entropyToKeyPair(BigInteger.valueOf(10)) - private val DUMMY_CASH_ISSUER_IDENTITY = getTestPartyAndCertificate(Party(CordaX500Name("Snake Oil Issuer", "London", "GB"), DUMMY_CASH_ISSUER_KEY.public)) - private val DUMMY_CASH_ISSUER = DUMMY_CASH_ISSUER_IDENTITY.party.ref(1) - - private val megaCorp = TestIdentity(CordaX500Name("MegaCorp", "London", "GB")) - private val MEGA_CORP: Party = megaCorp.party - private val MEGA_CORP_PUBKEY: PublicKey = megaCorp.keyPair.public - private val MINI_CORP_PUBKEY: PublicKey = TestIdentity(CordaX500Name("MiniCorp", "London", "GB")).keyPair.public - - private val ledgerServices = MockServices(emptyList(), MEGA_CORP.name, mock().also { - doReturn(MEGA_CORP).whenever(it).partyFromKey(MEGA_CORP_PUBKEY) - doReturn(DUMMY_CASH_ISSUER.party).whenever(it).partyFromKey(DUMMY_CASH_ISSUER_KEY.public) - }) - - fun writeSuccess(output: OutputStream) { - ledgerServices.ledger(DUMMY_NOTARY) { - // Issue a couple of cash states and spend them. - val wtx1 = transaction { - attachments(PROGRAM_ID) - output(PROGRAM_ID, "c1", State(1000.POUNDS `issued by` DUMMY_CASH_ISSUER, AnonymousParty(MEGA_CORP_PUBKEY))) - command(DUMMY_CASH_ISSUER.party.owningKey, Issue()) - verifies() - } - val wtx2 = transaction { - attachments(PROGRAM_ID) - output(PROGRAM_ID, "c2", State(2000.POUNDS `issued by` DUMMY_CASH_ISSUER, AnonymousParty(MEGA_CORP_PUBKEY))) - command(DUMMY_CASH_ISSUER.party.owningKey, Issue()) - verifies() - } - val wtx3 = transaction { - attachments(PROGRAM_ID) - input("c1") - input("c2") - output(PROGRAM_ID, "c3", State(3000.POUNDS `issued by` DUMMY_CASH_ISSUER, AnonymousParty(MINI_CORP_PUBKEY))) - command(MEGA_CORP_PUBKEY, Move()) - verifies() - } - val contractAttachment = MockContractAttachment(interpreter.services.cordappProvider.getContractAttachmentID(PROGRAM_ID)!!, PROGRAM_ID) - TransactionVerificationRequest( - wtx3.serialize(), - arrayOf(wtx1.serialize(), wtx2.serialize()), - arrayOf(contractAttachment.serialize().bytes), - ledgerServices.networkParameters.serialize()) - .serialize() - .writeTo(output) - } - } - - fun writeFailure(output: OutputStream) { - ledgerServices.ledger(DUMMY_NOTARY) { - // Issue a couple of cash states and spend them. - val wtx1 = transaction { - attachments(PROGRAM_ID) - output(PROGRAM_ID, "c1", State(1000.POUNDS `issued by` DUMMY_CASH_ISSUER, AnonymousParty(MEGA_CORP_PUBKEY))) - command(DUMMY_CASH_ISSUER.party.owningKey, Issue()) - verifies() - } - val wtx2 = transaction { - attachments(PROGRAM_ID) - output(PROGRAM_ID, "c2", State(2000.POUNDS `issued by` DUMMY_CASH_ISSUER, AnonymousParty(MEGA_CORP_PUBKEY))) - command(DUMMY_CASH_ISSUER.party.owningKey, Issue()) - verifies() - } - val wtx3 = transaction { - attachments(PROGRAM_ID) - input("c1") - input("c2") - command(DUMMY_CASH_ISSUER.party.owningKey, SampleCommandData) - output(PROGRAM_ID, "c3", State(3000.POUNDS `issued by` DUMMY_CASH_ISSUER, AnonymousParty(MINI_CORP_PUBKEY))) - failsWith("Required ${Move::class.java.canonicalName} command") - } - val contractAttachment = MockContractAttachment(interpreter.services.cordappProvider.getContractAttachmentID(PROGRAM_ID)!!, PROGRAM_ID) - TransactionVerificationRequest( - wtx3.serialize(), - arrayOf(wtx1.serialize(), wtx2.serialize()), - arrayOf(contractAttachment.serialize().bytes), - ledgerServices.networkParameters.serialize()) - .serialize() - .writeTo(output) - } - } -} \ No newline at end of file diff --git a/core-deterministic/testing/src/test/java/net/corda/deterministic/CheatingSecurityProvider.java b/core-deterministic/testing/src/test/java/net/corda/deterministic/CheatingSecurityProvider.java deleted file mode 100644 index 6d4c54a7b4..0000000000 --- a/core-deterministic/testing/src/test/java/net/corda/deterministic/CheatingSecurityProvider.java +++ /dev/null @@ -1,61 +0,0 @@ -package net.corda.deterministic; - -import java.security.Provider; -import java.security.SecureRandom; -import java.security.SecureRandomSpi; -import java.security.Security; -import java.util.concurrent.atomic.AtomicInteger; - -import static org.junit.Assert.assertEquals; - -/** - * Temporarily restore Sun's [SecureRandom] provider. - * This is ONLY for allowing us to generate test data, e.g. signatures. - * - * JDK11 upgrade: rewritten in Java to gain access to private internal JDK classes via module directives (not available to Kotlin compiler): - * sun.security.provider.SecureRandom() - */ -public class CheatingSecurityProvider extends Provider implements AutoCloseable { - - private static AtomicInteger counter = new AtomicInteger(); - - @SuppressWarnings("deprecation") // JDK11: should replace with Provider(String name, double version, String info) (since 9) - public CheatingSecurityProvider() { - super("Cheat-" + counter.getAndIncrement(), 1.8, "Cheat security provider"); - putService(new CheatingSecureRandomService(this)); - assertEquals(1, Security.insertProviderAt(this, 1)); - } - - public void close() { - Security.removeProvider(getName()); - } - - private class SunSecureRandom extends SecureRandom { - public SunSecureRandom() { - // JDK11 upgrade: rewritten in Java to gain access to private internal JDK classes via open module directive - super(new sun.security.provider.SecureRandom(), null); - } - } - - private class CheatingSecureRandomService extends Provider.Service { - - public CheatingSecureRandomService(Provider provider) { - super(provider, "SecureRandom", "CheatingPRNG", CheatingSecureRandomSpi.class.getName(), null, null); - } - - private SecureRandomSpi instance = new CheatingSecureRandomSpi(); - - public Object newInstance(Object constructorParameter){ - return instance; - } - } - - private class CheatingSecureRandomSpi extends SecureRandomSpi { - - private SecureRandom secureRandom = new SunSecureRandom(); - - public void engineSetSeed(byte[] seed) { secureRandom.setSeed(seed); } - public void engineNextBytes(byte[] bytes) { secureRandom.nextBytes(bytes); } - public byte[] engineGenerateSeed(int numBytes) { return secureRandom.generateSeed(numBytes); } - } -} \ No newline at end of file diff --git a/core-deterministic/testing/src/test/kotlin/net/corda/core/internal/ClassLoadingUtils.kt b/core-deterministic/testing/src/test/kotlin/net/corda/core/internal/ClassLoadingUtils.kt deleted file mode 100644 index e2f9075b8c..0000000000 --- a/core-deterministic/testing/src/test/kotlin/net/corda/core/internal/ClassLoadingUtils.kt +++ /dev/null @@ -1,9 +0,0 @@ -package net.corda.core.internal - -/** - * Stubbing out non-deterministic method. - */ -fun createInstancesOfClassesImplementing(@Suppress("UNUSED_PARAMETER") classloader: ClassLoader, @Suppress("UNUSED_PARAMETER") clazz: Class, - @Suppress("UNUSED_PARAMETER") classVersionRange: IntRange? = null): Set { - return emptySet() -} \ No newline at end of file diff --git a/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/CordaExceptionTest.kt b/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/CordaExceptionTest.kt deleted file mode 100644 index 3b95b952d2..0000000000 --- a/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/CordaExceptionTest.kt +++ /dev/null @@ -1,70 +0,0 @@ -package net.corda.deterministic - -import net.corda.core.CordaException -import net.corda.core.contracts.AttachmentResolutionException -import net.corda.core.contracts.TransactionResolutionException -import net.corda.core.contracts.TransactionVerificationException.* -import net.corda.core.crypto.SecureHash -import net.corda.core.identity.CordaX500Name -import net.corda.core.identity.Party -import org.junit.Assert.* -import org.junit.Test -import java.security.PublicKey -import kotlin.test.assertFailsWith - -class CordaExceptionTest { - companion object { - const val CONTRACT_CLASS = "com.r3.corda.contracts.TestContract" - val TEST_HASH = SecureHash.zeroHash - val TX_ID = SecureHash.allOnesHash - - val ALICE_NAME = CordaX500Name("Alice Corp", "Madrid", "ES") - val ALICE_KEY: PublicKey = object : PublicKey { - override fun getAlgorithm(): String = "TEST-256" - override fun getFormat(): String = "" - override fun getEncoded() = byteArrayOf() - } - val ALICE = Party(ALICE_NAME, ALICE_KEY) - - val BOB_NAME = CordaX500Name("Bob Plc", "Rome", "IT") - val BOB_KEY: PublicKey = object : PublicKey { - override fun getAlgorithm(): String = "TEST-512" - override fun getFormat(): String = "" - override fun getEncoded() = byteArrayOf() - } - val BOB = Party(BOB_NAME, BOB_KEY) - } - - @Test(timeout=300_000) - fun testCordaException() { - val ex = assertFailsWith { throw CordaException("BAD THING") } - assertEquals("BAD THING", ex.message) - } - - @Test(timeout=300_000) - fun testAttachmentResolutionException() { - val ex = assertFailsWith { throw AttachmentResolutionException(TEST_HASH) } - assertEquals(TEST_HASH, ex.hash) - } - - @Test(timeout=300_000) - fun testTransactionResolutionException() { - val ex = assertFailsWith { throw TransactionResolutionException(TEST_HASH) } - assertEquals(TEST_HASH, ex.hash) - } - - @Test(timeout=300_000) - fun testConflictingAttachmentsRejection() { - val ex = assertFailsWith { throw ConflictingAttachmentsRejection(TX_ID, CONTRACT_CLASS) } - assertEquals(TX_ID, ex.txId) - assertEquals(CONTRACT_CLASS, ex.contractClass) - } - - @Test(timeout=300_000) - fun testNotaryChangeInWrongTransactionType() { - val ex = assertFailsWith { throw NotaryChangeInWrongTransactionType(TX_ID, ALICE, BOB) } - assertEquals(TX_ID, ex.txId) - assertEquals(ALICE, ex.txNotary) - assertEquals(BOB, ex.outputNotary) - } -} \ No newline at end of file diff --git a/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/KeyStoreProvider.kt b/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/KeyStoreProvider.kt deleted file mode 100644 index 20929a557a..0000000000 --- a/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/KeyStoreProvider.kt +++ /dev/null @@ -1,44 +0,0 @@ -package net.corda.deterministic - -import org.junit.AssumptionViolatedException -import org.junit.rules.TestRule -import org.junit.runner.Description -import org.junit.runners.model.Statement -import java.security.KeyPair -import java.security.KeyStore -import java.security.PrivateKey -import java.security.cert.TrustAnchor -import java.security.cert.X509Certificate - -class KeyStoreProvider(private val storeName: String, private val storePassword: String) : TestRule { - private lateinit var keyStore: KeyStore - - private fun loadKeyStoreResource(resourceName: String, password: CharArray, type: String = "PKCS12"): KeyStore { - return KeyStore.getInstance(type).apply { - // Skip these tests if we cannot load the keystore. - val keyStream = KeyStoreProvider::class.java.classLoader.getResourceAsStream(resourceName) - ?: throw AssumptionViolatedException("KeyStore $resourceName not found") - keyStream.use { input -> - load(input, password) - } - } - } - - override fun apply(statement: Statement, description: Description?): Statement { - return object : Statement() { - override fun evaluate() { - keyStore = loadKeyStoreResource(storeName, storePassword.toCharArray()) - statement.evaluate() - } - } - } - - fun getKeyPair(alias: String): KeyPair { - val privateKey = keyStore.getKey(alias, storePassword.toCharArray()) as PrivateKey - return KeyPair(keyStore.getCertificate(alias).publicKey, privateKey) - } - - @Suppress("UNUSED") - fun trustAnchorsFor(vararg aliases: String): Set - = aliases.map { alias -> TrustAnchor(keyStore.getCertificate(alias) as X509Certificate, null) }.toSet() -} diff --git a/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/Utilities.kt b/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/Utilities.kt deleted file mode 100644 index bb7290206e..0000000000 --- a/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/Utilities.kt +++ /dev/null @@ -1,14 +0,0 @@ -package net.corda.deterministic - -import java.io.ByteArrayOutputStream -import java.io.IOException - -private val classLoader: ClassLoader = object {}.javaClass.classLoader - -@Throws(IOException::class) -fun bytesOfResource(resourceName: String): ByteArray { - return ByteArrayOutputStream().let { baos -> - classLoader.getResourceAsStream(resourceName).copyTo(baos) - baos.toByteArray() - } -} diff --git a/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/contracts/AttachmentTest.kt b/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/contracts/AttachmentTest.kt deleted file mode 100644 index 491aa33d9c..0000000000 --- a/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/contracts/AttachmentTest.kt +++ /dev/null @@ -1,79 +0,0 @@ -package net.corda.deterministic.contracts - -import net.corda.core.contracts.Attachment -import net.corda.core.crypto.SecureHash -import net.corda.core.identity.CordaX500Name -import net.corda.core.identity.Party -import org.junit.Assert.* -import org.junit.Before -import org.junit.Test -import java.io.ByteArrayOutputStream -import java.io.InputStream -import java.security.PublicKey -import java.util.jar.JarOutputStream -import java.util.zip.Deflater.* -import java.util.zip.ZipEntry - -class AttachmentTest { - private companion object { - private val data = byteArrayOf(0x73, 0x71, 0x18, 0x5F, 0x3A, 0x47, -0x22, 0x38) - private val jarData: ByteArray = ByteArrayOutputStream().let { baos -> - JarOutputStream(baos).use { jar -> - jar.setLevel(BEST_COMPRESSION) - jar.putNextEntry(ZipEntry("data.bin").apply { method = DEFLATED }) - data.inputStream().copyTo(jar) - } - baos.toByteArray() - } - - private val ALICE_NAME = CordaX500Name("Alice Corp", "Madrid", "ES") - private val ALICE_KEY: PublicKey = object : PublicKey { - override fun getAlgorithm(): String = "TEST-256" - override fun getFormat(): String = "" - override fun getEncoded() = byteArrayOf() - } - private val ALICE = Party(ALICE_NAME, ALICE_KEY) - } - - private lateinit var attachment: Attachment - - @Before - fun setup() { - attachment = object : Attachment { - override val signerKeys: List - get() = listOf(ALICE_KEY) - override val id: SecureHash - get() = SecureHash.allOnesHash - override val signers: List - get() = listOf(ALICE) - override val size: Int - get() = jarData.size - - override fun open(): InputStream { - return jarData.inputStream() - } - } - } - - @Test(timeout=300_000) - fun testAttachmentJar() { - attachment.openAsJAR().use { jar -> - val entry = jar.nextJarEntry ?: return@use - assertEquals("data.bin", entry.name) - val entryData = ByteArrayOutputStream().use { - jar.copyTo(it) - it.toByteArray() - } - assertArrayEquals(data, entryData) - } - } - - @Test(timeout=300_000) - fun testExtractFromAttachment() { - val resultData = ByteArrayOutputStream().use { - attachment.extractFile("data.bin", it) - it.toByteArray() - } - assertArrayEquals(data, resultData) - } -} \ No newline at end of file diff --git a/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/contracts/PrivacySaltTest.kt b/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/contracts/PrivacySaltTest.kt deleted file mode 100644 index 61fb7071a6..0000000000 --- a/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/contracts/PrivacySaltTest.kt +++ /dev/null @@ -1,28 +0,0 @@ -package net.corda.deterministic.contracts - -import net.corda.core.contracts.PrivacySalt -import org.junit.Test -import kotlin.test.* - -class PrivacySaltTest { - private companion object { - private const val SALT_SIZE = 32 - } - - @Test(timeout=300_000) - fun testValidSalt() { - PrivacySalt(ByteArray(SALT_SIZE) { 0x14 }) - } - - @Test(timeout=300_000) - fun testInvalidSaltWithAllZeros() { - val ex = assertFailsWith { PrivacySalt(ByteArray(SALT_SIZE)) } - assertEquals("Privacy salt should not be all zeros.", ex.message) - } - - @Test(timeout=300_000) - fun testTooShortPrivacySaltForSHA256() { - val ex = assertFailsWith { PrivacySalt(ByteArray(SALT_SIZE - 1) { 0x7f }) } - assertEquals("Privacy salt should be at least 32 bytes.", ex.message) - } -} diff --git a/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/contracts/UniqueIdentifierTest.kt b/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/contracts/UniqueIdentifierTest.kt deleted file mode 100644 index 1aed8eed0f..0000000000 --- a/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/contracts/UniqueIdentifierTest.kt +++ /dev/null @@ -1,37 +0,0 @@ -package net.corda.deterministic.contracts - -import net.corda.core.contracts.UniqueIdentifier -import org.assertj.core.api.Assertions.assertThat -import org.junit.Assert.* -import org.junit.Test -import java.util.* -import kotlin.reflect.full.primaryConstructor -import kotlin.test.assertFailsWith - -class UniqueIdentifierTest { - private companion object { - private const val NAME = "MyName" - private val TEST_UUID: UUID = UUID.fromString("00000000-1111-2222-3333-444444444444") - } - - @Test(timeout=300_000) - fun testNewInstance() { - val id = UniqueIdentifier(NAME, TEST_UUID) - assertEquals("${NAME}_$TEST_UUID", id.toString()) - assertEquals(NAME, id.externalId) - assertEquals(TEST_UUID, id.id) - } - - @Test(timeout=300_000) - fun testPrimaryConstructor() { - val primary = UniqueIdentifier::class.primaryConstructor ?: throw AssertionError("primary constructor missing") - assertThat(primary.call(NAME, TEST_UUID)).isEqualTo(UniqueIdentifier(NAME, TEST_UUID)) - } - - @Test(timeout=300_000) - fun testConstructors() { - assertEquals(1, UniqueIdentifier::class.constructors.size) - val ex = assertFailsWith { UniqueIdentifier::class.constructors.first().call() } - assertThat(ex).hasMessage("Callable expects 2 arguments, but 0 were provided.") - } -} \ No newline at end of file diff --git a/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/crypto/CryptoSignUtils.kt b/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/crypto/CryptoSignUtils.kt deleted file mode 100644 index 9351d06bc3..0000000000 --- a/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/crypto/CryptoSignUtils.kt +++ /dev/null @@ -1,68 +0,0 @@ -@file:JvmName("CryptoSignUtils") - -package net.corda.deterministic.crypto - -import net.corda.core.crypto.* -import net.corda.core.crypto.Crypto.findSignatureScheme -import net.corda.core.crypto.Crypto.isSupportedSignatureScheme -import net.corda.core.serialization.serialize -import java.security.* - -/** - * This is a slightly modified copy of signing utils from net.corda.core.crypto.Crypto, which are normally removed from DJVM. - * However, we need those for TransactionSignatureTest. - */ -object CryptoSignUtils { - @JvmStatic - @Throws(InvalidKeyException::class, SignatureException::class) - fun doSign(schemeCodeName: String, privateKey: PrivateKey, clearData: ByteArray): ByteArray { - return doSign(findSignatureScheme(schemeCodeName), privateKey, clearData) - } - - /** - * Generic way to sign [ByteArray] data with a [PrivateKey] and a known [Signature]. - * @param signatureScheme a [SignatureScheme] object, retrieved from supported signature schemes, see [Crypto]. - * @param privateKey the signer's [PrivateKey]. - * @param clearData the data/message to be signed in [ByteArray] form (usually the Merkle root). - * @return the digital signature (in [ByteArray]) on the input message. - * @throws IllegalArgumentException if the signature scheme is not supported for this private key. - * @throws InvalidKeyException if the private key is invalid. - * @throws SignatureException if signing is not possible due to malformed data or private key. - */ - @JvmStatic - @Throws(InvalidKeyException::class, SignatureException::class) - fun doSign(signatureScheme: SignatureScheme, privateKey: PrivateKey, clearData: ByteArray): ByteArray { - require(isSupportedSignatureScheme(signatureScheme)) { - "Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}" - } - require(clearData.isNotEmpty()) { "Signing of an empty array is not permitted!" } - val signature = Signature.getInstance(signatureScheme.signatureName, signatureScheme.providerName) - signature.initSign(privateKey) - signature.update(clearData) - return signature.sign() - } - - /** - * Generic way to sign [SignableData] objects with a [PrivateKey]. - * [SignableData] is a wrapper over the transaction's id (Merkle root) in order to attach extra information, such as - * a timestamp or partial and blind signature indicators. - * @param keyPair the signer's [KeyPair]. - * @param signableData a [SignableData] object that adds extra information to a transaction. - * @return a [TransactionSignature] object than contains the output of a successful signing, signer's public key and - * the signature metadata. - * @throws IllegalArgumentException if the signature scheme is not supported for this private key. - * @throws InvalidKeyException if the private key is invalid. - * @throws SignatureException if signing is not possible due to malformed data or private key. - */ - @JvmStatic - @Throws(InvalidKeyException::class, SignatureException::class) - fun doSign(keyPair: KeyPair, signableData: SignableData): TransactionSignature { - val sigKey: SignatureScheme = findSignatureScheme(keyPair.private) - val sigMetaData: SignatureScheme = findSignatureScheme(keyPair.public) - require(sigKey == sigMetaData) { - "Metadata schemeCodeName: ${sigMetaData.schemeCodeName} is not aligned with the key type: ${sigKey.schemeCodeName}." - } - val signatureBytes = doSign(sigKey.schemeCodeName, keyPair.private, signableData.serialize().bytes) - return TransactionSignature(signatureBytes, keyPair.public, signableData.signatureMetadata) - } -} diff --git a/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/crypto/MerkleTreeTest.kt b/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/crypto/MerkleTreeTest.kt deleted file mode 100644 index e2fcf99860..0000000000 --- a/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/crypto/MerkleTreeTest.kt +++ /dev/null @@ -1,50 +0,0 @@ -package net.corda.deterministic.crypto - -import net.corda.core.crypto.DigestService -import net.corda.core.crypto.MerkleTree -import net.corda.core.crypto.SecureHash -import org.junit.Assert.assertEquals -import org.junit.Test - -class MerkleTreeTest { - private fun leafs(algorithm : String) : List = - listOf(SecureHash.allOnesHashFor(algorithm), SecureHash.zeroHashFor(algorithm)) - - @Test(timeout=300_000) - fun testCreate() { - val merkle = MerkleTree.getMerkleTree(leafs(SecureHash.SHA2_256), DigestService.sha2_256) - assertEquals(SecureHash.create("A5DE9B714ACCD8AFAAABF1CBD6E1014C9D07FF95C2AE154D91EC68485B31E7B5"), merkle.hash) - } - - @Test(timeout=300_000) - fun `test create SHA2-384`() { - val merkle = MerkleTree.getMerkleTree(leafs(SecureHash.SHA2_384), DigestService.sha2_384) - assertEquals(SecureHash.create("SHA-384:2B83D37859E3665D7C239964D769CF950EE6478C13E4CA2D6643C23B6C4EAE035C88F654D22E0D65E7CA40BAE4F3718F"), merkle.hash) - } - - @Test(timeout=300_000) - fun `test create SHA2-256 to SHA2-384`() { - val merkle = MerkleTree.getMerkleTree(leafs(SecureHash.SHA2_256), DigestService.sha2_384) - assertEquals(SecureHash.create("SHA-384:02A4E8EA5AA4BBAFE80C0E7127B15994B84030BE8616EA2A0127D85203CF34221403635C08084A6BDDB1DB06333F0A49"), merkle.hash) - } - -// @Test(timeout=300_000) -// fun testCreateSHA3256() { -// val merkle = MerkleTree.getMerkleTree(listOf(SecureHash.allOnesHashFor(SecureHash.SHA3_256), -// SecureHash.zeroHashFor(SecureHash.SHA3_256)), DigestService.sha3_256) -// assertEquals(SecureHash.create("SHA3-256:80673DBEEC8F6761ACBB121E7E45F61D4279CCD8B8E2231741ECD0716F4C9EDC"), merkle.hash) -// } -// -// @Test(timeout=300_000) -// fun testCreateSHA2256toSHA3256() { -// val merkle = MerkleTree.getMerkleTree(listOf(SecureHash.allOnesHash, SecureHash.zeroHash), DigestService.sha3_256) -// assertEquals(SecureHash.create("SHA3-256:80673DBEEC8F6761ACBB121E7E45F61D4279CCD8B8E2231741ECD0716F4C9EDC"), merkle.hash) -// } -// -// @Test(timeout=300_000) -// fun testCreateSHA3256toSHA2256() { -// val merkle = MerkleTree.getMerkleTree(listOf(SecureHash.allOnesHashFor(SecureHash.SHA3_256), -// SecureHash.zeroHashFor(SecureHash.SHA3_256)), DigestService.sha2_256) -// assertEquals(SecureHash.create("A5DE9B714ACCD8AFAAABF1CBD6E1014C9D07FF95C2AE154D91EC68485B31E7B5"), merkle.hash) -// } -} \ No newline at end of file diff --git a/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/crypto/SecureHashTest.kt b/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/crypto/SecureHashTest.kt deleted file mode 100644 index 162b7cb488..0000000000 --- a/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/crypto/SecureHashTest.kt +++ /dev/null @@ -1,42 +0,0 @@ -package net.corda.deterministic.crypto - -import net.corda.core.crypto.SecureHash -import org.bouncycastle.util.encoders.Hex -import org.junit.Assert.* -import org.junit.Test -import java.security.MessageDigest - -class SecureHashTest { - @Test(timeout=300_000) - fun testSHA256() { - val hash = SecureHash.sha256(byteArrayOf(0x64, -0x13, 0x42, 0x3a)) - assertEquals(SecureHash.create("6D1687C143DF792A011A1E80670A4E4E0C25D0D87A39514409B1ABFC2043581F"), hash) - assertEquals("6D1687C143DF792A011A1E80670A4E4E0C25D0D87A39514409B1ABFC2043581F", hash.toString()) - } - - @Test(timeout=300_000) - fun testPrefix() { - val data = byteArrayOf(0x7d, 0x03, -0x21, 0x32, 0x56, 0x47) - val digest = data.digestFor("SHA-256") - val prefix = SecureHash.sha256(data).prefixChars(8) - assertEquals(Hex.toHexString(digest).substring(0, 8).toUpperCase(), prefix) - } - - @Test(timeout=300_000) - fun testConcat() { - val hash1 = SecureHash.sha256(byteArrayOf(0x7d, 0x03, -0x21, 0x32, 0x56, 0x47)) - val hash2 = SecureHash.sha256(byteArrayOf(0x63, 0x01, 0x7f, -0x29, 0x1e, 0x3c)) - val combined = hash1.hashConcat(hash2) - assertArrayEquals((hash1.bytes + hash2.bytes).digestFor("SHA-256"), combined.bytes) - } - - @Test(timeout=300_000) - fun testConstants() { - assertArrayEquals(SecureHash.zeroHash.bytes, ByteArray(32)) - assertArrayEquals(SecureHash.allOnesHash.bytes, ByteArray(32) { 0xFF.toByte() }) - } -} - -private fun ByteArray.digestFor(algorithm: String): ByteArray { - return MessageDigest.getInstance(algorithm).digest(this) -} \ No newline at end of file diff --git a/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/crypto/SecureRandomTest.kt b/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/crypto/SecureRandomTest.kt deleted file mode 100644 index d0c45cef39..0000000000 --- a/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/crypto/SecureRandomTest.kt +++ /dev/null @@ -1,22 +0,0 @@ -package net.corda.deterministic.crypto - -import net.corda.core.crypto.CordaSecurityProvider -import org.assertj.core.api.Assertions.assertThat -import org.junit.Test -import java.security.NoSuchAlgorithmException -import java.security.SecureRandom -import kotlin.test.assertFailsWith - -class SecureRandomTest { - private companion object { - init { - CordaSecurityProvider() - } - } - - @Test(timeout=300_000) - fun testNoCordaPRNG() { - val error = assertFailsWith { SecureRandom.getInstance("CordaPRNG") } - assertThat(error).hasMessage("CordaPRNG SecureRandom not available") - } -} \ No newline at end of file diff --git a/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/crypto/TransactionSignatureTest.kt b/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/crypto/TransactionSignatureTest.kt deleted file mode 100644 index a775acf8e2..0000000000 --- a/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/crypto/TransactionSignatureTest.kt +++ /dev/null @@ -1,143 +0,0 @@ -package net.corda.deterministic.crypto - -import net.corda.core.crypto.* -import net.corda.deterministic.KeyStoreProvider -import net.corda.deterministic.CheatingSecurityProvider -import net.corda.deterministic.verifier.LocalSerializationRule -import org.junit.* -import org.junit.rules.RuleChain -import java.security.* -import kotlin.test.* - -class TransactionSignatureTest { - companion object { - private const val KEYSTORE_PASSWORD = "deterministic" - private val testBytes = "12345678901234567890123456789012".toByteArray() - - private val keyStoreProvider = KeyStoreProvider("keystore/txsignature.pfx", KEYSTORE_PASSWORD) - private lateinit var keyPair: KeyPair - - @ClassRule - @JvmField - val rules: RuleChain = RuleChain.outerRule(LocalSerializationRule(TransactionSignatureTest::class)) - .around(keyStoreProvider) - - @BeforeClass - @JvmStatic - fun setupClass() { - keyPair = keyStoreProvider.getKeyPair("tx") - } - } - - /** Valid sign and verify. */ - @Test(timeout=300_000) - fun `Signature metadata full sign and verify`() { - // Create a SignableData object. - val signableData = SignableData(testBytes.sha256(), SignatureMetadata(1, Crypto.findSignatureScheme(keyPair.public).schemeNumberID)) - - // Sign the meta object. - val transactionSignature: TransactionSignature = CheatingSecurityProvider().use { - CryptoSignUtils.doSign(keyPair, signableData) - } - - // Check auto-verification. - assertTrue(transactionSignature.verify(testBytes.sha256())) - - // Check manual verification. - assertTrue(Crypto.doVerify(testBytes.sha256(), transactionSignature)) - } - - /** Verification should fail; corrupted metadata - clearData (Merkle root) has changed. */ - @Test(expected = SignatureException::class) - fun `Signature metadata full failure clearData has changed`() { - val signableData = SignableData(testBytes.sha256(), SignatureMetadata(1, Crypto.findSignatureScheme(keyPair.public).schemeNumberID)) - val transactionSignature = CheatingSecurityProvider().use { - CryptoSignUtils.doSign(keyPair, signableData) - } - Crypto.doVerify((testBytes + testBytes).sha256(), transactionSignature) - } - - @Test(timeout=300_000) - fun `Verify multi-tx signature`() { - // Deterministically create 5 txIds. - val txIds: List = IntRange(0, 4).map { byteArrayOf(it.toByte()).sha256() } - // Multi-tx signature. - val txSignature = signMultipleTx(txIds, keyPair) - - // The hash of all txIds are used as leaves. - val merkleTree = MerkleTree.getMerkleTree(txIds.map { it.sha256() }, DigestService.default) - - // We haven't added the partial tree yet. - assertNull(txSignature.partialMerkleTree) - // Because partial tree is still null, but we signed over a block of txs, verifying a single tx will fail. - assertFailsWith { Crypto.doVerify(txIds[3], txSignature) } - - // Create a partial tree for one tx. - val pmt = PartialMerkleTree.build(merkleTree, listOf(txIds[0].sha256())) - // Add the partial Merkle tree to the tx signature. - val txSignatureWithTree = TransactionSignature(txSignature.bytes, txSignature.by, txSignature.signatureMetadata, pmt) - - // Verify the corresponding txId with every possible way. - assertTrue(Crypto.doVerify(txIds[0], txSignatureWithTree)) - assertTrue(txSignatureWithTree.verify(txIds[0])) - assertTrue(Crypto.isValid(txIds[0], txSignatureWithTree)) - assertTrue(txSignatureWithTree.isValid(txIds[0])) - - // Verify the rest txs in the block, which are not included in the partial Merkle tree. - txIds.subList(1, txIds.size).forEach { - assertFailsWith { Crypto.doVerify(it, txSignatureWithTree) } - } - - // Test that the Merkle tree consists of hash(txId), not txId. - assertFailsWith { PartialMerkleTree.build(merkleTree, listOf(txIds[0])) } - - // What if we send the Full tree. This could be used if notaries didn't want to create a per tx partial tree. - // Create a partial tree for all txs, thus all leaves are included. - val pmtFull = PartialMerkleTree.build(merkleTree, txIds.map { it.sha256() }) - // Add the partial Merkle tree to the tx. - val txSignatureWithFullTree = TransactionSignature(txSignature.bytes, txSignature.by, txSignature.signatureMetadata, pmtFull) - - // All txs can be verified, as they are all included in the provided partial tree. - txIds.forEach { - assertTrue(Crypto.doVerify(it, txSignatureWithFullTree)) - } - } - - @Test(timeout=300_000) - fun `Verify one-tx signature`() { - val txId = "aTransaction".toByteArray().sha256() - // One-tx signature. - val txSignature = try { - signOneTx(txId, keyPair) - } catch (e: Throwable) { - e.cause?.printStackTrace() - throw e - } - - // partialMerkleTree should be null. - assertNull(txSignature.partialMerkleTree) - // Verify the corresponding txId with every possible way. - assertTrue(Crypto.doVerify(txId, txSignature)) - assertTrue(txSignature.verify(txId)) - assertTrue(Crypto.isValid(txId, txSignature)) - assertTrue(txSignature.isValid(txId)) - - // We signed the txId itself, not its hash (because it was a signature over one tx only and no partial tree has been received). - assertFailsWith { Crypto.doVerify(txId.sha256(), txSignature) } - } - - // Returns a TransactionSignature over the Merkle root, but the partial tree is null. - private fun signMultipleTx(txIds: List, keyPair: KeyPair): TransactionSignature { - val merkleTreeRoot = MerkleTree.getMerkleTree(txIds.map { it.sha256() }, DigestService.default).hash - return signOneTx(merkleTreeRoot, keyPair) - } - - // Returns a TransactionSignature over one SecureHash. - // Note that if one tx is to be signed, we don't create a Merkle tree and we directly sign over the txId. - private fun signOneTx(txId: SecureHash, keyPair: KeyPair): TransactionSignature { - val signableData = SignableData(txId, SignatureMetadata(3, Crypto.findSignatureScheme(keyPair.public).schemeNumberID)) - return CheatingSecurityProvider().use { - CryptoSignUtils.doSign(keyPair, signableData) - } - } -} diff --git a/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/transactions/TransactionWithSignaturesTest.kt b/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/transactions/TransactionWithSignaturesTest.kt deleted file mode 100644 index e092ee371d..0000000000 --- a/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/transactions/TransactionWithSignaturesTest.kt +++ /dev/null @@ -1,30 +0,0 @@ -package net.corda.deterministic.transactions - -import net.corda.core.crypto.SecureHash -import net.corda.core.crypto.TransactionSignature -import net.corda.core.transactions.TransactionWithSignatures -import org.junit.Test -import java.security.PublicKey - -class TransactionWithSignaturesTest { - @Test(timeout=300_000) - fun txWithSigs() { - val tx = object : TransactionWithSignatures { - override val id: SecureHash - get() = SecureHash.zeroHash - override val requiredSigningKeys: Set - get() = emptySet() - override val sigs: List - get() = emptyList() - - override fun getKeyDescriptions(keys: Set): List { - return emptyList() - } - } - tx.verifyRequiredSignatures() - tx.checkSignaturesAreValid() - tx.getMissingSigners() - tx.verifySignaturesExcept() - tx.verifySignaturesExcept(emptySet()) - } -} \ No newline at end of file diff --git a/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/txverify/VerifyTransactionTest.kt b/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/txverify/VerifyTransactionTest.kt deleted file mode 100644 index f5012485f2..0000000000 --- a/core-deterministic/testing/src/test/kotlin/net/corda/deterministic/txverify/VerifyTransactionTest.kt +++ /dev/null @@ -1,29 +0,0 @@ -package net.corda.deterministic.txverify - -import net.corda.deterministic.bytesOfResource -import net.corda.deterministic.verifier.LocalSerializationRule -import net.corda.deterministic.verifier.verifyTransaction -import net.corda.finance.contracts.asset.Cash.Commands.* -import org.assertj.core.api.Assertions.assertThat -import org.junit.ClassRule -import org.junit.Test -import kotlin.test.assertFailsWith - -class VerifyTransactionTest { - companion object { - @ClassRule - @JvmField - val serialization = LocalSerializationRule(VerifyTransactionTest::class) - } - - @Test(timeout=300_000) - fun success() { - verifyTransaction(bytesOfResource("txverify/tx-success.bin")) - } - - @Test(timeout=300_000) - fun failure() { - val e = assertFailsWith { verifyTransaction(bytesOfResource("txverify/tx-failure.bin")) } - assertThat(e).hasMessageContaining("Required ${Move::class.java.canonicalName} command") - } -} diff --git a/core-deterministic/testing/src/test/resources/log4j2-test.xml b/core-deterministic/testing/src/test/resources/log4j2-test.xml deleted file mode 100644 index 4e309bf567..0000000000 --- a/core-deterministic/testing/src/test/resources/log4j2-test.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - - - diff --git a/core-deterministic/testing/verifier/build.gradle b/core-deterministic/testing/verifier/build.gradle deleted file mode 100644 index 334191cb9f..0000000000 --- a/core-deterministic/testing/verifier/build.gradle +++ /dev/null @@ -1,56 +0,0 @@ -plugins { - id 'java-library' - id 'net.corda.plugins.publish-utils' - id 'com.jfrog.artifactory' - id 'idea' -} -apply from: "${rootProject.projectDir}/deterministic.gradle" - -description 'Test utilities for deterministic contract verification' - -configurations { - deterministicArtifacts { - canBeResolved = false - } - - // Compile against the deterministic artifacts to ensure that we use only the deterministic API subset. - compileOnly.extendsFrom deterministicArtifacts - runtimeArtifacts.extendsFrom api -} - -dependencies { - deterministicArtifacts project(path: ':serialization-deterministic', configuration: 'deterministicArtifacts') - deterministicArtifacts project(path: ':core-deterministic', configuration: 'deterministicArtifacts') - - runtimeArtifacts project(':serialization') - runtimeArtifacts project(':core') - - api "junit:junit:$junit_version" - runtimeOnly "org.junit.vintage:junit-vintage-engine:$junit_vintage_version" -} - -jar { - archiveBaseName = 'corda-deterministic-verifier' -} - -artifacts { - deterministicArtifacts jar - runtimeArtifacts jar - publish jar -} - -publish { - // Our published POM will contain dependencies on the non-deterministic Corda artifacts. - dependenciesFrom(configurations.runtimeArtifacts) { - defaultScope = 'compile' - } - name jar.archiveBaseName.get() -} - -idea { - module { - if (project.hasProperty("deterministic_idea_sdk")) { - jdkName project.property("deterministic_idea_sdk") as String - } - } -} diff --git a/core-deterministic/testing/verifier/src/main/kotlin/net/corda/deterministic/verifier/LocalSerializationRule.kt b/core-deterministic/testing/verifier/src/main/kotlin/net/corda/deterministic/verifier/LocalSerializationRule.kt deleted file mode 100644 index 35fbe9611c..0000000000 --- a/core-deterministic/testing/verifier/src/main/kotlin/net/corda/deterministic/verifier/LocalSerializationRule.kt +++ /dev/null @@ -1,82 +0,0 @@ -package net.corda.deterministic.verifier - -import net.corda.core.serialization.SerializationContext -import net.corda.core.serialization.SerializationContext.UseCase.P2P -import net.corda.core.serialization.SerializationCustomSerializer -import net.corda.core.serialization.SerializationWhitelist -import net.corda.core.serialization.internal.SerializationEnvironment -import net.corda.core.serialization.internal._driverSerializationEnv -import net.corda.serialization.internal.* -import net.corda.serialization.internal.amqp.* -import org.junit.rules.TestRule -import org.junit.runner.Description -import org.junit.runners.model.Statement -import kotlin.reflect.KClass -import kotlin.reflect.jvm.jvmName - -class LocalSerializationRule(private val label: String) : TestRule { - constructor(klass: KClass<*>) : this(klass.jvmName) - - private companion object { - private val AMQP_P2P_CONTEXT = SerializationContextImpl( - amqpMagic, - LocalSerializationRule::class.java.classLoader, - GlobalTransientClassWhiteList(BuiltInExceptionsWhitelist()), - emptyMap(), - true, - P2P, - null - ) - } - - override fun apply(base: Statement, description: Description): Statement { - return object : Statement() { - override fun evaluate() { - init() - try { - base.evaluate() - } finally { - clear() - } - } - } - } - - fun reset() { - clear() - init() - } - - private fun init() { - _driverSerializationEnv.set(createTestSerializationEnv()) - } - - private fun clear() { - _driverSerializationEnv.set(null) - } - - private fun createTestSerializationEnv(): SerializationEnvironment { - val factory = SerializationFactoryImpl(mutableMapOf()).apply { - registerScheme(AMQPSerializationScheme(emptySet(), emptySet(), AccessOrderLinkedHashMap(128))) - } - return SerializationEnvironment.with(factory, AMQP_P2P_CONTEXT) - } - - private class AMQPSerializationScheme( - cordappCustomSerializers: Set>, - cordappSerializationWhitelists: Set, - serializerFactoriesForContexts: AccessOrderLinkedHashMap - ) : AbstractAMQPSerializationScheme(cordappCustomSerializers, cordappSerializationWhitelists, serializerFactoriesForContexts) { - override fun rpcServerSerializerFactory(context: SerializationContext): SerializerFactory { - throw UnsupportedOperationException() - } - - override fun rpcClientSerializerFactory(context: SerializationContext): SerializerFactory { - throw UnsupportedOperationException() - } - - override fun canDeserializeVersion(magic: CordaSerializationMagic, target: SerializationContext.UseCase): Boolean { - return canDeserializeVersion(magic) && target == P2P - } - } -} diff --git a/core-deterministic/testing/verifier/src/main/kotlin/net/corda/deterministic/verifier/MockContractAttachment.kt b/core-deterministic/testing/verifier/src/main/kotlin/net/corda/deterministic/verifier/MockContractAttachment.kt deleted file mode 100644 index 70b5bc2c10..0000000000 --- a/core-deterministic/testing/verifier/src/main/kotlin/net/corda/deterministic/verifier/MockContractAttachment.kt +++ /dev/null @@ -1,19 +0,0 @@ -package net.corda.deterministic.verifier - -import net.corda.core.contracts.ContractClassName -import net.corda.core.crypto.SecureHash -import net.corda.core.identity.Party -import net.corda.core.internal.AbstractAttachment -import net.corda.core.serialization.CordaSerializable -import java.security.PublicKey - -// A valid zip file with 1 entry. -val simpleZip = byteArrayOf(80, 75, 3, 4, 20, 0, 8, 8, 8, 0, 15, 113, 79, 78, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 4, 0, 47, 97, -2, -54, 0, 0, 75, 4, 0, 80, 75, 7, 8, 67, -66, -73, -24, 3, 0, 0, 0, 1, 0, 0, 0, 80, 75, 1, 2, 20, 0, 20, 0, 8, 8, 8, 0, 15, 113, 79, 78, 67, -66, -73, -24, 3, 0, 0, 0, 1, 0, 0, 0, 2, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 47, 97, -2, -54, 0, 0, 80, 75, 5, 6, 0, 0, 0, 0, 1, 0, 1, 0, 52, 0, 0, 0, 55, 0, 0, 0, 0, 0) - -@CordaSerializable -class MockContractAttachment( - override val id: SecureHash = SecureHash.zeroHash, - val contract: ContractClassName, - override val signerKeys: List = emptyList(), - override val signers: List = emptyList() -) : AbstractAttachment({ simpleZip }, "app") \ No newline at end of file diff --git a/core-deterministic/testing/verifier/src/main/kotlin/net/corda/deterministic/verifier/SampleData.kt b/core-deterministic/testing/verifier/src/main/kotlin/net/corda/deterministic/verifier/SampleData.kt deleted file mode 100644 index 9c4cfdcb59..0000000000 --- a/core-deterministic/testing/verifier/src/main/kotlin/net/corda/deterministic/verifier/SampleData.kt +++ /dev/null @@ -1,6 +0,0 @@ -@file:JvmName("SampleData") -package net.corda.deterministic.verifier - -import net.corda.core.contracts.TypeOnlyCommandData - -object SampleCommandData : TypeOnlyCommandData() diff --git a/core-deterministic/testing/verifier/src/main/kotlin/net/corda/deterministic/verifier/TransactionVerificationRequest.kt b/core-deterministic/testing/verifier/src/main/kotlin/net/corda/deterministic/verifier/TransactionVerificationRequest.kt deleted file mode 100644 index 3c9fde9c06..0000000000 --- a/core-deterministic/testing/verifier/src/main/kotlin/net/corda/deterministic/verifier/TransactionVerificationRequest.kt +++ /dev/null @@ -1,35 +0,0 @@ -package net.corda.deterministic.verifier - -import net.corda.core.contracts.Attachment -import net.corda.core.contracts.ContractAttachment -import net.corda.core.internal.DEPLOYED_CORDAPP_UPLOADER -import net.corda.core.internal.toLtxDjvmInternal -import net.corda.core.node.NetworkParameters -import net.corda.core.serialization.CordaSerializable -import net.corda.core.serialization.SerializedBytes -import net.corda.core.serialization.deserialize -import net.corda.core.transactions.LedgerTransaction -import net.corda.core.transactions.WireTransaction - -@Suppress("MemberVisibilityCanBePrivate") -//TODO the use of deprecated toLedgerTransaction need to be revisited as resolveContractAttachment requires attachments of the transactions which created input states... -//TODO ...to check contract version non downgrade rule, currently dummy Attachment if not fund is used which sets contract version to '1' -@CordaSerializable -class TransactionVerificationRequest(val wtxToVerify: SerializedBytes, - val dependencies: Array>, - val attachments: Array, - val networkParameters: SerializedBytes) { - fun toLedgerTransaction(): LedgerTransaction { - val deps = dependencies.map { it.deserialize() }.associateBy(WireTransaction::id) - val attachments = attachments.map { it.deserialize() } - val attachmentMap = attachments - .mapNotNull { it as? MockContractAttachment } - .associateBy(Attachment::id) { ContractAttachment(it, it.contract, uploader = DEPLOYED_CORDAPP_UPLOADER) } - @Suppress("DEPRECATION") - return wtxToVerify.deserialize().toLtxDjvmInternal( - resolveAttachment = { attachmentMap[it] }, - resolveStateRef = { deps[it.txhash]?.outputs?.get(it.index) }, - resolveParameters = { networkParameters.deserialize() } - ) - } -} diff --git a/core-deterministic/testing/verifier/src/main/kotlin/net/corda/deterministic/verifier/Verifier.kt b/core-deterministic/testing/verifier/src/main/kotlin/net/corda/deterministic/verifier/Verifier.kt deleted file mode 100644 index e7a710d707..0000000000 --- a/core-deterministic/testing/verifier/src/main/kotlin/net/corda/deterministic/verifier/Verifier.kt +++ /dev/null @@ -1,21 +0,0 @@ -@file:JvmName("Verifier") -package net.corda.deterministic.verifier - -import net.corda.core.serialization.deserialize -import net.corda.core.transactions.LedgerTransaction - -/** - * We assume the signatures were already checked outside the sandbox: the purpose of this code - * is simply to check the sensitive, app-specific parts of a transaction. - * - * TODO: Transaction data is meant to be encrypted under an enclave-private key. - */ -@Throws(Exception::class) -fun verifyTransaction(reqBytes: ByteArray) { - deserialize(reqBytes).verify() -} - -private fun deserialize(reqBytes: ByteArray): LedgerTransaction { - return reqBytes.deserialize() - .toLedgerTransaction() -} diff --git a/core/build.gradle b/core/build.gradle index fc022ad5e9..36ca45311b 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -9,7 +9,6 @@ apply plugin: 'com.jfrog.artifactory' description 'Corda core' -// required by DJVM and Avian JVM (for running inside the SGX enclave) which only supports Java 8. targetCompatibility = VERSION_1_8 sourceSets { diff --git a/core/src/main/java/net/corda/core/crypto/Base58.java b/core/src/main/java/net/corda/core/crypto/Base58.java index 0e259eb9a8..80f58689c0 100644 --- a/core/src/main/java/net/corda/core/crypto/Base58.java +++ b/core/src/main/java/net/corda/core/crypto/Base58.java @@ -1,7 +1,5 @@ package net.corda.core.crypto; -import net.corda.core.KeepForDJVM; - import java.math.BigInteger; import java.util.Arrays; @@ -29,7 +27,6 @@ import java.util.Arrays; * NB: This class originally comes from the Apache licensed bitcoinj library. The original author of this code is the * same as the original author of the R3 repository. */ -@KeepForDJVM public class Base58 { private static final char[] ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz".toCharArray(); private static final char ENCODED_ZERO = ALPHABET[0]; diff --git a/core/src/main/java/net/corda/core/flows/IdentifiableException.java b/core/src/main/java/net/corda/core/flows/IdentifiableException.java index 2e87a8957b..93c9549685 100644 --- a/core/src/main/java/net/corda/core/flows/IdentifiableException.java +++ b/core/src/main/java/net/corda/core/flows/IdentifiableException.java @@ -1,14 +1,11 @@ package net.corda.core.flows; -import net.corda.core.KeepForDJVM; - import javax.annotation.Nullable; /** * An exception that may be identified with an ID. If an exception originates in a counter-flow this ID will be * propagated. This allows correlation of error conditions across different flows. */ -@KeepForDJVM public interface IdentifiableException { /** * @return the ID of the error, or null if the error doesn't have it set (yet). diff --git a/core/src/main/kotlin/net/corda/core/ClientRelevantError.kt b/core/src/main/kotlin/net/corda/core/ClientRelevantError.kt index 59c4b38438..a7e4d20ad3 100644 --- a/core/src/main/kotlin/net/corda/core/ClientRelevantError.kt +++ b/core/src/main/kotlin/net/corda/core/ClientRelevantError.kt @@ -6,6 +6,5 @@ import net.corda.core.serialization.CordaSerializable * Allows an implementing [Throwable] to be propagated to clients. */ @CordaSerializable -@KeepForDJVM @Deprecated("This is no longer used as the exception obfuscation feature is no longer available.") interface ClientRelevantError \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/CordaException.kt b/core/src/main/kotlin/net/corda/core/CordaException.kt index 7b5f822452..52bbd82172 100644 --- a/core/src/main/kotlin/net/corda/core/CordaException.kt +++ b/core/src/main/kotlin/net/corda/core/CordaException.kt @@ -4,7 +4,6 @@ import net.corda.core.serialization.CordaSerializable import java.util.* @CordaSerializable -@KeepForDJVM interface CordaThrowable { var originalExceptionClassName: String? val originalMessage: String? @@ -13,7 +12,6 @@ interface CordaThrowable { fun addSuppressed(suppressed: Array) } -@KeepForDJVM open class CordaException internal constructor(override var originalExceptionClassName: String? = null, private var _message: String? = null, private var _cause: Throwable? = null) : Exception(null, null, true, true), CordaThrowable { @@ -61,7 +59,6 @@ open class CordaException internal constructor(override var originalExceptionCla } } -@KeepForDJVM open class CordaRuntimeException(override var originalExceptionClassName: String?, private var _message: String?, private var _cause: Throwable?) : RuntimeException(null, null, true, true), CordaThrowable { diff --git a/core/src/main/kotlin/net/corda/core/CordaOID.kt b/core/src/main/kotlin/net/corda/core/CordaOID.kt index 0a6bf3b658..60a9e09281 100644 --- a/core/src/main/kotlin/net/corda/core/CordaOID.kt +++ b/core/src/main/kotlin/net/corda/core/CordaOID.kt @@ -6,7 +6,6 @@ import net.corda.core.crypto.internal.AliasPrivateKey * OIDs used for the Corda platform. All entries MUST be defined in this file only and they MUST NOT be removed. * If an OID is incorrectly assigned, it should be marked deprecated and NEVER be reused again. */ -@KeepForDJVM object CordaOID { /** Assigned to R3, see http://www.oid-info.com/cgi-bin/display?oid=1.3.6.1.4.1.50530&action=display */ const val R3_ROOT = "1.3.6.1.4.1.50530" diff --git a/core/src/main/kotlin/net/corda/core/DeleteForDJVM.kt b/core/src/main/kotlin/net/corda/core/DeleteForDJVM.kt deleted file mode 100644 index b4b92d1df8..0000000000 --- a/core/src/main/kotlin/net/corda/core/DeleteForDJVM.kt +++ /dev/null @@ -1,24 +0,0 @@ -package net.corda.core - -import kotlin.annotation.AnnotationRetention.BINARY -import kotlin.annotation.AnnotationTarget.* - -/** - * Declare the annotated element to unsuitable for the deterministic version of Corda. - */ -// DOCSTART 01 -@Target( - FILE, - CLASS, - CONSTRUCTOR, - FUNCTION, - PROPERTY_GETTER, - PROPERTY_SETTER, - PROPERTY, - FIELD, - TYPEALIAS -) -@Retention(BINARY) -@CordaInternal -annotation class DeleteForDJVM -// DOCEND 01 diff --git a/core/src/main/kotlin/net/corda/core/KeepForDJVM.kt b/core/src/main/kotlin/net/corda/core/KeepForDJVM.kt deleted file mode 100644 index beaa8281be..0000000000 --- a/core/src/main/kotlin/net/corda/core/KeepForDJVM.kt +++ /dev/null @@ -1,18 +0,0 @@ -package net.corda.core - -import kotlin.annotation.AnnotationRetention.BINARY -import kotlin.annotation.AnnotationTarget.CLASS -import kotlin.annotation.AnnotationTarget.FILE - -/** - * This annotates a class or file that we want to include into the deterministic version of Corda Core. - * We don't expect everything within that class/file to be deterministic; those non-deterministic - * elements need to be annotated with either [DeleteForDJVM] or [StubOutForDJVM] so that they - * can be deleted. - */ -// DOCSTART 01 -@Target(FILE, CLASS) -@Retention(BINARY) -@CordaInternal -annotation class KeepForDJVM -// DOCEND 01 \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/StubOutForDJVM.kt b/core/src/main/kotlin/net/corda/core/StubOutForDJVM.kt deleted file mode 100644 index c38c38569a..0000000000 --- a/core/src/main/kotlin/net/corda/core/StubOutForDJVM.kt +++ /dev/null @@ -1,22 +0,0 @@ -package net.corda.core - -import kotlin.annotation.AnnotationRetention.BINARY -import kotlin.annotation.AnnotationTarget.* - -/** - * We expect that almost every non-deterministic element can have its bytecode - * deleted entirely from the deterministic version of Corda. This annotation is - * for those (hopefully!) few occasions where the non-deterministic function - * cannot be deleted. In these cases, the function will be stubbed out instead. - */ -// DOCSTART 01 -@Target( - CONSTRUCTOR, - FUNCTION, - PROPERTY_GETTER, - PROPERTY_SETTER -) -@Retention(BINARY) -@CordaInternal -annotation class StubOutForDJVM -// DOCEND 01 diff --git a/core/src/main/kotlin/net/corda/core/context/InvocationContext.kt b/core/src/main/kotlin/net/corda/core/context/InvocationContext.kt index dbcfd0c192..7c3afc1c24 100644 --- a/core/src/main/kotlin/net/corda/core/context/InvocationContext.kt +++ b/core/src/main/kotlin/net/corda/core/context/InvocationContext.kt @@ -1,7 +1,5 @@ package net.corda.core.context -import net.corda.core.DeleteForDJVM -import net.corda.core.KeepForDJVM import net.corda.core.contracts.ScheduledStateRef import net.corda.core.identity.CordaX500Name import net.corda.core.internal.telemetry.SerializedTelemetry @@ -52,7 +50,6 @@ data class InvocationContext( /** * Creates an [InvocationContext] with a [Trace] that defaults to a [java.util.UUID] as value and [java.time.Instant.now] timestamp. */ - @DeleteForDJVM @JvmStatic @JvmOverloads @Suppress("LongParameterList") @@ -70,7 +67,6 @@ data class InvocationContext( /** * Creates an [InvocationContext] with [InvocationOrigin.RPC] origin. */ - @DeleteForDJVM @JvmStatic @JvmOverloads @Suppress("LongParameterList") @@ -86,28 +82,24 @@ data class InvocationContext( /** * Creates an [InvocationContext] with [InvocationOrigin.Peer] origin. */ - @DeleteForDJVM @JvmStatic fun peer(party: CordaX500Name, trace: Trace = Trace.newInstance(), externalTrace: Trace? = null, impersonatedActor: Actor? = null): InvocationContext = newInstance(InvocationOrigin.Peer(party), trace, null, externalTrace, impersonatedActor) /** * Creates an [InvocationContext] with [InvocationOrigin.Service] origin. */ - @DeleteForDJVM @JvmStatic fun service(serviceClassName: String, owningLegalIdentity: CordaX500Name, trace: Trace = Trace.newInstance(), externalTrace: Trace? = null): InvocationContext = newInstance(InvocationOrigin.Service(serviceClassName, owningLegalIdentity), trace, null, externalTrace) /** * Creates an [InvocationContext] with [InvocationOrigin.Scheduled] origin. */ - @DeleteForDJVM @JvmStatic fun scheduled(scheduledState: ScheduledStateRef, trace: Trace = Trace.newInstance(), externalTrace: Trace? = null): InvocationContext = newInstance(InvocationOrigin.Scheduled(scheduledState), trace, null, externalTrace) /** * Creates an [InvocationContext] with [InvocationOrigin.Shell] origin. */ - @DeleteForDJVM @JvmStatic fun shell(trace: Trace = Trace.newInstance(), externalTrace: Trace? = null): InvocationContext = InvocationContext(InvocationOrigin.Shell, trace, null, externalTrace) } @@ -161,7 +153,6 @@ data class InvocationContext( /** * Models an initiator in Corda, can be a user, a service, etc. */ -@KeepForDJVM @CordaSerializable data class Actor(val id: Id, val serviceId: AuthServiceId, val owningLegalIdentity: CordaX500Name) { @@ -173,7 +164,6 @@ data class Actor(val id: Id, val serviceId: AuthServiceId, val owningLegalIdenti /** * Actor id. */ - @KeepForDJVM @CordaSerializable data class Id(val value: String) } @@ -181,7 +171,6 @@ data class Actor(val id: Id, val serviceId: AuthServiceId, val owningLegalIdenti /** * Represents the source of an action such as a flow start, an RPC, a shell command etc. */ -@DeleteForDJVM @CordaSerializable sealed class InvocationOrigin { /** @@ -230,6 +219,5 @@ sealed class InvocationOrigin { /** * Authentication / Authorisation Service ID. */ -@KeepForDJVM @CordaSerializable -data class AuthServiceId(val value: String) \ No newline at end of file +data class AuthServiceId(val value: String) diff --git a/core/src/main/kotlin/net/corda/core/context/Trace.kt b/core/src/main/kotlin/net/corda/core/context/Trace.kt index 281d7fae28..6774460570 100644 --- a/core/src/main/kotlin/net/corda/core/context/Trace.kt +++ b/core/src/main/kotlin/net/corda/core/context/Trace.kt @@ -1,6 +1,5 @@ package net.corda.core.context -import net.corda.core.DeleteForDJVM import net.corda.core.serialization.CordaSerializable import net.corda.core.utilities.Id import net.corda.core.utilities.UuidGenerator @@ -17,7 +16,6 @@ data class Trace(val invocationId: InvocationId, val sessionId: SessionId) { /** * Creates a trace using a [InvocationId.newInstance] with default arguments and a [SessionId] matching the value and timestamp from the invocation id.. */ - @DeleteForDJVM @JvmStatic fun newInstance(invocationId: InvocationId = InvocationId.newInstance(), sessionId: SessionId = SessionId(invocationId.value, invocationId.timestamp)) = Trace(invocationId, sessionId) } @@ -34,7 +32,6 @@ data class Trace(val invocationId: InvocationId, val sessionId: SessionId) { /** * Creates an invocation id using a [java.util.UUID] as value and [Instant.now] as timestamp. */ - @DeleteForDJVM @JvmStatic fun newInstance(value: String = UuidGenerator.next().toString(), timestamp: Instant = Instant.now()) = InvocationId(value, timestamp) } @@ -52,9 +49,8 @@ data class Trace(val invocationId: InvocationId, val sessionId: SessionId) { /** * Creates a session id using a [java.util.UUID] as value and [Instant.now] as timestamp. */ - @DeleteForDJVM @JvmStatic fun newInstance(value: String = UuidGenerator.next().toString(), timestamp: Instant = Instant.now()) = SessionId(value, timestamp) } } -} \ No newline at end of file +} diff --git a/core/src/main/kotlin/net/corda/core/contracts/Amount.kt b/core/src/main/kotlin/net/corda/core/contracts/Amount.kt index d010ab5aa5..3355954ca8 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/Amount.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/Amount.kt @@ -1,6 +1,5 @@ package net.corda.core.contracts -import net.corda.core.KeepForDJVM import net.corda.core.crypto.CompositeKey import net.corda.core.identity.Party import net.corda.core.serialization.CordaSerializable @@ -37,7 +36,6 @@ interface TokenizableAssetInfo { * @property token the type of token this is an amount of. This is usually a singleton. * @param T the type of the token, for example [Currency]. T should implement [TokenizableAssetInfo] if automatic conversion to/from a display format is required. */ -@KeepForDJVM @CordaSerializable data class Amount(val quantity: Long, val displayTokenSize: BigDecimal, val token: T) : Comparable> { // TODO Proper lookup of currencies in a locale and context sensitive fashion is not supported and is left to the application. diff --git a/core/src/main/kotlin/net/corda/core/contracts/Attachment.kt b/core/src/main/kotlin/net/corda/core/contracts/Attachment.kt index 9d95250b56..4cb6c42ba6 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/Attachment.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/Attachment.kt @@ -1,7 +1,6 @@ package net.corda.core.contracts import net.corda.core.DoNotImplement -import net.corda.core.KeepForDJVM import net.corda.core.identity.Party import net.corda.core.internal.extractFile import net.corda.core.serialization.CordaSerializable @@ -31,7 +30,6 @@ import java.util.jar.JarInputStream * Finally, using ZIPs ensures files have a timestamp associated with them, and enables informational attachments * to be password protected (although in current releases password protected ZIPs are likely to fail to work). */ -@KeepForDJVM @CordaSerializable @DoNotImplement interface Attachment : NamedByHash { diff --git a/core/src/main/kotlin/net/corda/core/contracts/AttachmentConstraint.kt b/core/src/main/kotlin/net/corda/core/contracts/AttachmentConstraint.kt index 0540933683..e7efccdde9 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/AttachmentConstraint.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/AttachmentConstraint.kt @@ -2,7 +2,6 @@ package net.corda.core.contracts import net.corda.core.CordaInternal import net.corda.core.DoNotImplement -import net.corda.core.KeepForDJVM import net.corda.core.contracts.AlwaysAcceptAttachmentConstraint.isSatisfiedBy import net.corda.core.crypto.SecureHash import net.corda.core.crypto.isFulfilledBy @@ -38,7 +37,6 @@ interface AttachmentConstraint { } /** An [AttachmentConstraint] where [isSatisfiedBy] always returns true. */ -@KeepForDJVM object AlwaysAcceptAttachmentConstraint : AttachmentConstraint { override fun isSatisfiedBy(attachment: Attachment) = true } @@ -48,7 +46,6 @@ object AlwaysAcceptAttachmentConstraint : AttachmentConstraint { * The state protected by this constraint can only be used in a transaction created with that version of the jar. * And a receiving node will only accept it if a cordapp with that hash has (is) been deployed on the node. */ -@KeepForDJVM data class HashAttachmentConstraint(val attachmentId: SecureHash) : AttachmentConstraint { companion object { val disableHashConstraints = System.getProperty("net.corda.node.disableHashConstraints")?.toBoolean() ?: false @@ -69,7 +66,6 @@ data class HashAttachmentConstraint(val attachmentId: SecureHash) : AttachmentCo * See: [net.corda.core.node.NetworkParameters.whitelistedContractImplementations] * It allows for centralized control over the cordapps that can be used. */ -@KeepForDJVM object WhitelistedByZoneAttachmentConstraint : AttachmentConstraint { override fun isSatisfiedBy(attachment: Attachment): Boolean { return if (attachment is AttachmentWithContext) { @@ -83,7 +79,6 @@ object WhitelistedByZoneAttachmentConstraint : AttachmentConstraint { } } -@KeepForDJVM @Deprecated( "The name is no longer valid as multiple constraints were added.", replaceWith = ReplaceWith("AutomaticPlaceholderConstraint"), @@ -102,7 +97,6 @@ object AutomaticHashConstraint : AttachmentConstraint { * The resolution occurs in [TransactionBuilder.toWireTransaction] and is based on the input states and the attachments. * If the [Contract] was not annotated with [NoConstraintPropagation], then the platform will ensure the correct constraint propagation. */ -@KeepForDJVM object AutomaticPlaceholderConstraint : AttachmentConstraint { override fun isSatisfiedBy(attachment: Attachment): Boolean { throw UnsupportedOperationException("Contracts cannot be satisfied by an AutomaticPlaceholderConstraint placeholder.") @@ -115,7 +109,6 @@ object AutomaticPlaceholderConstraint : AttachmentConstraint { * * @property key A [PublicKey] that must be fulfilled by the owning keys of the attachment's signing parties. */ -@KeepForDJVM data class SignatureAttachmentConstraint(val key: PublicKey) : AttachmentConstraint { override fun isSatisfiedBy(attachment: Attachment): Boolean { log.debug("Checking signature constraints: verifying $key in contract attachment signer keys: ${attachment.signerKeys}") diff --git a/core/src/main/kotlin/net/corda/core/contracts/ContractAttachment.kt b/core/src/main/kotlin/net/corda/core/contracts/ContractAttachment.kt index 1b59a8c472..0b56707ee1 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/ContractAttachment.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/ContractAttachment.kt @@ -1,7 +1,6 @@ package net.corda.core.contracts import net.corda.core.CordaInternal -import net.corda.core.KeepForDJVM import net.corda.core.internal.cordapp.CordappImpl.Companion.DEFAULT_CORDAPP_VERSION import java.security.PublicKey @@ -12,7 +11,6 @@ import java.security.PublicKey * @property contract The contract name contained within the JAR. A Contract attachment has to contain at least 1 contract. * @property additionalContracts Additional contract names contained within the JAR. */ -@KeepForDJVM class ContractAttachment private constructor( val attachment: Attachment, val contract: ContractClassName, diff --git a/core/src/main/kotlin/net/corda/core/contracts/ContractState.kt b/core/src/main/kotlin/net/corda/core/contracts/ContractState.kt index 3b89363658..7cef40b7f6 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/ContractState.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/ContractState.kt @@ -1,6 +1,5 @@ package net.corda.core.contracts -import net.corda.core.KeepForDJVM import net.corda.core.identity.AbstractParty import net.corda.core.serialization.CordaSerializable @@ -12,7 +11,6 @@ import net.corda.core.serialization.CordaSerializable * notary is responsible for ensuring there is no "double spending" by only signing a transaction if the input states * are all free. */ -@KeepForDJVM @CordaSerializable interface ContractState { /** diff --git a/core/src/main/kotlin/net/corda/core/contracts/ContractsDSL.kt b/core/src/main/kotlin/net/corda/core/contracts/ContractsDSL.kt index ac0052c442..763af0163a 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/ContractsDSL.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/ContractsDSL.kt @@ -1,8 +1,6 @@ @file:JvmName("ContractsDSL") -@file:KeepForDJVM package net.corda.core.contracts -import net.corda.core.KeepForDJVM import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party import net.corda.core.internal.uncheckedCast @@ -18,7 +16,6 @@ import java.util.* //// Requirements ///////////////////////////////////////////////////////////////////////////////////////////////////// -@KeepForDJVM object Requirements { /** Throws [IllegalArgumentException] if the given expression evaluates to false. */ @Suppress("NOTHING_TO_INLINE") // Inlining this takes it out of our committed ABI. diff --git a/core/src/main/kotlin/net/corda/core/contracts/FungibleAsset.kt b/core/src/main/kotlin/net/corda/core/contracts/FungibleAsset.kt index 78878461f4..a787db3ed7 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/FungibleAsset.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/FungibleAsset.kt @@ -1,6 +1,5 @@ package net.corda.core.contracts -import net.corda.core.KeepForDJVM import net.corda.core.flows.FlowException import net.corda.core.identity.AbstractParty import net.corda.core.serialization.SerializableCalculatedProperty @@ -27,7 +26,6 @@ class InsufficientBalanceException(val amountMissing: Amount<*>) : FlowException * @param T a type that represents the asset in question. This should describe the basic type of the asset * (GBP, USD, oil, shares in company , etc.) and any additional metadata (issuer, grade, class, etc.). */ -@KeepForDJVM interface FungibleAsset : FungibleState>, OwnableState { /** * Amount represents a positive quantity of some issued product which can be cash, tokens, assets, or generally diff --git a/core/src/main/kotlin/net/corda/core/contracts/FungibleState.kt b/core/src/main/kotlin/net/corda/core/contracts/FungibleState.kt index 0e4a241ccd..38ea0656d6 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/FungibleState.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/FungibleState.kt @@ -1,7 +1,5 @@ package net.corda.core.contracts -import net.corda.core.KeepForDJVM - /** * Interface to represent things which are fungible, this means that there is an expectation that these things can * be split and merged. That's the only assumption made by this interface. @@ -25,7 +23,6 @@ import net.corda.core.KeepForDJVM * [TokenizableAssetInfo]. */ // DOCSTART 1 -@KeepForDJVM interface FungibleState : ContractState { /** * Amount represents a positive quantity of some token which can be cash, tokens, stock, agreements, or generally diff --git a/core/src/main/kotlin/net/corda/core/contracts/StatePointer.kt b/core/src/main/kotlin/net/corda/core/contracts/StatePointer.kt index 547eaab33d..b133ef165c 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/StatePointer.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/StatePointer.kt @@ -1,8 +1,6 @@ package net.corda.core.contracts -import net.corda.core.DeleteForDJVM import net.corda.core.DoNotImplement -import net.corda.core.KeepForDJVM import net.corda.core.node.ServiceHub import net.corda.core.node.services.Vault import net.corda.core.node.services.queryBy @@ -18,7 +16,6 @@ import net.corda.core.transactions.LedgerTransaction * [StaticPointer]s are for use with any type of [ContractState]. */ @CordaSerializable -@KeepForDJVM @DoNotImplement sealed class StatePointer { @@ -70,7 +67,6 @@ sealed class StatePointer { * * @param services a [ServiceHub] implementation is required to resolve the pointer. */ - @DeleteForDJVM abstract fun resolve(services: ServiceHub): StateAndRef /** @@ -89,7 +85,6 @@ sealed class StatePointer { * - The [ContractState] may not be known by the node performing the look-up in which case the [resolve] method will * throw a [TransactionResolutionException] */ -@KeepForDJVM class StaticPointer( override val pointer: StateRef, override val type: Class, @@ -110,7 +105,6 @@ class StaticPointer( */ @Throws(TransactionResolutionException::class) @Suppress("UNCHECKED_CAST") - @DeleteForDJVM override fun resolve(services: ServiceHub): StateAndRef { val transactionState = services.loadState(pointer) as TransactionState val castState: T = type.cast(transactionState.data) @@ -148,7 +142,6 @@ class StaticPointer( * then the transaction with such a reference state cannot be committed to the ledger until the most up-to-date version * of the [LinearState] is available. See reference states documentation on docs.corda.net for more info. */ -@KeepForDJVM class LinearPointer( override val pointer: UniqueIdentifier, override val type: Class, @@ -171,7 +164,6 @@ class LinearPointer( * @param services a [ServiceHub] implementation is required to perform a vault query. */ @Suppress("UNCHECKED_CAST") - @DeleteForDJVM override fun resolve(services: ServiceHub): StateAndRef { // Return the latest version of the linear state. // This query will only ever return one or zero states. diff --git a/core/src/main/kotlin/net/corda/core/contracts/Structures.kt b/core/src/main/kotlin/net/corda/core/contracts/Structures.kt index 9172236207..a678b28684 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/Structures.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/Structures.kt @@ -1,9 +1,6 @@ @file:JvmName("Structures") -@file:KeepForDJVM package net.corda.core.contracts -import net.corda.core.DeleteForDJVM -import net.corda.core.KeepForDJVM import net.corda.core.crypto.SecureHash import net.corda.core.crypto.secureRandomBytes import net.corda.core.crypto.toStringShort @@ -45,7 +42,6 @@ interface NamedByHash { * of product may differentiate different kinds of asset within the same logical class e.g the currency, or * it may just be a type marker for a single custom asset. */ -@KeepForDJVM @CordaSerializable data class Issued(val issuer: PartyAndReference, val product: P) { init { @@ -72,13 +68,11 @@ fun Amount>.withoutIssuer(): Amount = Amount(quantity, di /** * Return structure for [OwnableState.withNewOwner] */ -@KeepForDJVM data class CommandAndState(val command: CommandData, val ownableState: OwnableState) /** * A contract state that can have a single owner. */ -@KeepForDJVM interface OwnableState : ContractState { /** There must be a MoveCommand signed by this key to claim the amount. */ val owner: AbstractParty @@ -89,7 +83,6 @@ interface OwnableState : ContractState { // DOCEND 3 /** Something which is scheduled to happen at a point in time. */ -@KeepForDJVM interface Scheduled { val scheduledAt: Instant } @@ -102,7 +95,6 @@ interface Scheduled { * lifecycle processing needs to take place. e.g. a fixing or a late payment etc. */ @CordaSerializable -@KeepForDJVM data class ScheduledStateRef(val ref: StateRef, override val scheduledAt: Instant) : Scheduled /** @@ -117,7 +109,6 @@ data class ScheduledStateRef(val ref: StateRef, override val scheduledAt: Instan * for a particular [ContractState] have been processed/fired etc. If the activity is not "on ledger" then the * scheduled activity shouldn't be either. */ -@KeepForDJVM data class ScheduledActivity(val logicRef: FlowLogicRef, override val scheduledAt: Instant) : Scheduled // DOCSTART 2 @@ -126,7 +117,6 @@ data class ScheduledActivity(val logicRef: FlowLogicRef, override val scheduledA * * This simplifies the job of tracking the current version of certain types of state in e.g. a vault. */ -@KeepForDJVM interface LinearState : ContractState { /** * Unique id shared by all LinearState states throughout history within the vaults of all parties. @@ -136,7 +126,6 @@ interface LinearState : ContractState { val linearId: UniqueIdentifier } // DOCEND 2 -@KeepForDJVM interface SchedulableState : ContractState { /** * Indicate whether there is some activity to be performed at some future point in time with respect to this @@ -160,7 +149,6 @@ fun ContractState.hash(algorithm: String): SecureHash = SecureHash.hashAs(algori * A stateref is a pointer (reference) to a state, this is an equivalent of an "outpoint" in Bitcoin. It records which * transaction defined the state and where in that transaction it was. */ -@KeepForDJVM @CordaSerializable // DOCSTART 8 data class StateRef(val txhash: SecureHash, val index: Int) { @@ -169,7 +157,6 @@ data class StateRef(val txhash: SecureHash, val index: Int) { // DOCEND 8 /** A StateAndRef is simply a (state, ref) pair. For instance, a vault (which holds available assets) contains these. */ -@KeepForDJVM @CordaSerializable // DOCSTART 7 data class StateAndRef(val state: TransactionState, val ref: StateRef) { @@ -179,7 +166,6 @@ data class StateAndRef(val state: TransactionState, va // DOCEND 7 /** A wrapper for a [StateAndRef] indicating that it should be added to a transaction as a reference input state. */ -@KeepForDJVM data class ReferencedStateAndRef(val stateAndRef: StateAndRef) /** Filters a list of [StateAndRef] objects according to the type of the states */ @@ -191,7 +177,6 @@ inline fun Iterable>.filt * Reference to something being stored or issued by a party e.g. in a vault or (more likely) on their normal * ledger. The reference is intended to be encrypted so it's meaningless to anyone other than the party. */ -@KeepForDJVM @CordaSerializable data class PartyAndReference(val party: AbstractParty, val reference: OpaqueBytes) { override fun toString() = "$party$reference" @@ -202,14 +187,12 @@ data class PartyAndReference(val party: AbstractParty, val reference: OpaqueByte interface CommandData /** Commands that inherit from this are intended to have no data items: it's only their presence that matters. */ -@KeepForDJVM abstract class TypeOnlyCommandData : CommandData { override fun equals(other: Any?) = other?.javaClass == javaClass override fun hashCode() = javaClass.name.hashCode() } /** Command data/content plus pubkey pair: the signature is stored at the end of the serialized bytes */ -@KeepForDJVM @CordaSerializable data class Command(val value: T, val signers: List) { // TODO Introduce NonEmptyList? @@ -224,7 +207,6 @@ data class Command(val value: T, val signers: List) } /** A common move command for contract states which can change owner. */ -@KeepForDJVM interface MoveCommand : CommandData { /** * Contract code the moved state(s) are for the attention of, for example to indicate that the states are moved in @@ -236,7 +218,6 @@ interface MoveCommand : CommandData { // DOCSTART 6 /** A [Command] where the signing parties have been looked up if they have a well known/recognised institutional key. */ -@KeepForDJVM @CordaSerializable data class CommandWithParties( val signers: List, @@ -256,7 +237,6 @@ data class CommandWithParties( * * TODO: Contract serialization is likely to change, so the annotation is likely temporary. */ -@KeepForDJVM @CordaSerializable interface Contract { /** @@ -288,7 +268,6 @@ annotation class LegalProseReference(val uri: String) * more than one state). * @param NewState the upgraded contract state. */ -@KeepForDJVM interface UpgradedContract : Contract { /** * Name of the contract this is an upgraded version of, used as part of verification of upgrade transactions. @@ -307,7 +286,6 @@ interface UpgradedContract : UpgradedContract { /** * A validator for the legacy (pre-upgrade) contract attachments on the transaction. @@ -325,14 +303,11 @@ interface UpgradedContractWithLegacyConstraint) : this(txId, @@ -205,7 +188,6 @@ abstract class TransactionVerificationException(val txId: SecureHash, message: S * transactions are not supported and thus two encumbered states with different notaries cannot be consumed * in the same transaction. */ - @KeepForDJVM class TransactionNotaryMismatchEncumbranceException(txId: SecureHash, message: String) : TransactionVerificationException(txId, message, null) { constructor(txId: SecureHash, encumberedIndex: Int, encumbranceIndex: Int, encumberedNotary: Party, encumbranceNotary: Party) : @@ -222,7 +204,6 @@ abstract class TransactionVerificationException(val txId: SecureHash, message: S * @param state The [TransactionState] whose bundled state and contract are in conflict. * @param requiredContractClassName The class name of the contract to which the state belongs. */ - @KeepForDJVM class TransactionContractConflictException(txId: SecureHash, message: String) : TransactionVerificationException(txId, message, null) { constructor(txId: SecureHash, state: TransactionState, requiredContractClassName: String): this(txId, @@ -233,7 +214,6 @@ abstract class TransactionVerificationException(val txId: SecureHash, message: S } // TODO: add reference to documentation - @KeepForDJVM class TransactionRequiredContractUnspecifiedException(txId: SecureHash, message: String) : TransactionVerificationException(txId, message, null) { constructor(txId: SecureHash, state: TransactionState) : this(txId, @@ -247,7 +227,6 @@ abstract class TransactionVerificationException(val txId: SecureHash, message: S /** * If the network parameters associated with an input or reference state in a transaction are more recent than the network parameters of the new transaction itself. */ - @KeepForDJVM class TransactionNetworkParameterOrderingException(txId: SecureHash, message: String) : TransactionVerificationException(txId, message, null) { constructor(txId: SecureHash, @@ -265,7 +244,6 @@ abstract class TransactionVerificationException(val txId: SecureHash, message: S * @param txId Id of the transaction that has missing parameters hash in the resolution chain * @param missingNetworkParametersHash Missing hash of the network parameters associated to this transaction */ - @KeepForDJVM class MissingNetworkParametersException(txId: SecureHash, message: String) : TransactionVerificationException(txId, message, null) { constructor(txId: SecureHash, missingNetworkParametersHash: SecureHash) : @@ -275,13 +253,11 @@ abstract class TransactionVerificationException(val txId: SecureHash, message: S /** * @param txId Id of the transaction that Corda is no longer able to verify. */ - @KeepForDJVM class BrokenTransactionException(txId: SecureHash, message: String) : TransactionVerificationException(txId, message, null) /** Whether the inputs or outputs list contains an encumbrance issue, see [TransactionMissingEncumbranceException]. */ @CordaSerializable - @KeepForDJVM enum class Direction { /** Issue in the inputs list. */ INPUT, @@ -294,30 +270,25 @@ abstract class TransactionVerificationException(val txId: SecureHash, message: S // as a cause. /** @suppress This class is not used: duplicate inputs throw a [IllegalStateException] instead. */ @Deprecated("This class is not used: duplicate inputs throw a [IllegalStateException] instead.") - @DeleteForDJVM class DuplicateInputStates(txId: SecureHash, val duplicates: NonEmptySet) : TransactionVerificationException(txId, "Duplicate inputs: ${duplicates.joinToString()}", null) /** @suppress This class is obsolete and nothing has ever used it. */ @Deprecated("This class is obsolete and nothing has ever used it.") - @DeleteForDJVM class MoreThanOneNotary(txId: SecureHash) : TransactionVerificationException(txId, "More than one notary", null) /** @suppress This class is obsolete and nothing has ever used it. */ @Deprecated("This class is obsolete and nothing has ever used it.") - @DeleteForDJVM class SignersMissing(txId: SecureHash, val missing: List) : TransactionVerificationException(txId, "Signers missing: ${missing.joinToString()}", null) /** @suppress This class is obsolete and nothing has ever used it. */ @Deprecated("This class is obsolete and nothing has ever used it.") - @DeleteForDJVM class InvalidNotaryChange(txId: SecureHash) : TransactionVerificationException(txId, "Detected a notary change. Outputs must use the same notary as inputs", null) /** * Thrown when multiple attachments provide the same file when building the AttachmentsClassloader for a transaction. */ - @KeepForDJVM class OverlappingAttachmentsException(txId: SecureHash, val path: String) : TransactionVerificationException(txId, "Multiple attachments define a file at $path.", null) /** @@ -336,16 +307,12 @@ abstract class TransactionVerificationException(val txId: SecureHash, message: S // TODO: Make this descend from TransactionVerificationException so that untrusted attachments cause flows to be hospitalized. /** Thrown during classloading upon encountering an untrusted attachment (eg. not in the [TRUSTED_UPLOADERS] list) */ - @KeepForDJVM class UntrustedAttachmentsException(val txId: SecureHash, val ids: List) : CordaException("Attempting to load untrusted transaction attachments: $ids. " + - "At this time these are not loadable because the DJVM sandbox has not yet been integrated. " + "You will need to manually install the CorDapp to whitelist it for use.") - @KeepForDJVM class UnsupportedHashTypeException(txId: SecureHash) : TransactionVerificationException(txId, "The transaction Id is defined by an unsupported hash type", null) - @KeepForDJVM class AttachmentTooBigException(txId: SecureHash) : TransactionVerificationException( txId, "The transaction attachments are too large and exceed both max transaction size and the maximum allowed compression ratio", null) diff --git a/core/src/main/kotlin/net/corda/core/contracts/UniqueIdentifier.kt b/core/src/main/kotlin/net/corda/core/contracts/UniqueIdentifier.kt index ac4a390c34..20ef1ca7b9 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/UniqueIdentifier.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/UniqueIdentifier.kt @@ -1,7 +1,5 @@ package net.corda.core.contracts -import net.corda.core.DeleteForDJVM -import net.corda.core.KeepForDJVM import net.corda.core.internal.VisibleForTesting import net.corda.core.serialization.CordaSerializable import java.util.* @@ -19,8 +17,7 @@ import java.util.* * Subsequent copies and evolutions of a state should just copy the [externalId] and [id] fields unmodified. */ @CordaSerializable -@KeepForDJVM -data class UniqueIdentifier @JvmOverloads @DeleteForDJVM constructor(val externalId: String? = null, val id: UUID = UUID.randomUUID()) : Comparable { +data class UniqueIdentifier @JvmOverloads constructor(val externalId: String? = null, val id: UUID = UUID.randomUUID()) : Comparable { override fun toString(): String = if (externalId != null) "${externalId}_$id" else id.toString() companion object { diff --git a/core/src/main/kotlin/net/corda/core/cordapp/Cordapp.kt b/core/src/main/kotlin/net/corda/core/cordapp/Cordapp.kt index f10a17ef86..040a5455a8 100644 --- a/core/src/main/kotlin/net/corda/core/cordapp/Cordapp.kt +++ b/core/src/main/kotlin/net/corda/core/cordapp/Cordapp.kt @@ -1,6 +1,5 @@ package net.corda.core.cordapp -import net.corda.core.DeleteForDJVM import net.corda.core.DoNotImplement import net.corda.core.cordapp.Cordapp.Info.* import net.corda.core.crypto.SecureHash @@ -41,7 +40,6 @@ import java.net.URL * @property targetPlatformVersion The target platform version this CorDapp was designed and tested on. */ @DoNotImplement -@DeleteForDJVM interface Cordapp { val name: String val contractClassNames: List diff --git a/core/src/main/kotlin/net/corda/core/cordapp/CordappContext.kt b/core/src/main/kotlin/net/corda/core/cordapp/CordappContext.kt index da8790a66f..2441f04e3b 100644 --- a/core/src/main/kotlin/net/corda/core/cordapp/CordappContext.kt +++ b/core/src/main/kotlin/net/corda/core/cordapp/CordappContext.kt @@ -1,7 +1,6 @@ package net.corda.core.cordapp import net.corda.core.CordaInternal -import net.corda.core.DeleteForDJVM import net.corda.core.crypto.SecureHash import java.lang.UnsupportedOperationException @@ -18,7 +17,6 @@ import java.lang.UnsupportedOperationException * @property classLoader the classloader used to load this cordapp's classes * @property config Configuration for this CorDapp */ -@DeleteForDJVM class CordappContext private constructor( val cordapp: Cordapp, val attachmentId: SecureHash?, diff --git a/core/src/main/kotlin/net/corda/core/cordapp/CordappProvider.kt b/core/src/main/kotlin/net/corda/core/cordapp/CordappProvider.kt index bec2d6ab59..bf7864ee95 100644 --- a/core/src/main/kotlin/net/corda/core/cordapp/CordappProvider.kt +++ b/core/src/main/kotlin/net/corda/core/cordapp/CordappProvider.kt @@ -1,6 +1,5 @@ package net.corda.core.cordapp -import net.corda.core.DeleteForDJVM import net.corda.core.DoNotImplement import net.corda.core.contracts.ContractClassName import net.corda.core.node.services.AttachmentId @@ -9,7 +8,6 @@ import net.corda.core.node.services.AttachmentId * Provides access to what the node knows about loaded applications. */ @DoNotImplement -@DeleteForDJVM interface CordappProvider { /** * Exposes the current CorDapp context which will contain information and configuration of the CorDapp that diff --git a/core/src/main/kotlin/net/corda/core/crypto/CompositeKey.kt b/core/src/main/kotlin/net/corda/core/crypto/CompositeKey.kt index eb79aa9e57..e968257931 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/CompositeKey.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/CompositeKey.kt @@ -1,6 +1,5 @@ package net.corda.core.crypto -import net.corda.core.KeepForDJVM import net.corda.core.serialization.CordaSerializable import net.corda.core.utilities.exactAdd import net.corda.core.utilities.sequence @@ -34,7 +33,6 @@ import java.util.* * @property threshold specifies the minimum total weight required (in the simple case – the minimum number of child * signatures required) to satisfy the sub-tree rooted at this node. */ -@KeepForDJVM class CompositeKey private constructor(val threshold: Int, children: List) : PublicKey { companion object { const val KEY_ALGORITHM = "COMPOSITE" @@ -151,7 +149,6 @@ class CompositeKey private constructor(val threshold: Int, children: List, ASN1Object() { init { @@ -250,7 +247,6 @@ class CompositeKey private constructor(val threshold: Int, children: List = mutableListOf() diff --git a/core/src/main/kotlin/net/corda/core/crypto/CompositeKeyFactory.kt b/core/src/main/kotlin/net/corda/core/crypto/CompositeKeyFactory.kt index 7340e6e96b..df7fc7c433 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/CompositeKeyFactory.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/CompositeKeyFactory.kt @@ -1,6 +1,5 @@ package net.corda.core.crypto -import net.corda.core.KeepForDJVM import java.security.* import java.security.spec.InvalidKeySpecException import java.security.spec.KeySpec @@ -9,7 +8,6 @@ import java.security.spec.X509EncodedKeySpec /** * Factory for generating composite keys from ASN.1 format key specifications. This is used by [CordaSecurityProvider]. */ -@KeepForDJVM class CompositeKeyFactory : KeyFactorySpi() { @Throws(InvalidKeySpecException::class) diff --git a/core/src/main/kotlin/net/corda/core/crypto/CompositeSignature.kt b/core/src/main/kotlin/net/corda/core/crypto/CompositeSignature.kt index e960590c1d..8cac221158 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/CompositeSignature.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/CompositeSignature.kt @@ -1,6 +1,5 @@ package net.corda.core.crypto -import net.corda.core.KeepForDJVM import net.corda.core.serialization.deserialize import java.io.ByteArrayOutputStream import java.security.InvalidAlgorithmParameterException @@ -15,7 +14,6 @@ import java.security.spec.AlgorithmParameterSpec /** * Dedicated class for storing a set of signatures that comprise [CompositeKey]. */ -@KeepForDJVM class CompositeSignature : Signature(SIGNATURE_ALGORITHM) { companion object { const val SIGNATURE_ALGORITHM = "COMPOSITESIG" diff --git a/core/src/main/kotlin/net/corda/core/crypto/CompositeSignaturesWithKeys.kt b/core/src/main/kotlin/net/corda/core/crypto/CompositeSignaturesWithKeys.kt index 1175f80837..0ea62b1f0e 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/CompositeSignaturesWithKeys.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/CompositeSignaturesWithKeys.kt @@ -1,6 +1,5 @@ package net.corda.core.crypto -import net.corda.core.KeepForDJVM import net.corda.core.serialization.CordaSerializable /** @@ -8,7 +7,6 @@ import net.corda.core.serialization.CordaSerializable * serialization format. */ @CordaSerializable -@KeepForDJVM data class CompositeSignaturesWithKeys(val sigs: List) { companion object { val EMPTY = CompositeSignaturesWithKeys(emptyList()) diff --git a/core/src/main/kotlin/net/corda/core/crypto/CordaSecurityProvider.kt b/core/src/main/kotlin/net/corda/core/crypto/CordaSecurityProvider.kt index 91a6611e97..4530312eec 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/CordaSecurityProvider.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/CordaSecurityProvider.kt @@ -1,7 +1,5 @@ package net.corda.core.crypto -import net.corda.core.KeepForDJVM -import net.corda.core.StubOutForDJVM import net.corda.core.crypto.CordaObjectIdentifier.COMPOSITE_KEY import net.corda.core.crypto.CordaObjectIdentifier.COMPOSITE_SIGNATURE import net.corda.core.crypto.internal.PlatformSecureRandomService @@ -10,13 +8,14 @@ import java.security.Provider import java.util.* import java.util.concurrent.ConcurrentHashMap -@KeepForDJVM @Suppress("DEPRECATION") // JDK11: should replace with Provider(String name, double version, String info) (since 9) class CordaSecurityProvider : Provider(PROVIDER_NAME, 0.1, "$PROVIDER_NAME security provider wrapper") { companion object { const val PROVIDER_NAME = "Corda" } + private val services = ConcurrentHashMap, Optional>() + init { put("KeyFactory.${CompositeKey.KEY_ALGORITHM}", CompositeKeyFactory::class.java.name) put("Alg.Alias.KeyFactory.$COMPOSITE_KEY", CompositeKey.KEY_ALGORITHM) @@ -27,47 +26,19 @@ class CordaSecurityProvider : Provider(PROVIDER_NAME, 0.1, "$PROVIDER_NAME secur putPlatformSecureRandomService() } - @StubOutForDJVM private fun putPlatformSecureRandomService() { putService(PlatformSecureRandomService(this)) } - override fun getService(type: String, algorithm: String): Service? = serviceFactory(type, algorithm) - - // Used to work around banning of ConcurrentHashMap in DJVM - @Suppress("TooGenericExceptionCaught") - private val serviceFactory: (String, String) -> Service? = try { - // Will throw UnsupportedOperationException in DJVM - makeCachingFactory() - } catch (e: Exception) { - makeFactory() + override fun getService(type: String, algorithm: String): Service? { + return services.getOrPut(Pair(type, algorithm)) { + Optional.ofNullable(superGetService(type, algorithm)) + }.orElse(null) } private fun superGetService(type: String, algorithm: String): Service? = super.getService(type, algorithm) - - @StubOutForDJVM - private fun makeCachingFactory(): Function2 { - return object : Function2 { - private val services = ConcurrentHashMap, Optional>() - - override fun invoke(type: String, algorithm: String): Service? { - return services.getOrPut(Pair(type, algorithm)) { - Optional.ofNullable(superGetService(type, algorithm)) - }.orElse(null) - } - } - } - - private fun makeFactory(): Function2 { - return object : Function2 { - override fun invoke(type: String, algorithm: String): Service? { - return superGetService(type, algorithm) - } - } - } } -@KeepForDJVM object CordaObjectIdentifier { // UUID-based OID // TODO define and use an official Corda OID in [CordaOID]. We didn't do yet for backwards compatibility purposes, diff --git a/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt b/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt index b077907821..8018c68892 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt @@ -1,8 +1,6 @@ package net.corda.core.crypto import net.corda.core.CordaOID -import net.corda.core.DeleteForDJVM -import net.corda.core.KeepForDJVM import net.corda.core.crypto.internal.AliasPrivateKey import net.corda.core.crypto.internal.Instances.withSignature import net.corda.core.crypto.internal.PublicKeyCache @@ -83,7 +81,6 @@ import javax.crypto.spec.SecretKeySpec *
  • SPHINCS256_SHA512 (SPHINCS-256 hash-based signature scheme using SHA512 as hash algorithm). * */ -@KeepForDJVM object Crypto { /** * RSA PKCS#1 signature scheme using SHA256 for message hashing. @@ -229,7 +226,6 @@ object Crypto { @JvmStatic fun supportedSignatureSchemes(): List = ArrayList(signatureSchemeMap.values) - @DeleteForDJVM @JvmStatic fun findProvider(name: String): Provider { return providerMap[name] ?: throw IllegalArgumentException("Unrecognised provider: $name") @@ -308,7 +304,6 @@ object Crypto { * @throws IllegalArgumentException on not supported scheme or if the given key specification * is inappropriate for this key factory to produce a private key. */ - @DeleteForDJVM @JvmStatic fun decodePrivateKey(encodedKey: ByteArray): PrivateKey { val keyInfo = PrivateKeyInfo.getInstance(encodedKey) @@ -320,7 +315,6 @@ object Crypto { return convertIfBCEdDSAPrivateKey(keyFactory.generatePrivate(PKCS8EncodedKeySpec(encodedKey))) } - @DeleteForDJVM private fun decodeAliasPrivateKey(keyInfo: PrivateKeyInfo): PrivateKey { val encodable = keyInfo.parsePrivateKey() as DLSequence val derutF8String = encodable.getObjectAt(0) @@ -336,7 +330,6 @@ object Crypto { * @throws IllegalArgumentException on not supported scheme or if the given key specification * is inappropriate for this key factory to produce a private key. */ - @DeleteForDJVM @JvmStatic @Throws(InvalidKeySpecException::class) fun decodePrivateKey(schemeCodeName: String, encodedKey: ByteArray): PrivateKey { @@ -438,7 +431,6 @@ object Crypto { * @throws InvalidKeyException if the private key is invalid. * @throws SignatureException if signing is not possible due to malformed data or private key. */ - @DeleteForDJVM @JvmStatic @Throws(InvalidKeyException::class, SignatureException::class) fun doSign(privateKey: PrivateKey, clearData: ByteArray): ByteArray = doSign(findSignatureScheme(privateKey), privateKey, clearData) @@ -453,7 +445,6 @@ object Crypto { * @throws InvalidKeyException if the private key is invalid. * @throws SignatureException if signing is not possible due to malformed data or private key. */ - @DeleteForDJVM @JvmStatic @Throws(InvalidKeyException::class, SignatureException::class) fun doSign(schemeCodeName: String, privateKey: PrivateKey, clearData: ByteArray): ByteArray { @@ -470,7 +461,6 @@ object Crypto { * @throws InvalidKeyException if the private key is invalid. * @throws SignatureException if signing is not possible due to malformed data or private key. */ - @DeleteForDJVM @JvmStatic @Throws(InvalidKeyException::class, SignatureException::class) fun doSign(signatureScheme: SignatureScheme, privateKey: PrivateKey, clearData: ByteArray): ByteArray { @@ -510,7 +500,6 @@ object Crypto { * @throws InvalidKeyException if the private key is invalid. * @throws SignatureException if signing is not possible due to malformed data or private key. */ - @DeleteForDJVM @JvmStatic @Throws(InvalidKeyException::class, SignatureException::class) fun doSign(keyPair: KeyPair, signableData: SignableData): TransactionSignature { @@ -697,7 +686,6 @@ object Crypto { * @return a KeyPair for the requested signature scheme code name. * @throws IllegalArgumentException if the requested signature scheme is not supported. */ - @DeleteForDJVM @JvmStatic fun generateKeyPair(schemeCodeName: String): KeyPair = generateKeyPair(findSignatureScheme(schemeCodeName)) @@ -708,7 +696,6 @@ object Crypto { * @return a new [KeyPair] for the requested [SignatureScheme]. * @throws IllegalArgumentException if the requested signature scheme is not supported. */ - @DeleteForDJVM @JvmOverloads @JvmStatic fun generateKeyPair(signatureScheme: SignatureScheme = DEFAULT_SIGNATURE_SCHEME): KeyPair { @@ -1059,7 +1046,6 @@ object Crypto { * @throws IllegalArgumentException on not supported scheme or if the given key specification * is inappropriate for a supported key factory to produce a private key. */ - @DeleteForDJVM @JvmStatic fun toSupportedPrivateKey(key: PrivateKey): PrivateKey { return when (key) { @@ -1096,7 +1082,6 @@ object Crypto { * CRL & CSR checks etc.). */ // TODO: perform all cryptographic operations via Crypto. - @DeleteForDJVM @JvmStatic fun registerProviders() { providerMap @@ -1107,7 +1092,6 @@ object Crypto { setBouncyCastleRNG() } - @DeleteForDJVM private fun setBouncyCastleRNG() { CryptoServicesRegistrar.setSecureRandom(newSecureRandom()) } diff --git a/core/src/main/kotlin/net/corda/core/crypto/CryptoUtils.kt b/core/src/main/kotlin/net/corda/core/crypto/CryptoUtils.kt index c1c0d88cc0..3fc649f989 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/CryptoUtils.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/CryptoUtils.kt @@ -1,11 +1,7 @@ @file:Suppress("MatchingDeclarationName") -@file:KeepForDJVM @file:JvmName("CryptoUtils") - package net.corda.core.crypto -import net.corda.core.DeleteForDJVM -import net.corda.core.KeepForDJVM import net.corda.core.contracts.PrivacySalt import net.corda.core.crypto.internal.platformSecureRandomFactory import net.corda.core.serialization.SerializationDefaults @@ -32,7 +28,6 @@ import java.security.SignatureException * @throws InvalidKeyException if the private key is invalid. * @throws SignatureException if signing is not possible due to malformed data or private key. */ -@DeleteForDJVM @Throws(InvalidKeyException::class, SignatureException::class) fun PrivateKey.sign(bytesToSign: ByteArray): DigitalSignature = DigitalSignature(Crypto.doSign(this, bytesToSign)) @@ -45,7 +40,6 @@ fun PrivateKey.sign(bytesToSign: ByteArray): DigitalSignature = DigitalSignature * @throws InvalidKeyException if the private key is invalid. * @throws SignatureException if signing is not possible due to malformed data or private key. */ -@DeleteForDJVM @Throws(InvalidKeyException::class, SignatureException::class) fun PrivateKey.sign(bytesToSign: ByteArray, publicKey: PublicKey): DigitalSignature.WithKey { return DigitalSignature.WithKey(publicKey, this.sign(bytesToSign).bytes) @@ -59,12 +53,10 @@ fun PrivateKey.sign(bytesToSign: ByteArray, publicKey: PublicKey): DigitalSignat * @throws InvalidKeyException if the private key is invalid. * @throws SignatureException if signing is not possible due to malformed data or private key. */ -@DeleteForDJVM @Throws(InvalidKeyException::class, SignatureException::class) fun KeyPair.sign(bytesToSign: ByteArray): DigitalSignature.WithKey = private.sign(bytesToSign, public) /** Helper function to sign the bytes of [bytesToSign] with a key pair. */ -@DeleteForDJVM @Throws(InvalidKeyException::class, SignatureException::class) fun KeyPair.sign(bytesToSign: OpaqueBytes): DigitalSignature.WithKey = sign(bytesToSign.bytes) @@ -76,7 +68,6 @@ fun KeyPair.sign(bytesToSign: OpaqueBytes): DigitalSignature.WithKey = sign(byte * @throws InvalidKeyException if the private key is invalid. * @throws SignatureException if signing is not possible due to malformed data or private key. */ -@DeleteForDJVM @Throws(InvalidKeyException::class, SignatureException::class) fun KeyPair.sign(signableData: SignableData): TransactionSignature = Crypto.doSign(this, signableData) @@ -151,7 +142,6 @@ operator fun KeyPair.component1(): PrivateKey = this.private operator fun KeyPair.component2(): PublicKey = this.public /** A simple wrapper that will make it easier to swap out the signature algorithm we use in future. */ -@DeleteForDJVM fun generateKeyPair(): KeyPair = Crypto.generateKeyPair() /** @@ -196,7 +186,6 @@ fun KeyPair.verify(signatureData: ByteArray, clearData: ByteArray): Boolean = Cr * or if no strong [SecureRandom] implementations are available or if Security.getProperty("securerandom.strongAlgorithms") is null or empty, * which should never happen and suggests an unusual JVM or non-standard Java library. */ -@DeleteForDJVM @Throws(NoSuchAlgorithmException::class) fun secureRandomBytes(numOfBytes: Int): ByteArray = ByteArray(numOfBytes).apply { newSecureRandom().nextBytes(this) } @@ -241,7 +230,6 @@ object DummySecureRandom : SecureRandom(DummySecureRandomSpi(), null) * or if no strong SecureRandom implementations are available or if Security.getProperty("securerandom.strongAlgorithms") is null or empty, * which should never happen and suggests an unusual JVM or non-standard Java library. */ -@DeleteForDJVM @Throws(NoSuchAlgorithmException::class) fun newSecureRandom(): SecureRandom = platformSecureRandomFactory() @@ -249,7 +237,6 @@ fun newSecureRandom(): SecureRandom = platformSecureRandomFactory() * Returns a random positive non-zero long generated using a secure RNG. This function sacrifies a bit of entropy in order * to avoid potential bugs where the value is used in a context where negative numbers or zero are not expected. */ -@DeleteForDJVM fun random63BitValue(): Long { while (true) { val candidate = Math.abs(newSecureRandom().nextLong()) diff --git a/core/src/main/kotlin/net/corda/core/crypto/DigestAlgorithm.kt b/core/src/main/kotlin/net/corda/core/crypto/DigestAlgorithm.kt index 13610eba5e..548e4ea6c8 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/DigestAlgorithm.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/DigestAlgorithm.kt @@ -1,11 +1,8 @@ package net.corda.core.crypto -import net.corda.core.KeepForDJVM - /** * Interface for injecting custom digest implementation bypassing JCA. */ -@KeepForDJVM interface DigestAlgorithm { /** * Algorithm identifier. diff --git a/core/src/main/kotlin/net/corda/core/crypto/DigestService.kt b/core/src/main/kotlin/net/corda/core/crypto/DigestService.kt index e163993443..843d47cd29 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/DigestService.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/DigestService.kt @@ -1,7 +1,5 @@ package net.corda.core.crypto -import net.corda.core.DeleteForDJVM -import net.corda.core.KeepForDJVM import net.corda.core.contracts.PrivacySalt import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.SerializationDefaults @@ -24,13 +22,11 @@ import java.security.MessageDigest * @param hashAlgorithm the name of the hash algorithm to be used for the instance */ @CordaSerializable -@KeepForDJVM data class DigestService(val hashAlgorithm: String) { init { require(hashAlgorithm.isNotEmpty()) { "Hash algorithm name unavailable or not specified" } } - @KeepForDJVM companion object { private const val NONCE_SIZE = 8 /** @@ -114,5 +110,4 @@ data class DigestService(val hashAlgorithm: String) { } } -@DeleteForDJVM fun DigestService.randomHash(): SecureHash = SecureHash.random(this.hashAlgorithm) diff --git a/core/src/main/kotlin/net/corda/core/crypto/DigitalSignature.kt b/core/src/main/kotlin/net/corda/core/crypto/DigitalSignature.kt index da48067fb1..b7b9c031e4 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/DigitalSignature.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/DigitalSignature.kt @@ -1,6 +1,5 @@ package net.corda.core.crypto -import net.corda.core.KeepForDJVM import net.corda.core.serialization.CordaSerializable import net.corda.core.utilities.OpaqueBytes import java.security.InvalidKeyException @@ -9,10 +8,8 @@ import java.security.SignatureException /** A wrapper around a digital signature. */ @CordaSerializable -@KeepForDJVM open class DigitalSignature(bytes: ByteArray) : OpaqueBytes(bytes) { /** A digital signature that identifies who the public key is owned by. */ - @KeepForDJVM open class WithKey(val by: PublicKey, bytes: ByteArray) : DigitalSignature(bytes) { /** * Utility to simplify the act of verifying a signature. diff --git a/core/src/main/kotlin/net/corda/core/crypto/MerkleTree.kt b/core/src/main/kotlin/net/corda/core/crypto/MerkleTree.kt index 692d4e1345..321f717f12 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/MerkleTree.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/MerkleTree.kt @@ -1,6 +1,5 @@ package net.corda.core.crypto -import net.corda.core.KeepForDJVM import java.util.* /** @@ -15,8 +14,9 @@ import java.util.* sealed class MerkleTree { abstract val hash: SecureHash - @KeepForDJVM data class Leaf(override val hash: SecureHash) : MerkleTree() - @KeepForDJVM data class Node(override val hash: SecureHash, val left: MerkleTree, val right: MerkleTree) : MerkleTree() + data class Leaf(override val hash: SecureHash) : MerkleTree() + + data class Node(override val hash: SecureHash, val left: MerkleTree, val right: MerkleTree) : MerkleTree() companion object { private fun isPow2(num: Int): Boolean = num and (num - 1) == 0 diff --git a/core/src/main/kotlin/net/corda/core/crypto/NullKeys.kt b/core/src/main/kotlin/net/corda/core/crypto/NullKeys.kt index 082163c5ef..a828fd9425 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/NullKeys.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/NullKeys.kt @@ -1,10 +1,8 @@ package net.corda.core.crypto -import net.corda.core.KeepForDJVM import net.corda.core.identity.AnonymousParty import java.security.PublicKey -@KeepForDJVM object NullKeys { object NullPublicKey : PublicKey, Comparable { override fun getAlgorithm() = "NULL" diff --git a/core/src/main/kotlin/net/corda/core/crypto/PartialMerkleTree.kt b/core/src/main/kotlin/net/corda/core/crypto/PartialMerkleTree.kt index f3aab735ff..4fbffc24d7 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/PartialMerkleTree.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/PartialMerkleTree.kt @@ -2,12 +2,10 @@ package net.corda.core.crypto import net.corda.core.CordaException import net.corda.core.CordaInternal -import net.corda.core.KeepForDJVM import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.DeprecatedConstructorForDeserialization import java.util.* -@KeepForDJVM @CordaSerializable class MerkleTreeException(val reason: String) : CordaException("Partial Merkle Tree exception. Reason: $reason") @@ -45,7 +43,6 @@ class MerkleTreeException(val reason: String) : CordaException("Partial Merkle T * (there can be a difference in obtained leaves ordering - that's why it's a set comparison not hashing leaves into a tree). * If both equalities hold, we can assume that l3 and l5 belong to the transaction with root h15. */ -@KeepForDJVM @CordaSerializable class PartialMerkleTree(val root: PartialTree) { /** @@ -57,9 +54,9 @@ class PartialMerkleTree(val root: PartialTree) { */ @CordaSerializable sealed class PartialTree { - @KeepForDJVM data class IncludedLeaf(val hash: SecureHash) : PartialTree() - @KeepForDJVM data class Leaf(val hash: SecureHash) : PartialTree() - @KeepForDJVM data class Node(val left: PartialTree, val right: PartialTree, val hashAlgorithm: String? = SecureHash.SHA2_256) : PartialTree(){ + data class IncludedLeaf(val hash: SecureHash) : PartialTree() + data class Leaf(val hash: SecureHash) : PartialTree() + data class Node(val left: PartialTree, val right: PartialTree, val hashAlgorithm: String? = SecureHash.SHA2_256) : PartialTree() { /** * Old version of [PartialTree.Node] constructor for ABI compatibility. */ diff --git a/core/src/main/kotlin/net/corda/core/crypto/SecureHash.kt b/core/src/main/kotlin/net/corda/core/crypto/SecureHash.kt index 5c7e943c8c..a930aa254d 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/SecureHash.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/SecureHash.kt @@ -1,11 +1,8 @@ @file:Suppress("TooManyFunctions", "MagicNumber") -@file:KeepForDJVM package net.corda.core.crypto import io.netty.util.concurrent.FastThreadLocal import net.corda.core.CordaInternal -import net.corda.core.DeleteForDJVM -import net.corda.core.KeepForDJVM import net.corda.core.crypto.internal.DigestAlgorithmFactory import net.corda.core.internal.utilities.Internable import net.corda.core.internal.utilities.PrivateInterner @@ -22,7 +19,6 @@ import java.util.function.Supplier * Container for a cryptographically secure hash value. * Provides utilities for generating a cryptographic hash using different algorithms (currently only SHA-256 supported). */ -@KeepForDJVM @CordaSerializable sealed class SecureHash(bytes: ByteArray) : OpaqueBytes(bytes) { /** SHA-256 is part of the SHA-2 hash function family. Generated hash is fixed size, 256-bits (32-bytes). */ @@ -291,14 +287,12 @@ sealed class SecureHash(bytes: ByteArray) : OpaqueBytes(bytes) { /** * Generates a random SHA-256 value. */ - @DeleteForDJVM @JvmStatic fun randomSHA256() = sha256(secureRandomBytes(32)) /** * Generates a random hash value. */ - @DeleteForDJVM @JvmStatic fun random(algorithm: String): SecureHash { return if (algorithm == SHA2_256) { diff --git a/core/src/main/kotlin/net/corda/core/crypto/SignableData.kt b/core/src/main/kotlin/net/corda/core/crypto/SignableData.kt index cfe0ec96a8..124794b730 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/SignableData.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/SignableData.kt @@ -1,6 +1,5 @@ package net.corda.core.crypto -import net.corda.core.KeepForDJVM import net.corda.core.serialization.CordaSerializable /** @@ -13,5 +12,4 @@ import net.corda.core.serialization.CordaSerializable * @param signatureMetadata meta data required. */ @CordaSerializable -@KeepForDJVM data class SignableData(val txId: SecureHash, val signatureMetadata: SignatureMetadata) diff --git a/core/src/main/kotlin/net/corda/core/crypto/SignatureMetadata.kt b/core/src/main/kotlin/net/corda/core/crypto/SignatureMetadata.kt index 6c8d9c33e6..99335bde4c 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/SignatureMetadata.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/SignatureMetadata.kt @@ -1,6 +1,5 @@ package net.corda.core.crypto -import net.corda.core.KeepForDJVM import net.corda.core.serialization.CordaSerializable /** @@ -13,5 +12,4 @@ import net.corda.core.serialization.CordaSerializable * @param schemeNumberID number id of the signature scheme used based on signer's key-pair, see [SignatureScheme.schemeNumberID]. */ @CordaSerializable -@KeepForDJVM data class SignatureMetadata(val platformVersion: Int, val schemeNumberID: Int) diff --git a/core/src/main/kotlin/net/corda/core/crypto/SignatureScheme.kt b/core/src/main/kotlin/net/corda/core/crypto/SignatureScheme.kt index 4696e9b2db..27b3fc4750 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/SignatureScheme.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/SignatureScheme.kt @@ -1,6 +1,5 @@ package net.corda.core.crypto -import net.corda.core.KeepForDJVM import org.bouncycastle.asn1.x509.AlgorithmIdentifier import java.security.KeyFactory import java.security.Signature @@ -22,7 +21,6 @@ import java.security.spec.AlgorithmParameterSpec * @param keySize the private key size (currently used for RSA only). * @param desc a human-readable description for this scheme. */ -@KeepForDJVM data class SignatureScheme( val schemeNumberID: Int, val schemeCodeName: String, diff --git a/core/src/main/kotlin/net/corda/core/crypto/SignedData.kt b/core/src/main/kotlin/net/corda/core/crypto/SignedData.kt index c33ac597fd..87a4ba8a32 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/SignedData.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/SignedData.kt @@ -1,6 +1,5 @@ package net.corda.core.crypto -import net.corda.core.KeepForDJVM import net.corda.core.internal.uncheckedCast import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.SerializedBytes @@ -15,7 +14,6 @@ import java.security.SignatureException * @param sig the (unverified) signature for the data. */ @CordaSerializable -@KeepForDJVM open class SignedData(val raw: SerializedBytes, val sig: DigitalSignature.WithKey) { /** * Return the deserialized data if the signature can be verified. diff --git a/core/src/main/kotlin/net/corda/core/crypto/TransactionSignature.kt b/core/src/main/kotlin/net/corda/core/crypto/TransactionSignature.kt index a26139db1d..29d0a0d212 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/TransactionSignature.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/TransactionSignature.kt @@ -1,6 +1,5 @@ package net.corda.core.crypto -import net.corda.core.KeepForDJVM import net.corda.core.serialization.CordaSerializable import java.security.InvalidKeyException import java.security.PublicKey @@ -16,7 +15,6 @@ import java.util.* * @property partialMerkleTree required when multi-transaction signing is utilised. */ @CordaSerializable -@KeepForDJVM class TransactionSignature(bytes: ByteArray, val by: PublicKey, val signatureMetadata: SignatureMetadata, val partialMerkleTree: PartialMerkleTree?) : DigitalSignature(bytes) { /** * Construct a [TransactionSignature] with [partialMerkleTree] set to null. diff --git a/core/src/main/kotlin/net/corda/core/crypto/internal/Instances.kt b/core/src/main/kotlin/net/corda/core/crypto/internal/Instances.kt index 6990905d89..fc7336f855 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/internal/Instances.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/internal/Instances.kt @@ -1,7 +1,5 @@ package net.corda.core.crypto.internal -import net.corda.core.DeleteForDJVM -import net.corda.core.StubOutForDJVM import net.corda.core.crypto.SignatureScheme import net.corda.core.internal.LazyPool import java.security.Provider @@ -25,13 +23,7 @@ object Instances { fun getSignatureInstance(algorithm: String, provider: Provider?) = signatureFactory.borrow(algorithm, provider) fun releaseSignatureInstance(sig: Signature) = signatureFactory.release(sig) - // Used to work around banning of ConcurrentHashMap in DJVM - private val signatureFactory: SignatureFactory = try { - makeCachingFactory() - } catch (e: UnsupportedOperationException) { - // Thrown by DJVM for method stubbed out below. - makeFactory() - } + private val signatureFactory: SignatureFactory = CachingSignatureFactory() // The provider itself is a very bad key class as hashCode() is expensive and contended. So use name and version instead. private data class SignatureKey(val algorithm: String, val providerName: String?, val providerVersion: Double?) { @@ -39,12 +31,6 @@ object Instances { @Suppress("DEPRECATION") provider?.version) // JDK11: should replace with getVersionStr() (since 9) } - @StubOutForDJVM - private fun makeCachingFactory(): SignatureFactory { - return CachingSignatureFactory() - } - - @DeleteForDJVM private class CachingSignatureFactory : SignatureFactory { private val signatureInstances = ConcurrentHashMap>() diff --git a/core/src/main/kotlin/net/corda/core/crypto/internal/PlatformSecureRandom.kt b/core/src/main/kotlin/net/corda/core/crypto/internal/PlatformSecureRandom.kt index cd598dafad..3a0062b251 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/internal/PlatformSecureRandom.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/internal/PlatformSecureRandom.kt @@ -1,9 +1,7 @@ @file:JvmName("PlatformSecureRandom") -@file:DeleteForDJVM package net.corda.core.crypto.internal import io.netty.util.concurrent.FastThreadLocal -import net.corda.core.DeleteForDJVM import net.corda.core.crypto.DummySecureRandom import net.corda.core.utilities.SgxSupport import net.corda.core.utilities.loggerFor @@ -30,7 +28,6 @@ internal val platformSecureRandom: () -> SecureRandom = when { } } -@DeleteForDJVM class PlatformSecureRandomService(provider: Provider) : Provider.Service(provider, "SecureRandom", algorithm, PlatformSecureRandomSpi::javaClass.name, null, null) { @@ -54,7 +51,6 @@ class PlatformSecureRandomService(provider: Provider) override fun newInstance(constructorParameter: Any?) = instance } -@DeleteForDJVM private class PlatformSecureRandomSpi : SecureRandomSpi() { private val threadLocalSecureRandom = object : FastThreadLocal() { override fun initialValue() = SecureRandom.getInstanceStrong() @@ -67,7 +63,6 @@ private class PlatformSecureRandomSpi : SecureRandomSpi() { override fun engineGenerateSeed(numBytes: Int): ByteArray = secureRandom.generateSeed(numBytes) } -@DeleteForDJVM @Suppress("TooGenericExceptionCaught", "TooGenericExceptionThrown") private class LinuxSecureRandomSpi : SecureRandomSpi() { private fun openURandom(): InputStream { diff --git a/core/src/main/kotlin/net/corda/core/crypto/internal/ProviderMap.kt b/core/src/main/kotlin/net/corda/core/crypto/internal/ProviderMap.kt index 3b17e644f1..0ac52cdedb 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/internal/ProviderMap.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/internal/ProviderMap.kt @@ -1,6 +1,5 @@ package net.corda.core.crypto.internal -import net.corda.core.DeleteForDJVM import net.corda.core.crypto.CordaSecurityProvider import net.corda.core.crypto.Crypto.EDDSA_ED25519_SHA512 import net.corda.core.crypto.Crypto.decodePrivateKey @@ -62,5 +61,4 @@ internal val providerMap: Map = unmodifiableMap( .associateByTo(LinkedHashMap(), Provider::getName) ) -@DeleteForDJVM fun platformSecureRandomFactory(): SecureRandom = platformSecureRandom() // To minimise diff of CryptoUtils against open-source. diff --git a/core/src/main/kotlin/net/corda/core/flows/FlowLogic.kt b/core/src/main/kotlin/net/corda/core/flows/FlowLogic.kt index 8c207766fc..7520eae9ce 100644 --- a/core/src/main/kotlin/net/corda/core/flows/FlowLogic.kt +++ b/core/src/main/kotlin/net/corda/core/flows/FlowLogic.kt @@ -3,7 +3,6 @@ package net.corda.core.flows import co.paralleluniverse.fibers.Suspendable import co.paralleluniverse.strands.Strand import net.corda.core.CordaInternal -import net.corda.core.DeleteForDJVM import net.corda.core.contracts.StateRef import net.corda.core.crypto.SecureHash import net.corda.core.identity.AbstractParty @@ -66,12 +65,10 @@ import java.util.LinkedHashMap * relevant database transactions*. Only set this option to true if you know what you're doing. */ @Suppress("DEPRECATION", "DeprecatedCallableAddReplaceWith") -@DeleteForDJVM abstract class FlowLogic { /** This is where you should log things to. */ val logger: Logger get() = stateMachine.logger - @DeleteForDJVM companion object { /** * Return the outermost [FlowLogic] instance, or null if not in a flow. diff --git a/core/src/main/kotlin/net/corda/core/flows/FlowLogicRef.kt b/core/src/main/kotlin/net/corda/core/flows/FlowLogicRef.kt index 1b08620e2a..7781c38b95 100644 --- a/core/src/main/kotlin/net/corda/core/flows/FlowLogicRef.kt +++ b/core/src/main/kotlin/net/corda/core/flows/FlowLogicRef.kt @@ -1,9 +1,7 @@ package net.corda.core.flows import net.corda.core.CordaInternal -import net.corda.core.DeleteForDJVM import net.corda.core.DoNotImplement -import net.corda.core.KeepForDJVM import net.corda.core.serialization.CordaSerializable /** @@ -13,13 +11,11 @@ import net.corda.core.serialization.CordaSerializable * the flow to run at the scheduled time. */ @DoNotImplement -@KeepForDJVM interface FlowLogicRefFactory { /** * Construct a FlowLogicRef. This is intended for cases where the calling code has the relevant class already * and can provide it directly. */ - @DeleteForDJVM fun create(flowClass: Class>, vararg args: Any?): FlowLogicRef /** @@ -34,14 +30,12 @@ interface FlowLogicRefFactory { * [SchedulableFlow] annotation. */ @CordaInternal - @DeleteForDJVM fun createForRPC(flowClass: Class>, vararg args: Any?): FlowLogicRef /** * Converts a [FlowLogicRef] object that was obtained from the calls above into a [FlowLogic], after doing some * validation to ensure it points to a legitimate flow class. */ - @DeleteForDJVM fun toFlowLogic(ref: FlowLogicRef): FlowLogic<*> } @@ -65,5 +59,4 @@ class IllegalFlowLogicException(val type: String, msg: String) : // TODO: align this with the existing [FlowRef] in the bank-side API (probably replace some of the API classes) @CordaSerializable @DoNotImplement -@KeepForDJVM interface FlowLogicRef \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/flows/FlowSession.kt b/core/src/main/kotlin/net/corda/core/flows/FlowSession.kt index dd09a9d481..f40160b15a 100644 --- a/core/src/main/kotlin/net/corda/core/flows/FlowSession.kt +++ b/core/src/main/kotlin/net/corda/core/flows/FlowSession.kt @@ -2,7 +2,6 @@ package net.corda.core.flows import co.paralleluniverse.fibers.Suspendable import net.corda.core.DoNotImplement -import net.corda.core.KeepForDJVM import net.corda.core.identity.AnonymousParty import net.corda.core.identity.Party import net.corda.core.utilities.UntrustworthyData @@ -214,5 +213,4 @@ abstract class FlowSession { * in future releases. */ @DoNotImplement -@KeepForDJVM interface Destination diff --git a/core/src/main/kotlin/net/corda/core/flows/StateMachineRunId.kt b/core/src/main/kotlin/net/corda/core/flows/StateMachineRunId.kt index 47de376947..30f0d9599a 100644 --- a/core/src/main/kotlin/net/corda/core/flows/StateMachineRunId.kt +++ b/core/src/main/kotlin/net/corda/core/flows/StateMachineRunId.kt @@ -1,6 +1,5 @@ package net.corda.core.flows -import net.corda.core.DeleteForDJVM import net.corda.core.serialization.CordaSerializable import java.util.* @@ -8,7 +7,6 @@ import java.util.* * A unique identifier for a single state machine run, valid across node restarts. Note that a single run always * has at least one flow, but that flow may also invoke sub-flows: they all share the same run id. */ -@DeleteForDJVM @CordaSerializable data class StateMachineRunId(val uuid: UUID) { companion object { diff --git a/core/src/main/kotlin/net/corda/core/identity/AnonymousParty.kt b/core/src/main/kotlin/net/corda/core/identity/AnonymousParty.kt index 8797f9e5a6..e61fe4ce81 100644 --- a/core/src/main/kotlin/net/corda/core/identity/AnonymousParty.kt +++ b/core/src/main/kotlin/net/corda/core/identity/AnonymousParty.kt @@ -1,6 +1,5 @@ package net.corda.core.identity -import net.corda.core.KeepForDJVM import net.corda.core.contracts.PartyAndReference import net.corda.core.crypto.toStringShort import net.corda.core.flows.Destination @@ -17,7 +16,6 @@ import java.security.PublicKey * Anonymous parties can be used to communicate using the [FlowLogic.initiateFlow] method. Message routing is simply routing to the well-known * [Party] the anonymous party belongs to. This mechanism assumes the party initiating the communication knows who the anonymous party is. */ -@KeepForDJVM class AnonymousParty(owningKey: PublicKey) : Destination, AbstractParty(owningKey) { override fun nameOrNull(): CordaX500Name? = null override fun ref(bytes: OpaqueBytes): PartyAndReference = PartyAndReference(this, bytes) diff --git a/core/src/main/kotlin/net/corda/core/identity/CordaX500Name.kt b/core/src/main/kotlin/net/corda/core/identity/CordaX500Name.kt index f61ba04b58..a2cc82d1e3 100644 --- a/core/src/main/kotlin/net/corda/core/identity/CordaX500Name.kt +++ b/core/src/main/kotlin/net/corda/core/identity/CordaX500Name.kt @@ -1,7 +1,6 @@ package net.corda.core.identity import net.corda.core.CordaInternal -import net.corda.core.KeepForDJVM import net.corda.core.internal.LegalNameValidator import net.corda.core.internal.toAttributesMap import net.corda.core.internal.toX500Name @@ -30,7 +29,6 @@ import javax.security.auth.x500.X500Principal * attribute type. */ @CordaSerializable -@KeepForDJVM data class CordaX500Name(val commonName: String?, val organisationUnit: String?, val organisation: String, diff --git a/core/src/main/kotlin/net/corda/core/identity/Party.kt b/core/src/main/kotlin/net/corda/core/identity/Party.kt index a01caadf8b..75aceaa233 100644 --- a/core/src/main/kotlin/net/corda/core/identity/Party.kt +++ b/core/src/main/kotlin/net/corda/core/identity/Party.kt @@ -1,6 +1,5 @@ package net.corda.core.identity -import net.corda.core.KeepForDJVM import net.corda.core.contracts.PartyAndReference import net.corda.core.crypto.CompositeKey import net.corda.core.crypto.Crypto @@ -35,7 +34,6 @@ import java.security.cert.X509Certificate * * @see CompositeKey */ -@KeepForDJVM class Party(val name: CordaX500Name, owningKey: PublicKey) : Destination, AbstractParty(owningKey) { constructor(certificate: X509Certificate) : this(CordaX500Name.build(certificate.subjectX500Principal), Crypto.toSupportedPublicKey(certificate.publicKey)) diff --git a/core/src/main/kotlin/net/corda/core/identity/PartyAndCertificate.kt b/core/src/main/kotlin/net/corda/core/identity/PartyAndCertificate.kt index d1adcc73c2..4fdea7cda2 100644 --- a/core/src/main/kotlin/net/corda/core/identity/PartyAndCertificate.kt +++ b/core/src/main/kotlin/net/corda/core/identity/PartyAndCertificate.kt @@ -1,6 +1,5 @@ package net.corda.core.identity -import net.corda.core.KeepForDJVM import net.corda.core.internal.CertRole import net.corda.core.internal.uncheckedCast import net.corda.core.internal.validate @@ -18,7 +17,6 @@ import java.security.cert.X509Certificate * not part of the identifier themselves. */ @CordaSerializable -@KeepForDJVM class PartyAndCertificate(val certPath: CertPath) { @Transient val certificate: X509Certificate diff --git a/core/src/main/kotlin/net/corda/core/internal/AbstractAttachment.kt b/core/src/main/kotlin/net/corda/core/internal/AbstractAttachment.kt index 198416260f..d5d25bc6cc 100644 --- a/core/src/main/kotlin/net/corda/core/internal/AbstractAttachment.kt +++ b/core/src/main/kotlin/net/corda/core/internal/AbstractAttachment.kt @@ -1,9 +1,5 @@ -@file:KeepForDJVM - package net.corda.core.internal -import net.corda.core.DeleteForDJVM -import net.corda.core.KeepForDJVM import net.corda.core.contracts.Attachment import net.corda.core.contracts.ContractAttachment import net.corda.core.crypto.SecureHash @@ -23,7 +19,7 @@ const val P2P_UPLOADER = "p2p" const val TESTDSL_UPLOADER = "TestDSL" const val UNKNOWN_UPLOADER = "unknown" -// We whitelist sources of transaction JARs for now as a temporary state until the DJVM and other security sandboxes +// We whitelist sources of transaction JARs for now as a temporary state until security sandboxes // have been integrated, at which point we'll be able to run untrusted code downloaded over the network and this mechanism // can be removed. Because we ARE downloading attachments over the P2P network in anticipation of this upgrade, we // track the source of each attachment in our store. TestDSL is used by LedgerDSLInterpreter when custom attachments @@ -38,7 +34,6 @@ fun Attachment.isUploaderTrusted(): Boolean = when (this) { else -> false } -@KeepForDJVM abstract class AbstractAttachment(dataLoader: () -> ByteArray, val uploader: String?) : Attachment { companion object { /** @@ -46,7 +41,6 @@ abstract class AbstractAttachment(dataLoader: () -> ByteArray, val uploader: Str * * TODO - this code together with the rest of the Attachment handling (including [FetchedAttachment]) needs some refactoring as it is really hard to follow. */ - @DeleteForDJVM fun SerializeAsTokenContext.attachmentDataLoader(id: SecureHash): () -> ByteArray { return { val a = serviceHub.attachments.openAttachment(id) ?: throw MissingAttachmentsException(listOf(id)) diff --git a/core/src/main/kotlin/net/corda/core/internal/ClassGraphUtils.kt b/core/src/main/kotlin/net/corda/core/internal/ClassGraphUtils.kt index fb420e373a..94f51595d8 100644 --- a/core/src/main/kotlin/net/corda/core/internal/ClassGraphUtils.kt +++ b/core/src/main/kotlin/net/corda/core/internal/ClassGraphUtils.kt @@ -1,10 +1,7 @@ -@file:DeleteForDJVM - package net.corda.core.internal import io.github.classgraph.ClassGraph import io.github.classgraph.ScanResult -import net.corda.core.DeleteForDJVM import java.util.concurrent.locks.ReentrantLock import kotlin.concurrent.withLock diff --git a/core/src/main/kotlin/net/corda/core/internal/ClassLoadingUtils.kt b/core/src/main/kotlin/net/corda/core/internal/ClassLoadingUtils.kt index 5ead87ca59..e7834fd567 100644 --- a/core/src/main/kotlin/net/corda/core/internal/ClassLoadingUtils.kt +++ b/core/src/main/kotlin/net/corda/core/internal/ClassLoadingUtils.kt @@ -2,7 +2,6 @@ package net.corda.core.internal import io.github.classgraph.ClassGraph import io.github.classgraph.ClassInfo -import net.corda.core.StubOutForDJVM import net.corda.core.serialization.internal.AttachmentURLStreamHandlerFactory.attachmentScheme /** @@ -19,7 +18,6 @@ import net.corda.core.serialization.internal.AttachmentURLStreamHandlerFactory.a * - be non-abstract * - either be a Kotlin object or have a constructor with no parameters (or only optional ones) */ -@StubOutForDJVM fun createInstancesOfClassesImplementing(classloader: ClassLoader, clazz: Class, classVersionRange: IntRange? = null): Set { return getNamesOfClassesImplementing(classloader, clazz, classVersionRange) @@ -36,7 +34,6 @@ fun createInstancesOfClassesImplementing(classloader: ClassLoader, claz * @return names of the identified classes. * @throws UnsupportedClassVersionError if the class version is not within range. */ -@StubOutForDJVM fun getNamesOfClassesImplementing(classloader: ClassLoader, clazz: Class, classVersionRange: IntRange? = null): Set { return ClassGraph().overrideClassLoaders(classloader) diff --git a/core/src/main/kotlin/net/corda/core/internal/ConstraintsUtils.kt b/core/src/main/kotlin/net/corda/core/internal/ConstraintsUtils.kt index f558024078..7e59c207b5 100644 --- a/core/src/main/kotlin/net/corda/core/internal/ConstraintsUtils.kt +++ b/core/src/main/kotlin/net/corda/core/internal/ConstraintsUtils.kt @@ -16,7 +16,6 @@ typealias Version = Int * Attention: this value affects consensus, so it requires a minimum platform version bump in order to be changed. */ const val MAX_NUMBER_OF_KEYS_IN_SIGNATURE_CONSTRAINT = 20 -private const val DJVM_SANDBOX_PREFIX = "sandbox." private val log = loggerFor() @@ -31,14 +30,10 @@ val ContractState.requiredContractClassName: String? get() { return ContractStateClassCache.contractClassName(this.javaClass) ?: let { val annotation = javaClass.getAnnotation(BelongsToContract::class.java) val className = if (annotation != null) { - annotation.value.java.typeName.removePrefix(DJVM_SANDBOX_PREFIX) + annotation.value.java.typeName } else { val enclosingClass = javaClass.enclosingClass ?: return null - if (Contract::class.java.isAssignableFrom(enclosingClass)) { - enclosingClass.typeName.removePrefix(DJVM_SANDBOX_PREFIX) - } else { - null - } + if (Contract::class.java.isAssignableFrom(enclosingClass)) enclosingClass.typeName else null } ContractStateClassCache.cacheContractClassName(this.javaClass, className) } diff --git a/core/src/main/kotlin/net/corda/core/internal/ContractUpgradeUtils.kt b/core/src/main/kotlin/net/corda/core/internal/ContractUpgradeUtils.kt index 31c09c3e97..ec0589efdc 100644 --- a/core/src/main/kotlin/net/corda/core/internal/ContractUpgradeUtils.kt +++ b/core/src/main/kotlin/net/corda/core/internal/ContractUpgradeUtils.kt @@ -1,12 +1,10 @@ package net.corda.core.internal -import net.corda.core.DeleteForDJVM import net.corda.core.contracts.* import net.corda.core.node.ServicesForResolution import net.corda.core.node.services.AttachmentId import net.corda.core.transactions.ContractUpgradeWireTransaction -@DeleteForDJVM object ContractUpgradeUtils { fun assembleUpgradeTx( stateAndRef: StateAndRef, diff --git a/core/src/main/kotlin/net/corda/core/internal/CordaUtils.kt b/core/src/main/kotlin/net/corda/core/internal/CordaUtils.kt index d431fd4259..8461583901 100644 --- a/core/src/main/kotlin/net/corda/core/internal/CordaUtils.kt +++ b/core/src/main/kotlin/net/corda/core/internal/CordaUtils.kt @@ -1,7 +1,6 @@ @file:Suppress("TooManyFunctions") package net.corda.core.internal -import net.corda.core.DeleteForDJVM import net.corda.core.contracts.Attachment import net.corda.core.contracts.ContractClassName import net.corda.core.cordapp.CordappProvider @@ -65,13 +64,11 @@ enum class JavaVersion(val versionString: String) { } /** Provide access to internal method for AttachmentClassLoaderTests. */ -@DeleteForDJVM fun TransactionBuilder.toWireTransaction(services: ServicesForResolution, serializationContext: SerializationContext): WireTransaction { return toWireTransactionWithContext(services, serializationContext) } /** Provide access to internal method for AttachmentClassLoaderTests. */ -@DeleteForDJVM fun TransactionBuilder.toLedgerTransaction(services: ServicesForResolution, serializationContext: SerializationContext): LedgerTransaction { return toLedgerTransactionWithContext(services, serializationContext) } diff --git a/core/src/main/kotlin/net/corda/core/internal/CordappFixupInternal.kt b/core/src/main/kotlin/net/corda/core/internal/CordappFixupInternal.kt index adbc6f5f85..e1ac4e22c6 100644 --- a/core/src/main/kotlin/net/corda/core/internal/CordappFixupInternal.kt +++ b/core/src/main/kotlin/net/corda/core/internal/CordappFixupInternal.kt @@ -1,9 +1,7 @@ package net.corda.core.internal -import net.corda.core.DeleteForDJVM import net.corda.core.node.services.AttachmentId -@DeleteForDJVM interface CordappFixupInternal { fun fixupAttachmentIds(attachmentIds: Collection): Set } diff --git a/core/src/main/kotlin/net/corda/core/internal/DjvmUtils.kt b/core/src/main/kotlin/net/corda/core/internal/DjvmUtils.kt deleted file mode 100644 index 755a7959e1..0000000000 --- a/core/src/main/kotlin/net/corda/core/internal/DjvmUtils.kt +++ /dev/null @@ -1,20 +0,0 @@ -@file:KeepForDJVM - -package net.corda.core.internal - -import net.corda.core.KeepForDJVM -import net.corda.core.contracts.Attachment -import net.corda.core.contracts.StateRef -import net.corda.core.contracts.TransactionState -import net.corda.core.crypto.SecureHash -import net.corda.core.node.NetworkParameters -import net.corda.core.transactions.LedgerTransaction -import net.corda.core.transactions.WireTransaction - -fun WireTransaction.toLtxDjvmInternal( - resolveAttachment: (SecureHash) -> Attachment?, - resolveStateRef: (StateRef) -> TransactionState<*>?, - resolveParameters: (SecureHash?) -> NetworkParameters? -): LedgerTransaction { - return toLtxDjvmInternalBridge(resolveAttachment, resolveStateRef, resolveParameters) -} diff --git a/core/src/main/kotlin/net/corda/core/internal/Emoji.kt b/core/src/main/kotlin/net/corda/core/internal/Emoji.kt index c911275126..27b780ece0 100644 --- a/core/src/main/kotlin/net/corda/core/internal/Emoji.kt +++ b/core/src/main/kotlin/net/corda/core/internal/Emoji.kt @@ -1,11 +1,8 @@ package net.corda.core.internal -import net.corda.core.DeleteForDJVM - /** * A simple wrapper class that contains icons and support for printing them only when we're connected to a terminal. */ -@DeleteForDJVM object Emoji { // Unfortunately only Apple has a terminal that can do colour emoji AND an emoji font installed by default. // However the JediTerm java terminal emulator can also do emoji on OS X when using the JetBrains JRE. diff --git a/core/src/main/kotlin/net/corda/core/internal/FlowIORequest.kt b/core/src/main/kotlin/net/corda/core/internal/FlowIORequest.kt index 7ced0d46a0..aa8285770e 100644 --- a/core/src/main/kotlin/net/corda/core/internal/FlowIORequest.kt +++ b/core/src/main/kotlin/net/corda/core/internal/FlowIORequest.kt @@ -1,6 +1,5 @@ package net.corda.core.internal -import net.corda.core.DeleteForDJVM import net.corda.core.crypto.SecureHash import net.corda.core.flows.FlowInfo import net.corda.core.flows.FlowSession @@ -13,7 +12,6 @@ import java.time.Instant /** * A [FlowIORequest] represents an IO request of a flow when it suspends. It is persisted in checkpoints. */ -@DeleteForDJVM sealed class FlowIORequest { /** * Send messages to sessions. diff --git a/core/src/main/kotlin/net/corda/core/internal/FlowStateMachine.kt b/core/src/main/kotlin/net/corda/core/internal/FlowStateMachine.kt index bc5f0b0157..9ead75ce33 100644 --- a/core/src/main/kotlin/net/corda/core/internal/FlowStateMachine.kt +++ b/core/src/main/kotlin/net/corda/core/internal/FlowStateMachine.kt @@ -1,7 +1,6 @@ package net.corda.core.internal import co.paralleluniverse.fibers.Suspendable -import net.corda.core.DeleteForDJVM import net.corda.core.DoNotImplement import net.corda.core.concurrent.CordaFuture import net.corda.core.context.InvocationContext @@ -12,7 +11,6 @@ import net.corda.core.internal.telemetry.SerializedTelemetry import net.corda.core.serialization.SerializedBytes import org.slf4j.Logger -@DeleteForDJVM @DoNotImplement interface FlowStateMachineHandle { val logic: FlowLogic? @@ -22,7 +20,6 @@ interface FlowStateMachineHandle { } /** This is an internal interface that is implemented by code in the node module. You should look at [FlowLogic]. */ -@DeleteForDJVM @DoNotImplement interface FlowStateMachine : FlowStateMachineHandle { @Suspendable diff --git a/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt b/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt index 274582ce53..e3df734e91 100644 --- a/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt +++ b/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt @@ -1,10 +1,6 @@ @file:JvmName("InternalUtils") -@file:KeepForDJVM package net.corda.core.internal -import net.corda.core.DeleteForDJVM -import net.corda.core.KeepForDJVM -import net.corda.core.StubOutForDJVM import net.corda.core.crypto.Crypto import net.corda.core.crypto.DigitalSignature import net.corda.core.crypto.SecureHash @@ -131,7 +127,6 @@ fun List.noneOrSingle(): T? { } /** Returns a random element in the list, or `null` if empty */ -@DeleteForDJVM fun List.randomOrNull(): T? { return when (size) { 0 -> null @@ -147,7 +142,7 @@ fun List.indexOfOrThrow(item: T): Int { return i } -@DeleteForDJVM fun InputStream.copyTo(target: Path, vararg options: CopyOption): Long = Files.copy(this, target, *options) +fun InputStream.copyTo(target: Path, vararg options: CopyOption): Long = Files.copy(this, target, *options) /** Same as [InputStream.readBytes] but also closes the stream. */ fun InputStream.readFully(): ByteArray = use { it.readBytes() } @@ -185,7 +180,6 @@ fun Iterable.sum(): BigDecimal = fold(BigDecimal.ZERO) { a, b -> a + * Returns an Observable that buffers events until subscribed. * @see UnicastSubject */ -@DeleteForDJVM fun Observable.bufferUntilSubscribed(): Observable { val subject = UnicastSubject.create() val subscription = subscribe(subject) @@ -193,7 +187,6 @@ fun Observable.bufferUntilSubscribed(): Observable { } /** Copy an [Observer] to multiple other [Observer]s. */ -@DeleteForDJVM fun Observer.tee(vararg teeTo: Observer): Observer { val subject = PublishSubject.create() // use unsafe subscribe, so that the teed subscribers will not get wrapped with SafeSubscribers, @@ -205,7 +198,6 @@ fun Observer.tee(vararg teeTo: Observer): Observer { } /** Executes the given code block and returns a [Duration] of how long it took to execute in nanosecond precision. */ -@DeleteForDJVM inline fun elapsedTime(block: () -> Unit): Duration { val start = System.nanoTime() block() @@ -218,7 +210,6 @@ fun Logger.logElapsedTime(label: String, body: () -> T): T = logElapsedTime( // TODO: Add inline back when a new Kotlin version is released and check if the java.lang.VerifyError // returns in the IRSSimulationTest. If not, commit the inline back. -@DeleteForDJVM fun logElapsedTime(label: String, logger: Logger? = null, body: () -> T): T { // Use nanoTime as it's monotonic. val now = System.nanoTime() @@ -246,7 +237,6 @@ fun ByteArrayOutputStream.toInputStreamAndHash(): InputStreamAndHash { return InputStreamAndHash(bytes.inputStream(), bytes.sha256()) } -@KeepForDJVM data class InputStreamAndHash(val inputStream: InputStream, val sha256: SecureHash.SHA256) { companion object { /** @@ -254,7 +244,6 @@ data class InputStreamAndHash(val inputStream: InputStream, val sha256: SecureHa * called "z" that contains the given content byte repeated the given number of times. * Note that a slightly bigger than numOfExpectedBytes size is expected. */ - @DeleteForDJVM fun createInMemoryTestZip(numOfExpectedBytes: Int, content: Byte, entryName: String = "z"): InputStreamAndHash { require(numOfExpectedBytes > 0){"Expected bytes must be greater than zero"} require(numOfExpectedBytes > 0) @@ -312,29 +301,24 @@ fun Stream.toSet(): Set = collect(toCollection { LinkedHashSet() }) fun Class.castIfPossible(obj: Any): T? = if (isInstance(obj)) cast(obj) else null /** Returns a [DeclaredField] wrapper around the declared (possibly non-public) static field of the receiver [Class]. */ -@DeleteForDJVM fun Class<*>.staticField(name: String): DeclaredField = DeclaredField(this, name, null) /** Returns a [DeclaredField] wrapper around the declared (possibly non-public) static field of the receiver [KClass]. */ -@DeleteForDJVM fun KClass<*>.staticField(name: String): DeclaredField = DeclaredField(java, name, null) /** Returns a [DeclaredField] wrapper around the declared (possibly non-public) instance field of the receiver object. */ -@DeleteForDJVM fun Any.declaredField(name: String): DeclaredField = DeclaredField(javaClass, name, this) /** * Returns a [DeclaredField] wrapper around the (possibly non-public) instance field of the receiver object, but declared * in its superclass [clazz]. */ -@DeleteForDJVM fun Any.declaredField(clazz: KClass<*>, name: String): DeclaredField = DeclaredField(clazz.java, name, this) /** * Returns a [DeclaredField] wrapper around the (possibly non-public) instance field of the receiver object, but declared * in its superclass [clazz]. */ -@DeleteForDJVM fun Any.declaredField(clazz: Class<*>, name: String): DeclaredField = DeclaredField(clazz, name, this) /** creates a new instance if not a Kotlin object */ @@ -363,7 +347,6 @@ val Class.kotlinObjectInstance: T? get() { * A simple wrapper around a [Field] object providing type safe read and write access using [value], ignoring the field's * visibility. */ -@DeleteForDJVM class DeclaredField(clazz: Class<*>, name: String, private val receiver: Any?) { private val javaField = findField(name, clazz) var value: T @@ -421,7 +404,6 @@ fun uncheckedCast(obj: T) = obj as U fun Iterable>.toMultiMap(): Map> = this.groupBy({ it.first }) { it.second } /** Returns the location of this class. */ -@get:StubOutForDJVM val Class<*>.location: URL get() = protectionDomain.codeSource.location /** Convenience method to get the package name of a class literal. */ @@ -449,14 +431,13 @@ inline val Member.isStatic: Boolean get() = Modifier.isStatic(modifiers) inline val Member.isFinal: Boolean get() = Modifier.isFinal(modifiers) -@DeleteForDJVM fun URI.toPath(): Path = Paths.get(this) +fun URI.toPath(): Path = Paths.get(this) -@DeleteForDJVM fun URL.toPath(): Path = toURI().toPath() +fun URL.toPath(): Path = toURI().toPath() val DEFAULT_HTTP_CONNECT_TIMEOUT = 30.seconds.toMillis() val DEFAULT_HTTP_READ_TIMEOUT = 30.seconds.toMillis() -@DeleteForDJVM fun URL.openHttpConnection(proxy: Proxy? = null): HttpURLConnection = ( if (proxy == null) openConnection() else openConnection(proxy)).also { @@ -465,7 +446,6 @@ fun URL.openHttpConnection(proxy: Proxy? = null): HttpURLConnection = ( it.readTimeout = DEFAULT_HTTP_READ_TIMEOUT.toInt() } as HttpURLConnection -@DeleteForDJVM fun URL.post(serializedData: OpaqueBytes, vararg properties: Pair, proxy: Proxy? = null): ByteArray { return openHttpConnection(proxy).run { doOutput = true @@ -478,7 +458,6 @@ fun URL.post(serializedData: OpaqueBytes, vararg properties: Pair HttpURLConnection.responseAs(): T { checkOkResponse() return inputStream.readObject() } /** Analogous to [Thread.join]. */ -@DeleteForDJVM fun ExecutorService.join() { shutdown() // Do not change to shutdownNow, tests use this method to assert the executor has no more tasks. while (!awaitTermination(1, TimeUnit.SECONDS)) { @@ -524,13 +500,11 @@ $trustAnchors""", e, this, e.index) } } -@DeleteForDJVM inline fun T.signWithCert(signer: (SerializedBytes) -> DigitalSignatureWithCert): SignedDataWithCert { val serialised = serialize() return SignedDataWithCert(serialised, signer(serialised)) } -@DeleteForDJVM fun T.signWithCert(privateKey: PrivateKey, certificate: X509Certificate): SignedDataWithCert { return signWithCert { val signature = Crypto.doSign(privateKey, it.bytes) @@ -538,19 +512,17 @@ fun T.signWithCert(privateKey: PrivateKey, certificate: X509Certificat } } -@DeleteForDJVM fun T.signWithCertPath(privateKey: PrivateKey, certPath: List): SignedDataWithCert { return signWithCert { val signature = Crypto.doSign(privateKey, it.bytes) DigitalSignatureWithCert(certPath.first(), certPath.takeLast(certPath.size - 1), signature) } } -@DeleteForDJVM + inline fun SerializedBytes.sign(signer: (SerializedBytes) -> DigitalSignature.WithKey): SignedData { return SignedData(this, signer(this)) } -@DeleteForDJVM fun SerializedBytes.sign(keyPair: KeyPair): SignedData = SignedData(this, keyPair.sign(this.bytes)) fun ByteBuffer.copyBytes(): ByteArray = ByteArray(remaining()).also { get(it) } @@ -573,9 +545,6 @@ fun SerializedBytes.checkPayloadIs(type: Class): Untrustworthy ?: throw IllegalArgumentException("We were expecting a ${type.name} but we instead got a ${payloadData.javaClass.name} ($payloadData)") } -/** - * Simple Map structure that can be used as a cache in the DJVM. - */ fun createSimpleCache(maxSize: Int, onEject: (MutableMap.MutableEntry) -> Unit = {}): MutableMap { return object : LinkedHashMap() { override fun removeEldestEntry(eldest: MutableMap.MutableEntry?): Boolean { diff --git a/core/src/main/kotlin/net/corda/core/internal/LazyStickyPool.kt b/core/src/main/kotlin/net/corda/core/internal/LazyStickyPool.kt index 868997bc56..76b5f3f17e 100644 --- a/core/src/main/kotlin/net/corda/core/internal/LazyStickyPool.kt +++ b/core/src/main/kotlin/net/corda/core/internal/LazyStickyPool.kt @@ -1,6 +1,5 @@ package net.corda.core.internal -import net.corda.core.DeleteForDJVM import java.util.* import java.util.concurrent.LinkedBlockingQueue @@ -12,7 +11,6 @@ import java.util.concurrent.LinkedBlockingQueue * @param newInstance The function to call to create a pooled resource. */ // TODO This could be implemented more efficiently. Currently the "non-sticky" use case is not optimised, it just chooses a random instance to wait on. -@DeleteForDJVM class LazyStickyPool( size: Int, private val newInstance: () -> A diff --git a/core/src/main/kotlin/net/corda/core/internal/LegalNameValidator.kt b/core/src/main/kotlin/net/corda/core/internal/LegalNameValidator.kt index db15d33356..9f97612852 100644 --- a/core/src/main/kotlin/net/corda/core/internal/LegalNameValidator.kt +++ b/core/src/main/kotlin/net/corda/core/internal/LegalNameValidator.kt @@ -1,6 +1,5 @@ package net.corda.core.internal -import net.corda.core.KeepForDJVM import net.corda.core.internal.LegalNameValidator.normalize import java.text.Normalizer import javax.security.auth.x500.X500Principal @@ -106,14 +105,12 @@ object LegalNameValidator { abstract fun validate(legalName: T) - @KeepForDJVM private class UnicodeNormalizationRule : Rule() { override fun validate(legalName: String) { require(legalName == normalize(legalName)) { "Legal name must be normalized. Please use 'normalize' to normalize the legal name before validation." } } } - @KeepForDJVM private class UnicodeRangeRule(vararg supportScripts: Character.UnicodeBlock) : Rule() { val supportScriptsSet = supportScripts.toSet() @@ -124,7 +121,6 @@ object LegalNameValidator { } } - @KeepForDJVM private class CharacterRule(vararg val bannedChars: Char) : Rule() { override fun validate(legalName: String) { bannedChars.forEach { @@ -133,7 +129,6 @@ object LegalNameValidator { } } - @KeepForDJVM private class WordRule(vararg val bannedWords: String) : Rule() { override fun validate(legalName: String) { bannedWords.forEach { @@ -142,14 +137,12 @@ object LegalNameValidator { } } - @KeepForDJVM private class LengthRule(val maxLength: Int) : Rule() { override fun validate(legalName: String) { require(legalName.length <= maxLength) { "Legal name longer then $maxLength characters." } } } - @KeepForDJVM private class CapitalLetterRule : Rule() { override fun validate(legalName: String) { val capitalizedLegalName = legalName.capitalize() @@ -157,7 +150,6 @@ object LegalNameValidator { } } - @KeepForDJVM private class X500NameRule : Rule() { override fun validate(legalName: String) { // This will throw IllegalArgumentException if the name does not comply with X500 name format. @@ -165,7 +157,6 @@ object LegalNameValidator { } } - @KeepForDJVM private class MustHaveAtLeastTwoLettersRule : Rule() { override fun validate(legalName: String) { // Try to exclude names like "/", "£", "X" etc. diff --git a/core/src/main/kotlin/net/corda/core/internal/LifeCycle.kt b/core/src/main/kotlin/net/corda/core/internal/LifeCycle.kt index d0f3118309..3b27cd746f 100644 --- a/core/src/main/kotlin/net/corda/core/internal/LifeCycle.kt +++ b/core/src/main/kotlin/net/corda/core/internal/LifeCycle.kt @@ -1,6 +1,5 @@ package net.corda.core.internal -import net.corda.core.DeleteForDJVM import java.util.concurrent.locks.ReentrantReadWriteLock import kotlin.concurrent.withLock @@ -10,7 +9,6 @@ import kotlin.concurrent.withLock * * @param initial The initial state. */ -@DeleteForDJVM class LifeCycle>(initial: S) { private val lock = ReentrantReadWriteLock() private var state = initial diff --git a/core/src/main/kotlin/net/corda/core/internal/PathUtils.kt b/core/src/main/kotlin/net/corda/core/internal/PathUtils.kt index e8559f2f62..ffe686894c 100644 --- a/core/src/main/kotlin/net/corda/core/internal/PathUtils.kt +++ b/core/src/main/kotlin/net/corda/core/internal/PathUtils.kt @@ -1,7 +1,5 @@ -@file:DeleteForDJVM package net.corda.core.internal -import net.corda.core.DeleteForDJVM import net.corda.core.crypto.SecureHash import net.corda.core.serialization.deserialize import java.io.* diff --git a/core/src/main/kotlin/net/corda/core/internal/ResolveTransactionsFlow.kt b/core/src/main/kotlin/net/corda/core/internal/ResolveTransactionsFlow.kt index cf3359f2e7..882ca901fe 100644 --- a/core/src/main/kotlin/net/corda/core/internal/ResolveTransactionsFlow.kt +++ b/core/src/main/kotlin/net/corda/core/internal/ResolveTransactionsFlow.kt @@ -1,7 +1,6 @@ package net.corda.core.internal import co.paralleluniverse.fibers.Suspendable -import net.corda.core.DeleteForDJVM import net.corda.core.crypto.SecureHash import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowSession @@ -16,7 +15,6 @@ import net.corda.core.utilities.trace * Resolves transactions for the specified [txHashes] along with their full history (dependency graph) from [otherSide]. * Each retrieved transaction is validated and inserted into the local transaction storage. */ -@DeleteForDJVM class ResolveTransactionsFlow private constructor( val initialTx: SignedTransaction?, val txHashes: Set, diff --git a/core/src/main/kotlin/net/corda/core/internal/ServiceHubCoreInternal.kt b/core/src/main/kotlin/net/corda/core/internal/ServiceHubCoreInternal.kt index f5febbad97..803b52d300 100644 --- a/core/src/main/kotlin/net/corda/core/internal/ServiceHubCoreInternal.kt +++ b/core/src/main/kotlin/net/corda/core/internal/ServiceHubCoreInternal.kt @@ -1,7 +1,6 @@ package net.corda.core.internal import co.paralleluniverse.fibers.Suspendable -import net.corda.core.DeleteForDJVM import net.corda.core.crypto.SecureHash import net.corda.core.crypto.TransactionSignature import net.corda.core.flows.TransactionMetadata @@ -14,7 +13,6 @@ import net.corda.core.transactions.SignedTransaction import java.util.concurrent.ExecutorService // TODO: This should really be called ServiceHubInternal but that name is already taken by net.corda.node.services.api.ServiceHubInternal. -@DeleteForDJVM interface ServiceHubCoreInternal : ServiceHub { val externalOperationExecutor: ExecutorService diff --git a/core/src/main/kotlin/net/corda/core/internal/ThreadBox.kt b/core/src/main/kotlin/net/corda/core/internal/ThreadBox.kt index b19439710b..eedd576694 100644 --- a/core/src/main/kotlin/net/corda/core/internal/ThreadBox.kt +++ b/core/src/main/kotlin/net/corda/core/internal/ThreadBox.kt @@ -1,6 +1,5 @@ package net.corda.core.internal -import net.corda.core.DeleteForDJVM import java.util.concurrent.locks.ReentrantLock import kotlin.concurrent.withLock @@ -22,7 +21,6 @@ import kotlin.concurrent.withLock * val ii = state.locked { i } * ``` */ -@DeleteForDJVM class ThreadBox(val content: T, val lock: ReentrantLock = ReentrantLock()) { inline fun locked(body: T.() -> R): R = lock.withLock { body(content) } inline fun alreadyLocked(body: T.() -> R): R { diff --git a/core/src/main/kotlin/net/corda/core/internal/TransactionUtils.kt b/core/src/main/kotlin/net/corda/core/internal/TransactionUtils.kt index 7bdfec76be..41e94a236a 100644 --- a/core/src/main/kotlin/net/corda/core/internal/TransactionUtils.kt +++ b/core/src/main/kotlin/net/corda/core/internal/TransactionUtils.kt @@ -1,6 +1,5 @@ package net.corda.core.internal -import net.corda.core.KeepForDJVM import net.corda.core.contracts.* import net.corda.core.crypto.DigestService import net.corda.core.crypto.SecureHash @@ -177,7 +176,6 @@ fun createComponentGroups(inputs: List, * A SerializedStateAndRef is a pair (BinaryStateRepresentation, StateRef). * The [serializedState] is the actual component from the original wire transaction. */ -@KeepForDJVM data class SerializedStateAndRef(val serializedState: SerializedBytes>, val ref: StateRef) { fun toStateAndRef(factory: SerializationFactory, context: SerializationContext) = StateAndRef(serializedState.deserialize(factory, context), ref) fun toStateAndRef(): StateAndRef { diff --git a/core/src/main/kotlin/net/corda/core/internal/TransactionVerifierServiceInternal.kt b/core/src/main/kotlin/net/corda/core/internal/TransactionVerifierServiceInternal.kt index 0171d71e91..840163238f 100644 --- a/core/src/main/kotlin/net/corda/core/internal/TransactionVerifierServiceInternal.kt +++ b/core/src/main/kotlin/net/corda/core/internal/TransactionVerifierServiceInternal.kt @@ -1,7 +1,5 @@ package net.corda.core.internal -import net.corda.core.DeleteForDJVM -import net.corda.core.KeepForDJVM import net.corda.core.concurrent.CordaFuture import net.corda.core.contracts.Attachment import net.corda.core.contracts.Contract @@ -38,7 +36,6 @@ import net.corda.core.utilities.loggerFor import java.util.function.Function import java.util.function.Supplier -@DeleteForDJVM interface TransactionVerifierServiceInternal { fun reverifyWithFixups(transaction: LedgerTransaction, missingClass: String?): CordaFuture<*> } @@ -89,7 +86,6 @@ abstract class AbstractVerifier( * Because we create a separate [LedgerTransaction] onto which we need to perform verification, it becomes important we don't verify the * wrong object instance. This class helps avoid that. */ -@KeepForDJVM private class Validator(private val ltx: LedgerTransaction, private val transactionClassLoader: ClassLoader) { private val inputStates: List> = ltx.inputs.map(StateAndRef::state) private val allStates: List> = inputStates + ltx.references.map(StateAndRef::state) + ltx.outputs @@ -440,12 +436,7 @@ private class Validator(private val ltx: LedgerTransaction, private val transact * its contents, as well as executing all of its smart contracts. */ @Suppress("TooGenericExceptionCaught") -@KeepForDJVM class TransactionVerifier(private val transactionClassLoader: ClassLoader) : Function, Unit> { - // This constructor is used inside the DJVM's sandbox. - @Suppress("unused") - constructor() : this(ClassLoader.getSystemClassLoader()) - // Loads the contract class from the transactionClassLoader. private fun createContractClass(id: SecureHash, contractClassName: ContractClassName): Class { return try { @@ -462,15 +453,7 @@ class TransactionVerifier(private val transactionClassLoader: ClassLoader) : Fun createContractClass(ltx.id, contractClassName) }.map { contractClass -> try { - /** - * This function must execute within the DJVM's sandbox, which does not - * permit user code to invoke [java.lang.reflect.Constructor.newInstance]. - * (This would be fixable now, provided the constructor is public.) - * - * [Class.newInstance] is deprecated as of Java 9. - */ - @Suppress("deprecation") - contractClass.newInstance() + contractClass.getDeclaredConstructor().newInstance() } catch (e: Exception) { throw ContractCreationError(ltx.id, contractClass.name, e) } diff --git a/core/src/main/kotlin/net/corda/core/internal/X500Utils.kt b/core/src/main/kotlin/net/corda/core/internal/X500Utils.kt index 35b7cddefa..4359139eaf 100644 --- a/core/src/main/kotlin/net/corda/core/internal/X500Utils.kt +++ b/core/src/main/kotlin/net/corda/core/internal/X500Utils.kt @@ -1,8 +1,5 @@ -@file:KeepForDJVM - package net.corda.core.internal -import net.corda.core.KeepForDJVM import net.corda.core.identity.CordaX500Name import org.bouncycastle.asn1.ASN1ObjectIdentifier import org.bouncycastle.asn1.x500.AttributeTypeAndValue diff --git a/core/src/main/kotlin/net/corda/core/internal/X509EdDSAEngine.kt b/core/src/main/kotlin/net/corda/core/internal/X509EdDSAEngine.kt index 1900f23eb1..94c4897da8 100644 --- a/core/src/main/kotlin/net/corda/core/internal/X509EdDSAEngine.kt +++ b/core/src/main/kotlin/net/corda/core/internal/X509EdDSAEngine.kt @@ -1,6 +1,5 @@ package net.corda.core.internal -import net.corda.core.DeleteForDJVM import net.corda.core.crypto.Crypto import net.i2p.crypto.eddsa.EdDSAEngine import net.i2p.crypto.eddsa.EdDSAPublicKey @@ -32,7 +31,7 @@ class X509EdDSAEngine : Signature { } override fun engineInitSign(privateKey: PrivateKey) = engine.initSign(privateKey) - @DeleteForDJVM + override fun engineInitSign(privateKey: PrivateKey, random: SecureRandom) = engine.initSign(privateKey, random) override fun engineInitVerify(publicKey: PublicKey) { diff --git a/core/src/main/kotlin/net/corda/core/internal/cordapp/CordappImpl.kt b/core/src/main/kotlin/net/corda/core/internal/cordapp/CordappImpl.kt index 327723cb32..32951790c1 100644 --- a/core/src/main/kotlin/net/corda/core/internal/cordapp/CordappImpl.kt +++ b/core/src/main/kotlin/net/corda/core/internal/cordapp/CordappImpl.kt @@ -1,6 +1,5 @@ package net.corda.core.internal.cordapp -import net.corda.core.DeleteForDJVM import net.corda.core.cordapp.Cordapp import net.corda.core.crypto.SecureHash import net.corda.core.flows.FlowLogic @@ -17,7 +16,6 @@ import net.corda.core.serialization.SerializeAsToken import java.net.URL import java.nio.file.Paths -@DeleteForDJVM data class CordappImpl( override val contractClassNames: List, override val initiatedFlows: List>>, diff --git a/core/src/main/kotlin/net/corda/core/internal/notary/NotaryService.kt b/core/src/main/kotlin/net/corda/core/internal/notary/NotaryService.kt index 3f1842e25b..479d6476a5 100644 --- a/core/src/main/kotlin/net/corda/core/internal/notary/NotaryService.kt +++ b/core/src/main/kotlin/net/corda/core/internal/notary/NotaryService.kt @@ -1,6 +1,5 @@ package net.corda.core.internal.notary -import net.corda.core.DeleteForDJVM import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowSession import net.corda.core.flows.NotaryFlow @@ -9,7 +8,6 @@ import net.corda.core.node.ServiceHub import net.corda.core.serialization.SingletonSerializeAsToken import java.security.PublicKey -@DeleteForDJVM abstract class NotaryService : SingletonSerializeAsToken() { abstract val services: ServiceHub abstract val notaryIdentityKey: PublicKey diff --git a/core/src/main/kotlin/net/corda/core/internal/rules/TargetVersionDependentRules.kt b/core/src/main/kotlin/net/corda/core/internal/rules/TargetVersionDependentRules.kt index ae1ce4ab31..1b845a49e3 100644 --- a/core/src/main/kotlin/net/corda/core/internal/rules/TargetVersionDependentRules.kt +++ b/core/src/main/kotlin/net/corda/core/internal/rules/TargetVersionDependentRules.kt @@ -10,8 +10,6 @@ import java.util.concurrent.ConcurrentHashMap import java.util.jar.JarInputStream // This file provides rules that depend on the targetVersion of the current Contract or Flow. -// Rules defined in this package are automatically removed from the DJVM in core-deterministic, -// and must be replaced by a deterministic alternative defined within that module. /** * Rule which determines whether [ContractState]s must declare the [Contract] to which they belong (e.g. via the diff --git a/core/src/main/kotlin/net/corda/core/internal/utilities/Internable.kt b/core/src/main/kotlin/net/corda/core/internal/utilities/Internable.kt index f1523ed038..4d1d578bb3 100644 --- a/core/src/main/kotlin/net/corda/core/internal/utilities/Internable.kt +++ b/core/src/main/kotlin/net/corda/core/internal/utilities/Internable.kt @@ -1,15 +1,12 @@ package net.corda.core.internal.utilities import net.corda.core.CordaInternal -import net.corda.core.KeepForDJVM -@KeepForDJVM interface Internable { @CordaInternal val interner: PrivateInterner } -@KeepForDJVM @CordaInternal interface IternabilityVerifier { // If a type being interned has a slightly dodgy equality check, the more strict rules you probably @@ -17,7 +14,6 @@ interface IternabilityVerifier { fun choose(original: T, interned: T): T } -@KeepForDJVM @CordaInternal class AlwaysInternableVerifier : IternabilityVerifier { override fun choose(original: T, interned: T): T = interned diff --git a/core/src/main/kotlin/net/corda/core/node/AppServiceHub.kt b/core/src/main/kotlin/net/corda/core/node/AppServiceHub.kt index e1726a28ed..a6041f81f0 100644 --- a/core/src/main/kotlin/net/corda/core/node/AppServiceHub.kt +++ b/core/src/main/kotlin/net/corda/core/node/AppServiceHub.kt @@ -1,6 +1,5 @@ package net.corda.core.node -import net.corda.core.DeleteForDJVM import net.corda.core.flows.FlowLogic import net.corda.core.messaging.FlowHandle import net.corda.core.messaging.FlowProgressHandle @@ -16,7 +15,6 @@ import rx.Observable * In particular such a [net.corda.core.node.services.CordaService] can initiate and track flows marked * with [net.corda.core.flows.StartableByService]. */ -@DeleteForDJVM interface AppServiceHub : ServiceHub { companion object { diff --git a/core/src/main/kotlin/net/corda/core/node/NetworkParameters.kt b/core/src/main/kotlin/net/corda/core/node/NetworkParameters.kt index 4495a483ef..098b45f802 100644 --- a/core/src/main/kotlin/net/corda/core/node/NetworkParameters.kt +++ b/core/src/main/kotlin/net/corda/core/node/NetworkParameters.kt @@ -1,7 +1,6 @@ package net.corda.core.node import net.corda.core.CordaRuntimeException -import net.corda.core.KeepForDJVM import net.corda.core.crypto.toStringShort import net.corda.core.identity.Party import net.corda.core.internal.noPackageOverlap @@ -44,7 +43,6 @@ import java.util.Collections.unmodifiableMap * it can be used. This can be overridden in the node configuration or if a more recent database backup is indicated via RPC / shell. It is * optional in both the network parameters and the node configuration and if no value is set for either then it is assumed to be zero. */ -@KeepForDJVM @CordaSerializable @Suppress("LongParameterList") data class NetworkParameters( @@ -284,7 +282,6 @@ private inline fun unmodifiable(map: Map, transform: (Map.Entry /** Future to track completion of the NetworkMapService registration. */ - @get:DeleteForDJVM val nodeReady: CordaFuture + val nodeReady: CordaFuture /** * Atomically get the current party nodes and a stream of updates. Note that the Observable buffers updates until the diff --git a/core/src/main/kotlin/net/corda/core/node/services/TimeWindowChecker.kt b/core/src/main/kotlin/net/corda/core/node/services/TimeWindowChecker.kt index b7bf3611fa..a4eeb0b487 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/TimeWindowChecker.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/TimeWindowChecker.kt @@ -1,6 +1,5 @@ package net.corda.core.node.services -import net.corda.core.DeleteForDJVM import net.corda.core.contracts.TimeWindow import java.time.Clock @@ -8,7 +7,6 @@ import java.time.Clock * Checks if the current instant provided by the input clock falls within the provided time-window. */ @Deprecated("This class is no longer used") -@DeleteForDJVM class TimeWindowChecker(val clock: Clock = Clock.systemUTC()) { fun isValid(timeWindow: TimeWindow): Boolean = clock.instant() in timeWindow } diff --git a/core/src/main/kotlin/net/corda/core/node/services/TransactionStorage.kt b/core/src/main/kotlin/net/corda/core/node/services/TransactionStorage.kt index 5bdb494be6..b04c96729f 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/TransactionStorage.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/TransactionStorage.kt @@ -1,6 +1,5 @@ package net.corda.core.node.services -import net.corda.core.DeleteForDJVM import net.corda.core.DoNotImplement import net.corda.core.concurrent.CordaFuture import net.corda.core.crypto.SecureHash @@ -11,7 +10,6 @@ import rx.Observable /** * Thread-safe storage of transactions. */ -@DeleteForDJVM @DoNotImplement interface TransactionStorage { /** diff --git a/core/src/main/kotlin/net/corda/core/node/services/TransactionVerifierService.kt b/core/src/main/kotlin/net/corda/core/node/services/TransactionVerifierService.kt index e1c731ce86..d72eec72f2 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/TransactionVerifierService.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/TransactionVerifierService.kt @@ -1,6 +1,5 @@ package net.corda.core.node.services -import net.corda.core.DeleteForDJVM import net.corda.core.DoNotImplement import net.corda.core.concurrent.CordaFuture import net.corda.core.transactions.LedgerTransaction @@ -10,7 +9,6 @@ import net.corda.core.transactions.LedgerTransaction * @suppress */ @DoNotImplement -@DeleteForDJVM interface TransactionVerifierService { /** * @param transaction The transaction to be verified. diff --git a/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt b/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt index a6b42b9541..e11e59cc9c 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt @@ -3,7 +3,6 @@ package net.corda.core.node.services import co.paralleluniverse.fibers.Suspendable -import net.corda.core.DeleteForDJVM import net.corda.core.DoNotImplement import net.corda.core.concurrent.CordaFuture import net.corda.core.contracts.AlwaysAcceptAttachmentConstraint @@ -358,7 +357,6 @@ interface VaultService { /** * Provide a [CordaFuture] for when a [StateRef] is consumed, which can be very useful in building tests. */ - @DeleteForDJVM fun whenConsumed(ref: StateRef): CordaFuture> { val query = QueryCriteria.VaultQueryCriteria( stateRefs = listOf(ref), diff --git a/core/src/main/kotlin/net/corda/core/schemas/PersistentTypes.kt b/core/src/main/kotlin/net/corda/core/schemas/PersistentTypes.kt index 4a7a10b21e..5295b4a46a 100644 --- a/core/src/main/kotlin/net/corda/core/schemas/PersistentTypes.kt +++ b/core/src/main/kotlin/net/corda/core/schemas/PersistentTypes.kt @@ -1,6 +1,5 @@ package net.corda.core.schemas -import net.corda.core.KeepForDJVM import net.corda.core.contracts.ContractState import net.corda.core.contracts.StateRef import net.corda.core.serialization.CordaSerializable @@ -16,7 +15,6 @@ import javax.persistence.MappedSuperclass * A contract state that may be mapped to database schemas configured for this node to support querying for, * or filtering of, states. */ -@KeepForDJVM interface QueryableState : ContractState { /** * Enumerate the schemas this state can export representations of itself as. @@ -39,7 +37,6 @@ interface QueryableState : ContractState { * @param version The version number of this instance within the family. * @param mappedTypes The JPA entity classes that the ORM layer needs to be configure with for this schema. */ -@KeepForDJVM open class MappedSchema(schemaFamily: Class<*>, val version: Int, val mappedTypes: Iterable>) { @@ -78,7 +75,6 @@ open class MappedSchema(schemaFamily: Class<*>, * A super class for all mapped states exported to a schema that ensures the [StateRef] appears on the database row. The * [StateRef] will be set to the correct value by the framework (there's no need to set during mapping generation by the state itself). */ -@KeepForDJVM @MappedSuperclass @CordaSerializable class PersistentState(@EmbeddedId override var stateRef: PersistentStateRef? = null) : DirectStatePersistable @@ -86,7 +82,6 @@ class PersistentState(@EmbeddedId override var stateRef: PersistentStateRef? = n /** * Embedded [StateRef] representation used in state mapping. */ -@KeepForDJVM @Embeddable @Immutable @@ -104,7 +99,6 @@ data class PersistentStateRef( /** * Marker interface to denote a persistable Corda state entity that will always have a transaction id and index */ -@KeepForDJVM interface StatePersistable /** diff --git a/core/src/main/kotlin/net/corda/core/serialization/MissingAttachmentsException.kt b/core/src/main/kotlin/net/corda/core/serialization/MissingAttachmentsException.kt index b3c73786a1..f353a6ee27 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/MissingAttachmentsException.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/MissingAttachmentsException.kt @@ -1,11 +1,9 @@ package net.corda.core.serialization import net.corda.core.CordaException -import net.corda.core.KeepForDJVM import net.corda.core.crypto.SecureHash /** Thrown during deserialization to indicate that an attachment needed to construct the [WireTransaction] is not found. */ -@KeepForDJVM @CordaSerializable class MissingAttachmentsException(val ids: List, message: String?) : CordaException(message) { diff --git a/core/src/main/kotlin/net/corda/core/serialization/MissingAttachmentsRuntimeException.kt b/core/src/main/kotlin/net/corda/core/serialization/MissingAttachmentsRuntimeException.kt index ddeeacc618..d09b700e25 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/MissingAttachmentsRuntimeException.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/MissingAttachmentsRuntimeException.kt @@ -1,10 +1,8 @@ package net.corda.core.serialization import net.corda.core.CordaRuntimeException -import net.corda.core.KeepForDJVM import net.corda.core.node.services.AttachmentId -@KeepForDJVM @CordaSerializable class MissingAttachmentsRuntimeException(val ids: List, message: String?, cause: Throwable?) : CordaRuntimeException(message, cause) { diff --git a/core/src/main/kotlin/net/corda/core/serialization/SerializationAPI.kt b/core/src/main/kotlin/net/corda/core/serialization/SerializationAPI.kt index 77289d8c8e..f4c543a25d 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/SerializationAPI.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/SerializationAPI.kt @@ -1,9 +1,7 @@ -@file:KeepForDJVM + package net.corda.core.serialization -import net.corda.core.DeleteForDJVM import net.corda.core.DoNotImplement -import net.corda.core.KeepForDJVM import net.corda.core.crypto.SecureHash import net.corda.core.crypto.sha256 import net.corda.core.serialization.internal.effectiveSerializationEnv @@ -118,7 +116,6 @@ interface SerializationEncoding /** * Parameters to serialization and deserialization. */ -@KeepForDJVM @DoNotImplement interface SerializationContext { /** @@ -254,7 +251,6 @@ interface SerializationContext { /** * The use case that we are serializing for, since it influences the implementations chosen. */ - @KeepForDJVM enum class UseCase { P2P, RPCServer, @@ -269,7 +265,6 @@ interface SerializationContext { * others being set that aren't keyed on this enumeration, but for general use properties adding a * well known key here is preferred. */ -@KeepForDJVM enum class ContextPropertyKeys { SERIALIZERS } @@ -277,13 +272,12 @@ enum class ContextPropertyKeys { /** * Global singletons to be used as defaults that are injected elsewhere (generally, in the node or in RPC client). */ -@KeepForDJVM object SerializationDefaults { val SERIALIZATION_FACTORY get() = effectiveSerializationEnv.serializationFactory val P2P_CONTEXT get() = effectiveSerializationEnv.p2pContext - @DeleteForDJVM val RPC_SERVER_CONTEXT get() = effectiveSerializationEnv.rpcServerContext - @DeleteForDJVM val RPC_CLIENT_CONTEXT get() = effectiveSerializationEnv.rpcClientContext - @DeleteForDJVM val STORAGE_CONTEXT get() = effectiveSerializationEnv.storageContext + val RPC_SERVER_CONTEXT get() = effectiveSerializationEnv.rpcServerContext + val RPC_CLIENT_CONTEXT get() = effectiveSerializationEnv.rpcClientContext + val STORAGE_CONTEXT get() = effectiveSerializationEnv.storageContext } /** @@ -323,7 +317,6 @@ inline fun ByteArray.deserialize(serializationFactory: Seriali /** * Convenience extension method for deserializing a JDBC Blob, utilising the defaults. */ -@DeleteForDJVM inline fun Blob.deserialize(serializationFactory: SerializationFactory = SerializationFactory.defaultFactory, context: SerializationContext = serializationFactory.defaultContext): T { return this.getBytes(1, this.length().toInt()).deserialize(serializationFactory, context) @@ -342,7 +335,6 @@ fun T.serialize(serializationFactory: SerializationFactory = Serializa * to get the original object back. */ @Suppress("unused") -@KeepForDJVM @CordaSerializable class SerializedBytes(bytes: ByteArray) : OpaqueBytes(bytes) { companion object { @@ -362,12 +354,10 @@ class SerializedBytes(bytes: ByteArray) : OpaqueBytes(bytes) { val hash: SecureHash by lazy { bytes.sha256() } } -@KeepForDJVM interface ClassWhitelist { fun hasListed(type: Class<*>): Boolean } -@KeepForDJVM @DoNotImplement interface EncodingWhitelist { fun acceptEncoding(encoding: SerializationEncoding): Boolean diff --git a/core/src/main/kotlin/net/corda/core/serialization/SerializationCustomSerializer.kt b/core/src/main/kotlin/net/corda/core/serialization/SerializationCustomSerializer.kt index ed387f8f94..979be233e4 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/SerializationCustomSerializer.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/SerializationCustomSerializer.kt @@ -1,7 +1,5 @@ package net.corda.core.serialization -import net.corda.core.KeepForDJVM - /** * Allows CorDapps to provide custom serializers for third party libraries where those libraries cannot * be recompiled with the -parameters flag rendering their classes natively serializable by Corda. In this case @@ -11,7 +9,6 @@ import net.corda.core.KeepForDJVM * NOTE: The proxy object should be specified as a separate class. However, this can be defined within the * scope of the custom serializer. */ -@KeepForDJVM interface SerializationCustomSerializer { /** * Should facilitate the conversion of the third party object into the serializable @@ -34,7 +31,6 @@ interface SerializationCustomSerializer { * NOTE: Only implement this interface if you have a class that triggers an error during normal checkpoint * serialization/deserialization. */ -@KeepForDJVM interface CheckpointCustomSerializer { /** * Should facilitate the conversion of the third party object into the serializable diff --git a/core/src/main/kotlin/net/corda/core/serialization/SerializationToken.kt b/core/src/main/kotlin/net/corda/core/serialization/SerializationToken.kt index 1bc11091f3..9ec9fb75c0 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/SerializationToken.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/SerializationToken.kt @@ -1,6 +1,5 @@ package net.corda.core.serialization -import net.corda.core.DeleteForDJVM import net.corda.core.node.ServiceHub import net.corda.core.serialization.SingletonSerializationToken.Companion.singletonSerializationToken @@ -19,7 +18,6 @@ import net.corda.core.serialization.SingletonSerializationToken.Companion.single * * This models a similar pattern to the readReplace/writeReplace methods in Java serialization. */ -@DeleteForDJVM @CordaSerializable interface SerializeAsToken { fun toToken(context: SerializeAsTokenContext): SerializationToken @@ -28,7 +26,6 @@ interface SerializeAsToken { /** * This represents a token in the serialized stream for an instance of a type that implements [SerializeAsToken]. */ -@DeleteForDJVM interface SerializationToken { fun fromToken(context: SerializeAsTokenContext): Any } @@ -36,7 +33,6 @@ interface SerializationToken { /** * A context for mapping SerializationTokens to/from SerializeAsTokens. */ -@DeleteForDJVM interface SerializeAsTokenContext { val serviceHub: ServiceHub fun putSingleton(toBeTokenized: SerializeAsToken) @@ -47,7 +43,6 @@ interface SerializeAsTokenContext { * A class representing a [SerializationToken] for some object that is not serializable but can be looked up * (when deserialized) via just the class name. */ -@DeleteForDJVM class SingletonSerializationToken private constructor(private val className: String) : SerializationToken { override fun fromToken(context: SerializeAsTokenContext) = context.getSingleton(className) @@ -63,7 +58,6 @@ class SingletonSerializationToken private constructor(private val className: Str * A base class for implementing large objects / components / services that need to serialize themselves to a string token * to indicate which instance the token is a serialized form of. */ -@DeleteForDJVM abstract class SingletonSerializeAsToken : SerializeAsToken { private val token = singletonSerializationToken(javaClass) diff --git a/core/src/main/kotlin/net/corda/core/serialization/SerializationWhitelist.kt b/core/src/main/kotlin/net/corda/core/serialization/SerializationWhitelist.kt index cc0b56e32e..0c2be0c16b 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/SerializationWhitelist.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/SerializationWhitelist.kt @@ -1,13 +1,10 @@ package net.corda.core.serialization -import net.corda.core.KeepForDJVM - /** * Provide a subclass of this via the [java.util.ServiceLoader] mechanism to be able to whitelist types for * serialisation that you cannot otherwise annotate. The name of the class must appear in a text file on the * classpath under the path META-INF/services/net.corda.core.serialization.SerializationWhitelist */ -@KeepForDJVM interface SerializationWhitelist { /** * Optionally whitelist types for use in object serialization, as we lock down the types that can be serialized. diff --git a/core/src/main/kotlin/net/corda/core/serialization/internal/AttachmentsClassLoader.kt b/core/src/main/kotlin/net/corda/core/serialization/internal/AttachmentsClassLoader.kt index 23f61df19a..0d84556633 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/internal/AttachmentsClassLoader.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/internal/AttachmentsClassLoader.kt @@ -2,7 +2,6 @@ package net.corda.core.serialization.internal import com.github.benmanes.caffeine.cache.Cache import com.github.benmanes.caffeine.cache.Caffeine -import net.corda.core.DeleteForDJVM import net.corda.core.contracts.Attachment import net.corda.core.contracts.ContractAttachment import net.corda.core.contracts.TransactionVerificationException @@ -477,7 +476,6 @@ interface AttachmentsClassLoaderCache { fun computeIfAbsent(key: AttachmentsClassLoaderKey, mappingFunction: Function): SerializationContext } -@DeleteForDJVM class AttachmentsClassLoaderCacheImpl(cacheFactory: NamedCacheFactory) : SingletonSerializeAsToken(), AttachmentsClassLoaderCache { private class ToBeClosed( diff --git a/core/src/main/kotlin/net/corda/core/serialization/internal/CheckpointSerializationAPI.kt b/core/src/main/kotlin/net/corda/core/serialization/internal/CheckpointSerializationAPI.kt index 510986141c..5e49813957 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/internal/CheckpointSerializationAPI.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/internal/CheckpointSerializationAPI.kt @@ -1,21 +1,16 @@ package net.corda.core.serialization.internal -import net.corda.core.DeleteForDJVM import net.corda.core.DoNotImplement -import net.corda.core.KeepForDJVM import net.corda.core.serialization.* import net.corda.core.utilities.ByteSequence import net.corda.core.utilities.sequence import java.io.NotSerializableException - object CheckpointSerializationDefaults { - @DeleteForDJVM val CHECKPOINT_CONTEXT get() = effectiveSerializationEnv.checkpointContext val CHECKPOINT_SERIALIZER get() = effectiveSerializationEnv.checkpointSerializer } -@KeepForDJVM @DoNotImplement interface CheckpointSerializer { @Throws(NotSerializableException::class) @@ -28,7 +23,6 @@ interface CheckpointSerializer { /** * Parameters to checkpoint serialization and deserialization. */ -@KeepForDJVM @DoNotImplement interface CheckpointSerializationContext { /** diff --git a/core/src/main/kotlin/net/corda/core/serialization/internal/CustomSerializationSchemeUtils.kt b/core/src/main/kotlin/net/corda/core/serialization/internal/CustomSerializationSchemeUtils.kt index b0588755aa..39e4f15e8c 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/internal/CustomSerializationSchemeUtils.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/internal/CustomSerializationSchemeUtils.kt @@ -1,13 +1,11 @@ package net.corda.core.serialization.internal -import net.corda.core.KeepForDJVM import net.corda.core.serialization.SerializationMagic import net.corda.core.utilities.ByteSequence import java.nio.ByteBuffer class CustomSerializationSchemeUtils { - @KeepForDJVM companion object { private const val SERIALIZATION_SCHEME_ID_SIZE = 4 diff --git a/core/src/main/kotlin/net/corda/core/serialization/internal/MissingSerializerException.kt b/core/src/main/kotlin/net/corda/core/serialization/internal/MissingSerializerException.kt index 600f4d77cc..7d9a1c8512 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/internal/MissingSerializerException.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/internal/MissingSerializerException.kt @@ -1,13 +1,11 @@ package net.corda.core.serialization.internal -import net.corda.core.KeepForDJVM import java.io.NotSerializableException /** * Thrown by the serialization framework, probably indicating that a custom serializer * needs to be included in a transaction. */ -@KeepForDJVM open class MissingSerializerException private constructor( message: String, val typeDescriptor: String?, diff --git a/core/src/main/kotlin/net/corda/core/serialization/internal/SerializationEnvironment.kt b/core/src/main/kotlin/net/corda/core/serialization/internal/SerializationEnvironment.kt index f306e84a08..540a21c3b2 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/internal/SerializationEnvironment.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/internal/SerializationEnvironment.kt @@ -1,14 +1,11 @@ -@file:KeepForDJVM package net.corda.core.serialization.internal -import net.corda.core.KeepForDJVM import net.corda.core.internal.InheritableThreadLocalToggleField import net.corda.core.internal.SimpleToggleField import net.corda.core.internal.ThreadLocalToggleField import net.corda.core.serialization.SerializationContext import net.corda.core.serialization.SerializationFactory -@KeepForDJVM interface SerializationEnvironment { companion object { @@ -43,7 +40,6 @@ interface SerializationEnvironment { val checkpointContext: CheckpointSerializationContext } -@KeepForDJVM private class SerializationEnvironmentImpl( override val serializationFactory: SerializationFactory, override val p2pContext: SerializationContext, diff --git a/core/src/main/kotlin/net/corda/core/transactions/BaseTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/BaseTransaction.kt index 80494a8706..23b986b721 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/BaseTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/BaseTransaction.kt @@ -1,7 +1,6 @@ package net.corda.core.transactions import net.corda.core.DoNotImplement -import net.corda.core.KeepForDJVM import net.corda.core.contracts.* import net.corda.core.identity.Party import net.corda.core.internal.castIfPossible @@ -12,7 +11,6 @@ import java.util.function.Predicate /** * An abstract class defining fields shared by all transaction types in the system. */ -@KeepForDJVM @DoNotImplement abstract class BaseTransaction : NamedByHash { /** A list of reusable reference data states which can be referred to by other contracts in this transaction. */ diff --git a/core/src/main/kotlin/net/corda/core/transactions/ContractUpgradeTransactions.kt b/core/src/main/kotlin/net/corda/core/transactions/ContractUpgradeTransactions.kt index 911c67d0da..145da6a07c 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/ContractUpgradeTransactions.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/ContractUpgradeTransactions.kt @@ -1,7 +1,6 @@ package net.corda.core.transactions import net.corda.core.CordaInternal -import net.corda.core.KeepForDJVM import net.corda.core.contracts.* import net.corda.core.crypto.DigestService import net.corda.core.crypto.SecureHash @@ -32,7 +31,6 @@ import java.security.PublicKey // TODO: check transaction size is within limits /** A special transaction for upgrading the contract of a state. */ -@KeepForDJVM @CordaSerializable data class ContractUpgradeWireTransaction( /** @@ -200,7 +198,6 @@ data class ContractUpgradeWireTransaction( * is no flexibility on what parts of the transaction to reveal – the inputs, notary and network parameters hash fields are always visible and the * rest of the transaction is always hidden. Its only purpose is to hide transaction data when using a non-validating notary. */ -@KeepForDJVM @CordaSerializable data class ContractUpgradeFilteredTransaction( /** Transaction components that are exposed. */ @@ -269,7 +266,6 @@ data class ContractUpgradeFilteredTransaction( * In contrast with a regular transaction, signatures are checked against the signers specified by input states' * *participants* fields, so full resolution is needed for signature verification. */ -@KeepForDJVM class ContractUpgradeLedgerTransaction private constructor( override val inputs: List>, diff --git a/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt index 25dfa7f293..846799d0b3 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt @@ -1,8 +1,6 @@ package net.corda.core.transactions import net.corda.core.CordaInternal -import net.corda.core.KeepForDJVM -import net.corda.core.StubOutForDJVM import net.corda.core.contracts.Attachment import net.corda.core.contracts.Command import net.corda.core.contracts.CommandData @@ -61,7 +59,6 @@ import java.util.function.Supplier * [LedgerTransaction]s should never be instantiated directly from client code, but rather via WireTransaction.toLedgerTransaction */ @Suppress("LongParameterList") -@KeepForDJVM class LedgerTransaction private constructor( // DOCSTART 1 @@ -125,7 +122,6 @@ private constructor( networkParameters, references, componentGroups, serializedInputs, serializedReferences, isAttachmentTrusted, verifierFactory, attachmentsClassLoaderCache, DigestService.sha2_256) - @KeepForDJVM companion object { private val logger = contextLogger() @@ -183,8 +179,8 @@ private constructor( /** * This factory function will create an instance of [LedgerTransaction] - * that will be used for contract verification. See [BasicVerifier] and - * [DeterministicVerifier][net.corda.node.internal.djvm.DeterministicVerifier]. + * that will be used for contract verification. + * @see BasicVerifier */ @CordaInternal fun createForContractVerify( @@ -323,18 +319,13 @@ private constructor( logger.warn("Network parameters on the LedgerTransaction with id: $id are null. Please don't use deprecated constructors of the LedgerTransaction. " + "Use WireTransaction.toLedgerTransaction instead. The result of the verify method would not be accurate.") // Roll the dice - we're probably in flow context if we got here at all, which means we can fish the current params out. - try { - params = getParamsFromFlowLogic() - } catch (e: UnsupportedOperationException) { - // Inside DJVM, ignore. - } + params = getParamsFromFlowLogic() if (params == null) throw UnsupportedOperationException("Cannot verify a LedgerTransaction created using deprecated constructors outside of flow context.") } return params } - @StubOutForDJVM private fun getParamsFromFlowLogic(): NetworkParameters? { return FlowLogic.currentTopLevel?.serviceHub?.networkParameters } @@ -398,7 +389,6 @@ private constructor( * be used to simplify this logic. */ // DOCSTART 3 - @KeepForDJVM data class InOutGroup(val inputs: List, val outputs: List, val groupingKey: K) // DOCEND 3 diff --git a/core/src/main/kotlin/net/corda/core/transactions/MerkleTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/MerkleTransaction.kt index e5076d4d26..897598335b 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/MerkleTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/MerkleTransaction.kt @@ -1,7 +1,6 @@ package net.corda.core.transactions import net.corda.core.CordaException -import net.corda.core.KeepForDJVM import net.corda.core.contracts.* import net.corda.core.contracts.ComponentGroupEnum.* import net.corda.core.crypto.* @@ -90,7 +89,6 @@ abstract class TraversableTransaction(open val componentGroups: List, @@ -361,7 +358,6 @@ data class FilteredComponentGroup(override val groupIndex: Int, * @param id transaction's id. * @param reason information about the exception. */ -@KeepForDJVM @CordaSerializable class ComponentVisibilityException(val id: SecureHash, val reason: String) : CordaException("Component visibility error for transaction with id:$id. Reason: $reason") @@ -369,16 +365,13 @@ class ComponentVisibilityException(val id: SecureHash, val reason: String) : Cor * @param id transaction's id. * @param reason information about the exception. */ -@KeepForDJVM @CordaSerializable class FilteredTransactionVerificationException(val id: SecureHash, val reason: String) : CordaException("Transaction with id:$id cannot be verified. Reason: $reason") /** Wrapper over [StateRef] to be used when filtering reference states. */ -@KeepForDJVM @CordaSerializable data class ReferenceStateRef(val stateRef: StateRef) /** Wrapper over [SecureHash] to be used when filtering network parameters hash. */ -@KeepForDJVM @CordaSerializable -data class NetworkParametersHash(val hash: SecureHash) \ No newline at end of file +data class NetworkParametersHash(val hash: SecureHash) diff --git a/core/src/main/kotlin/net/corda/core/transactions/MissingContractAttachments.kt b/core/src/main/kotlin/net/corda/core/transactions/MissingContractAttachments.kt index ddcd01728f..e2debdcd1a 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/MissingContractAttachments.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/MissingContractAttachments.kt @@ -1,6 +1,5 @@ package net.corda.core.transactions -import net.corda.core.KeepForDJVM import net.corda.core.contracts.ContractState import net.corda.core.contracts.TransactionState import net.corda.core.flows.FlowException @@ -13,7 +12,6 @@ import net.corda.core.serialization.CordaSerializable * @property states States which have contracts that do not have corresponding attachments in the attachment store. */ @CordaSerializable -@KeepForDJVM class MissingContractAttachments @JvmOverloads constructor(val states: List>, contractsClassName: String? = null, minimumRequiredContractClassVersion: Version? = null) : FlowException( diff --git a/core/src/main/kotlin/net/corda/core/transactions/NotaryChangeTransactions.kt b/core/src/main/kotlin/net/corda/core/transactions/NotaryChangeTransactions.kt index d9fdee9173..ab42260f37 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/NotaryChangeTransactions.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/NotaryChangeTransactions.kt @@ -1,8 +1,6 @@ package net.corda.core.transactions import net.corda.core.CordaInternal -import net.corda.core.DeleteForDJVM -import net.corda.core.KeepForDJVM import net.corda.core.contracts.* import net.corda.core.crypto.DigestService import net.corda.core.crypto.SecureHash @@ -27,7 +25,6 @@ import java.security.PublicKey * on the fly. */ @CordaSerializable -@KeepForDJVM data class NotaryChangeWireTransaction( /** * Contains all of the transaction components in serialized form. @@ -90,7 +87,6 @@ data class NotaryChangeWireTransaction( } /** Resolves input states and network parameters and builds a [NotaryChangeLedgerTransaction]. */ - @DeleteForDJVM fun resolve(services: ServicesForResolution, sigs: List): NotaryChangeLedgerTransaction { val resolvedInputs = services.loadStates(inputs.toSet()).toList() val hashToResolve = networkParametersHash ?: services.networkParametersService.defaultHash @@ -100,7 +96,6 @@ data class NotaryChangeWireTransaction( } /** Resolves input states and builds a [NotaryChangeLedgerTransaction]. */ - @DeleteForDJVM fun resolve(services: ServiceHub, sigs: List) = resolve(services as ServicesForResolution, sigs) /** @@ -134,7 +129,6 @@ data class NotaryChangeWireTransaction( * signatures are checked against the signers specified by input states' *participants* fields, so full resolution is * needed for signature verification. */ -@KeepForDJVM class NotaryChangeLedgerTransaction private constructor( override val inputs: List>, diff --git a/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt index ec1067d815..94e2079967 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt @@ -2,8 +2,6 @@ package net.corda.core.transactions import net.corda.core.CordaException import net.corda.core.CordaThrowable -import net.corda.core.DeleteForDJVM -import net.corda.core.KeepForDJVM import net.corda.core.contracts.* import net.corda.core.crypto.* import net.corda.core.identity.Party @@ -40,7 +38,6 @@ import java.util.function.Predicate * sign. */ // DOCSTART 1 -@KeepForDJVM @CordaSerializable data class SignedTransaction(val txBits: SerializedBytes, override val sigs: List @@ -98,7 +95,6 @@ data class SignedTransaction(val txBits: SerializedBytes, return descriptions } - @DeleteForDJVM @VisibleForTesting fun withAdditionalSignature(keyPair: KeyPair, signatureMetadata: SignatureMetadata): SignedTransaction { val signableData = SignableData(tx.id, signatureMetadata) @@ -144,7 +140,6 @@ data class SignedTransaction(val txBits: SerializedBytes, * @throws SignaturesMissingException if any signatures that should have been present are missing. */ @JvmOverloads - @DeleteForDJVM @Throws(SignatureException::class, AttachmentResolutionException::class, TransactionResolutionException::class) fun toLedgerTransaction(services: ServiceHub, checkSufficientSignatures: Boolean = true): LedgerTransaction { // TODO: We could probably optimise the below by @@ -175,7 +170,6 @@ data class SignedTransaction(val txBits: SerializedBytes, * @throws SignaturesMissingException if any signatures that should have been present are missing. */ @JvmOverloads - @DeleteForDJVM @Throws(SignatureException::class, AttachmentResolutionException::class, TransactionResolutionException::class, TransactionVerificationException::class) fun verify(services: ServiceHub, checkSufficientSignatures: Boolean = true) { resolveAndCheckNetworkParameters(services) @@ -186,7 +180,7 @@ data class SignedTransaction(val txBits: SerializedBytes, } } - @DeleteForDJVM + @Suppress("ThrowsCount") private fun resolveAndCheckNetworkParameters(services: ServiceHub) { val hashOrDefault = networkParametersHash ?: services.networkParametersService.defaultHash val txNetworkParameters = services.networkParametersService.lookup(hashOrDefault) @@ -203,7 +197,6 @@ data class SignedTransaction(val txBits: SerializedBytes, } /** No contract code is run when verifying notary change transactions, it is sufficient to check invariants during initialisation. */ - @DeleteForDJVM private fun verifyNotaryChangeTransaction(services: ServiceHub, checkSufficientSignatures: Boolean) { val ntx = resolveNotaryChangeTransaction(services) if (checkSufficientSignatures) ntx.verifyRequiredSignatures() @@ -211,7 +204,6 @@ data class SignedTransaction(val txBits: SerializedBytes, } /** No contract code is run when verifying contract upgrade transactions, it is sufficient to check invariants during initialisation. */ - @DeleteForDJVM private fun verifyContractUpgradeTransaction(services: ServicesForResolution, checkSufficientSignatures: Boolean) { val ctx = resolveContractUpgradeTransaction(services) if (checkSufficientSignatures) ctx.verifyRequiredSignatures() @@ -221,7 +213,6 @@ data class SignedTransaction(val txBits: SerializedBytes, // TODO: Verify contract constraints here as well as in LedgerTransaction to ensure that anything being deserialised // from the attachment is trusted. This will require some partial serialisation work to not load the ContractState // objects from the TransactionState. - @DeleteForDJVM private fun verifyRegularTransaction(services: ServiceHub, checkSufficientSignatures: Boolean) { val ltx = toLedgerTransaction(services, checkSufficientSignatures) try { @@ -251,7 +242,6 @@ data class SignedTransaction(val txBits: SerializedBytes, } } - @DeleteForDJVM @Suppress("ThrowsCount") private fun retryVerification(cause: Throwable?, ex: Throwable, ltx: LedgerTransaction, services: ServiceHub) { when (cause) { @@ -276,7 +266,6 @@ data class SignedTransaction(val txBits: SerializedBytes, // Transactions created before Corda 4 can be missing dependencies on other CorDapps. // This code has detected a missing custom serializer - probably located inside a workflow CorDapp. // We need to extract this CorDapp from AttachmentStorage and try verifying this transaction again. - @DeleteForDJVM private fun reverifyWithFixups(ltx: LedgerTransaction, services: ServiceHub, missingClass: String?) { log.warn("""Detected that transaction $id does not contain all cordapp dependencies. |This may be the result of a bug in a previous version of Corda. @@ -292,7 +281,6 @@ data class SignedTransaction(val txBits: SerializedBytes, * Resolves the underlying base transaction and then returns it, handling any special case transactions such as * [NotaryChangeWireTransaction]. */ - @DeleteForDJVM fun resolveBaseTransaction(servicesForResolution: ServicesForResolution): BaseTransaction { return when (coreTransaction) { is NotaryChangeWireTransaction -> resolveNotaryChangeTransaction(servicesForResolution) @@ -307,7 +295,6 @@ data class SignedTransaction(val txBits: SerializedBytes, * Resolves the underlying transaction with signatures and then returns it, handling any special case transactions * such as [NotaryChangeWireTransaction]. */ - @DeleteForDJVM fun resolveTransactionWithSignatures(services: ServicesForResolution): TransactionWithSignatures { return when (coreTransaction) { is NotaryChangeWireTransaction -> resolveNotaryChangeTransaction(services) @@ -322,7 +309,6 @@ data class SignedTransaction(val txBits: SerializedBytes, * If [transaction] is a [NotaryChangeWireTransaction], loads the input states and resolves it to a * [NotaryChangeLedgerTransaction] so the signatures can be verified. */ - @DeleteForDJVM fun resolveNotaryChangeTransaction(services: ServicesForResolution): NotaryChangeLedgerTransaction { val ntx = coreTransaction as? NotaryChangeWireTransaction ?: throw IllegalStateException("Expected a ${NotaryChangeWireTransaction::class.simpleName} but found ${coreTransaction::class.simpleName}") @@ -333,14 +319,12 @@ data class SignedTransaction(val txBits: SerializedBytes, * If [transaction] is a [NotaryChangeWireTransaction], loads the input states and resolves it to a * [NotaryChangeLedgerTransaction] so the signatures can be verified. */ - @DeleteForDJVM fun resolveNotaryChangeTransaction(services: ServiceHub) = resolveNotaryChangeTransaction(services as ServicesForResolution) /** * If [coreTransaction] is a [ContractUpgradeWireTransaction], loads the input states and resolves it to a * [ContractUpgradeLedgerTransaction] so the signatures can be verified. */ - @DeleteForDJVM fun resolveContractUpgradeTransaction(services: ServicesForResolution): ContractUpgradeLedgerTransaction { val ctx = coreTransaction as? ContractUpgradeWireTransaction ?: throw IllegalStateException("Expected a ${ContractUpgradeWireTransaction::class.simpleName} but found ${coreTransaction::class.simpleName}") @@ -359,7 +343,6 @@ data class SignedTransaction(val txBits: SerializedBytes, private val log = contextLogger() } - @KeepForDJVM class SignaturesMissingException(val missing: Set, val descriptions: List, override val id: SecureHash) : NamedByHash, SignatureException(missingSignatureMsg(missing, descriptions, id)), CordaThrowable by CordaException(missingSignatureMsg(missing, descriptions, id)) diff --git a/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt b/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt index 62c022e5b8..4cede898a0 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt @@ -3,7 +3,6 @@ package net.corda.core.transactions import co.paralleluniverse.strands.Strand import net.corda.core.CordaInternal -import net.corda.core.DeleteForDJVM import net.corda.core.contracts.* import net.corda.core.crypto.CompositeKey import net.corda.core.crypto.SignableData @@ -46,7 +45,6 @@ import kotlin.reflect.KClass * When this is set to a non-null value, an output state can be added by just passing in a [ContractState] – a * [TransactionState] with this notary specified will be generated automatically. */ -@DeleteForDJVM open class TransactionBuilder( var notary: Party? = null, var lockId: UUID = defaultLockId(), diff --git a/core/src/main/kotlin/net/corda/core/transactions/TransactionWithSignatures.kt b/core/src/main/kotlin/net/corda/core/transactions/TransactionWithSignatures.kt index 74b2a2760a..72c027b9ff 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/TransactionWithSignatures.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/TransactionWithSignatures.kt @@ -1,7 +1,6 @@ package net.corda.core.transactions import net.corda.core.DoNotImplement -import net.corda.core.KeepForDJVM import net.corda.core.contracts.NamedByHash import net.corda.core.crypto.TransactionSignature import net.corda.core.crypto.isFulfilledBy @@ -13,7 +12,6 @@ import java.security.SignatureException import java.util.* /** An interface for transactions containing signatures, with logic for signature verification. */ -@KeepForDJVM @DoNotImplement interface TransactionWithSignatures : NamedByHash { /** diff --git a/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt index 5ff7cae23e..a0fa249240 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt @@ -1,8 +1,6 @@ package net.corda.core.transactions import net.corda.core.CordaInternal -import net.corda.core.DeleteForDJVM -import net.corda.core.KeepForDJVM import net.corda.core.contracts.* import net.corda.core.contracts.ComponentGroupEnum.COMMANDS_GROUP import net.corda.core.contracts.ComponentGroupEnum.OUTPUTS_GROUP @@ -49,9 +47,7 @@ import java.util.function.Predicate *

    */ @CordaSerializable -@KeepForDJVM class WireTransaction(componentGroups: List, val privacySalt: PrivacySalt, digestService: DigestService) : TraversableTransaction(componentGroups, digestService) { - @DeleteForDJVM constructor(componentGroups: List) : this(componentGroups, PrivacySalt()) /** @@ -62,7 +58,6 @@ class WireTransaction(componentGroups: List, val privacySalt: Pr @Deprecated("Required only in some unit-tests and for backwards compatibility purposes.", ReplaceWith("WireTransaction(val componentGroups: List, override val privacySalt: PrivacySalt)"), DeprecationLevel.WARNING) - @DeleteForDJVM @JvmOverloads constructor( inputs: List, @@ -106,7 +101,6 @@ class WireTransaction(componentGroups: List, val privacySalt: Pr * @throws TransactionResolutionException if an input points to a transaction not found in storage. */ @Throws(AttachmentResolutionException::class, TransactionResolutionException::class) - @DeleteForDJVM fun toLedgerTransaction(services: ServicesForResolution): LedgerTransaction { return services.specialise( toLedgerTransactionInternal( @@ -125,7 +119,6 @@ class WireTransaction(componentGroups: List, val privacySalt: Pr } // Helper for deprecated toLedgerTransaction - // TODO: revisit once Deterministic JVM code updated @Suppress("UNUSED") // not sure if this field can be removed safely?? private val missingAttachment: Attachment by lazy { object : AbstractAttachment({ byteArrayOf() }, DEPLOYED_CORDAPP_UPLOADER ) { @@ -160,23 +153,6 @@ class WireTransaction(componentGroups: List, val privacySalt: Pr ) } - // Especially crafted for TransactionVerificationRequest - @CordaInternal - internal fun toLtxDjvmInternalBridge( - resolveAttachment: (SecureHash) -> Attachment?, - resolveStateRef: (StateRef) -> TransactionState<*>?, - resolveParameters: (SecureHash?) -> NetworkParameters? - ): LedgerTransaction { - return toLedgerTransactionInternal( - { null }, - resolveAttachment, - { stateRef -> resolveStateRef(stateRef)?.serialize() }, - resolveParameters, - { true }, // Any attachment loaded through the DJVM should be trusted - null - ) - } - @Suppress("LongParameterList", "ThrowsCount") private fun toLedgerTransactionInternal( resolveIdentity: (PublicKey) -> Party?, @@ -397,7 +373,6 @@ class WireTransaction(componentGroups: List, val privacySalt: Pr } } - @DeleteForDJVM override fun toString(): String { val buf = StringBuilder() buf.appendln("Transaction:") diff --git a/core/src/main/kotlin/net/corda/core/utilities/ByteArrays.kt b/core/src/main/kotlin/net/corda/core/utilities/ByteArrays.kt index 01050545e7..67643c5998 100644 --- a/core/src/main/kotlin/net/corda/core/utilities/ByteArrays.kt +++ b/core/src/main/kotlin/net/corda/core/utilities/ByteArrays.kt @@ -1,9 +1,6 @@ @file:JvmName("ByteArrays") -@file:KeepForDJVM - package net.corda.core.utilities -import net.corda.core.KeepForDJVM import net.corda.core.serialization.CordaSerializable import java.io.ByteArrayInputStream import java.io.OutputStream @@ -19,7 +16,6 @@ import java.nio.ByteBuffer * @property offset The start position of the sequence within the byte array. * @property size The number of bytes this sequence represents. */ -@KeepForDJVM sealed class ByteSequence(private val _bytes: ByteArray, val offset: Int, val size: Int) : Comparable { /** * The underlying bytes. Some implementations may choose to make a copy of the underlying [ByteArray] for @@ -148,7 +144,6 @@ sealed class ByteSequence(private val _bytes: ByteArray, val offset: Int, val si * In an ideal JVM this would be a value type and be completely overhead free. Project Valhalla is adding such * functionality to Java, but it won't arrive for a few years yet! */ -@KeepForDJVM @CordaSerializable open class OpaqueBytes(bytes: ByteArray) : ByteSequence(bytes, 0, bytes.size) { companion object { @@ -242,7 +237,6 @@ private fun parseHexBinary(s: String): ByteArray { /** * Class is public for serialization purposes. */ -@KeepForDJVM class OpaqueBytesSubSequence(override val bytes: ByteArray, offset: Int, size: Int) : ByteSequence(bytes, offset, size) { init { require(offset >= 0 && offset < bytes.size) { "Offset must be greater than or equal to 0, and less than the size of the backing array" } diff --git a/core/src/main/kotlin/net/corda/core/utilities/EncodingUtils.kt b/core/src/main/kotlin/net/corda/core/utilities/EncodingUtils.kt index 2d0014827e..65aa987704 100644 --- a/core/src/main/kotlin/net/corda/core/utilities/EncodingUtils.kt +++ b/core/src/main/kotlin/net/corda/core/utilities/EncodingUtils.kt @@ -1,9 +1,7 @@ @file:JvmName("EncodingUtils") -@file:KeepForDJVM package net.corda.core.utilities -import net.corda.core.KeepForDJVM import net.corda.core.crypto.Base58 import net.corda.core.crypto.Crypto import net.corda.core.internal.hash diff --git a/core/src/main/kotlin/net/corda/core/utilities/Id.kt b/core/src/main/kotlin/net/corda/core/utilities/Id.kt index a68bfb2d49..5020a08700 100644 --- a/core/src/main/kotlin/net/corda/core/utilities/Id.kt +++ b/core/src/main/kotlin/net/corda/core/utilities/Id.kt @@ -1,6 +1,5 @@ package net.corda.core.utilities -import net.corda.core.DeleteForDJVM import java.time.Instant import java.time.Instant.now @@ -16,7 +15,6 @@ open class Id(val value: VALUE, val entityType: String?, val ti /** * Creates an id using [Instant.now] as timestamp. */ - @DeleteForDJVM @JvmStatic fun newInstance(value: V, entityType: String? = null, timestamp: Instant = now()) = Id(value, entityType, timestamp) } diff --git a/core/src/main/kotlin/net/corda/core/utilities/KotlinUtils.kt b/core/src/main/kotlin/net/corda/core/utilities/KotlinUtils.kt index a274085e63..f1299b61e5 100644 --- a/core/src/main/kotlin/net/corda/core/utilities/KotlinUtils.kt +++ b/core/src/main/kotlin/net/corda/core/utilities/KotlinUtils.kt @@ -1,9 +1,5 @@ -@file:KeepForDJVM - package net.corda.core.utilities -import net.corda.core.DeleteForDJVM -import net.corda.core.KeepForDJVM import net.corda.core.internal.concurrent.get import net.corda.core.internal.uncheckedCast import net.corda.core.serialization.CordaSerializable @@ -132,7 +128,6 @@ private class TransientProperty internal constructor(private val initiali fun Collection.toNonEmptySet(): NonEmptySet = NonEmptySet.copyOf(this) /** Same as [Future.get] except that the [ExecutionException] is unwrapped. */ -@DeleteForDJVM fun Future.getOrThrow(timeout: Duration? = null): V = try { get(timeout) } catch (e: ExecutionException) { diff --git a/core/src/main/kotlin/net/corda/core/utilities/NonEmptySet.kt b/core/src/main/kotlin/net/corda/core/utilities/NonEmptySet.kt index 06fb7834d6..ab5f0b86d7 100644 --- a/core/src/main/kotlin/net/corda/core/utilities/NonEmptySet.kt +++ b/core/src/main/kotlin/net/corda/core/utilities/NonEmptySet.kt @@ -1,6 +1,5 @@ package net.corda.core.utilities -import net.corda.core.KeepForDJVM import java.util.* import java.util.function.Consumer import java.util.stream.Stream @@ -8,7 +7,6 @@ import java.util.stream.Stream /** * An immutable ordered non-empty set. */ -@KeepForDJVM class NonEmptySet private constructor(private val elements: Set) : Set by elements { companion object { /** diff --git a/core/src/main/kotlin/net/corda/core/utilities/ProgressTracker.kt b/core/src/main/kotlin/net/corda/core/utilities/ProgressTracker.kt index 96a43e80e7..5532ba1f61 100644 --- a/core/src/main/kotlin/net/corda/core/utilities/ProgressTracker.kt +++ b/core/src/main/kotlin/net/corda/core/utilities/ProgressTracker.kt @@ -1,6 +1,5 @@ package net.corda.core.utilities -import net.corda.core.DeleteForDJVM import net.corda.core.internal.STRUCTURAL_STEP_PREFIX import net.corda.core.internal.warnOnce import net.corda.core.serialization.CordaSerializable @@ -32,7 +31,6 @@ import java.util.* * using the [Observable] subscribeOn call. */ @CordaSerializable -@DeleteForDJVM class ProgressTracker(vararg inputSteps: Step) { private companion object { @@ -40,7 +38,6 @@ class ProgressTracker(vararg inputSteps: Step) { } @CordaSerializable - @DeleteForDJVM sealed class Change(val progressTracker: ProgressTracker) { data class Position(val tracker: ProgressTracker, val newStep: Step) : Change(tracker) { override fun toString() = newStep.label @@ -87,17 +84,14 @@ class ProgressTracker(vararg inputSteps: Step) { } // Sentinel objects. Overrides equals() to survive process restarts and serialization. - @DeleteForDJVM object UNSTARTED : Step("Unstarted") { override fun equals(other: Any?) = other === UNSTARTED } - @DeleteForDJVM object STARTING : Step("Starting") { override fun equals(other: Any?) = other === STARTING } - @DeleteForDJVM object DONE : Step("Done") { override fun equals(other: Any?) = other === DONE } diff --git a/core/src/main/kotlin/net/corda/core/utilities/SgxSupport.kt b/core/src/main/kotlin/net/corda/core/utilities/SgxSupport.kt index 82f805defc..b4691cb1e0 100644 --- a/core/src/main/kotlin/net/corda/core/utilities/SgxSupport.kt +++ b/core/src/main/kotlin/net/corda/core/utilities/SgxSupport.kt @@ -1,8 +1,5 @@ package net.corda.core.utilities -import net.corda.core.DeleteForDJVM - -@DeleteForDJVM object SgxSupport { @JvmStatic val isInsideEnclave: Boolean by lazy { diff --git a/core/src/main/kotlin/net/corda/core/utilities/Try.kt b/core/src/main/kotlin/net/corda/core/utilities/Try.kt index 98fc5c1764..f492f9af8f 100644 --- a/core/src/main/kotlin/net/corda/core/utilities/Try.kt +++ b/core/src/main/kotlin/net/corda/core/utilities/Try.kt @@ -1,6 +1,5 @@ package net.corda.core.utilities -import net.corda.core.KeepForDJVM import net.corda.core.internal.uncheckedCast import net.corda.core.serialization.CordaSerializable import java.util.function.Consumer @@ -85,7 +84,6 @@ sealed class Try { return this } - @KeepForDJVM data class Success(val value: A) : Try
    () { override val isSuccess: Boolean get() = true override val isFailure: Boolean get() = false @@ -93,7 +91,6 @@ sealed class Try { override fun toString(): String = "Success($value)" } - @KeepForDJVM data class Failure(val exception: Throwable) : Try() { override val isSuccess: Boolean get() = false override val isFailure: Boolean get() = true diff --git a/core/src/main/kotlin/net/corda/core/utilities/UntrustworthyData.kt b/core/src/main/kotlin/net/corda/core/utilities/UntrustworthyData.kt index 94fcb2a4cf..272b5ec200 100644 --- a/core/src/main/kotlin/net/corda/core/utilities/UntrustworthyData.kt +++ b/core/src/main/kotlin/net/corda/core/utilities/UntrustworthyData.kt @@ -1,8 +1,6 @@ -@file:KeepForDJVM package net.corda.core.utilities import co.paralleluniverse.fibers.Suspendable -import net.corda.core.KeepForDJVM import net.corda.core.flows.FlowException import java.io.Serializable @@ -17,13 +15,11 @@ import java.io.Serializable * - Are any objects *reachable* from this object mismatched or not what you expected? * - Is it suspiciously large or small? */ -@KeepForDJVM class UntrustworthyData(@PublishedApi internal val fromUntrustedWorld: T) { @Suspendable @Throws(FlowException::class) fun unwrap(validator: Validator) = validator.validate(fromUntrustedWorld) - @KeepForDJVM @FunctionalInterface interface Validator : Serializable { @Suspendable diff --git a/core/src/main/kotlin/net/corda/core/utilities/UuidGenerator.kt b/core/src/main/kotlin/net/corda/core/utilities/UuidGenerator.kt index 72beb0d624..e851dae583 100644 --- a/core/src/main/kotlin/net/corda/core/utilities/UuidGenerator.kt +++ b/core/src/main/kotlin/net/corda/core/utilities/UuidGenerator.kt @@ -1,9 +1,7 @@ package net.corda.core.utilities -import net.corda.core.DeleteForDJVM import java.util.* -@DeleteForDJVM class UuidGenerator { companion object { diff --git a/detekt-baseline.xml b/detekt-baseline.xml index 046c21e008..a3bfde2f82 100644 --- a/detekt-baseline.xml +++ b/detekt-baseline.xml @@ -467,7 +467,6 @@ ForbiddenComment:WebServerConfig.kt$WebServerConfig$// TODO: remove this once config format is updated ForbiddenComment:WebServerConfig.kt$WebServerConfig$// TODO: replace with credentials supplied by a user ForbiddenComment:WireTransaction.kt$WireTransaction$// TODO: prevent notary field from being set if there are no inputs and no time-window. - ForbiddenComment:WireTransaction.kt$WireTransaction$// TODO: revisit once Deterministic JVM code updated ForbiddenComment:X509Utilities.kt$CertificateType.LEGAL_IDENTITY$// TODO: Identity certs should have tight name constraints on child certificates FunctionNaming:ArtemisRpcTests.kt$ArtemisRpcTests$@Test(timeout=300_000) fun rpc_client_certificate_untrusted_to_server() FunctionNaming:ArtemisRpcTests.kt$ArtemisRpcTests$@Test(timeout=300_000) fun rpc_with_no_ssl_on_client_side_and_ssl_on_server_side() @@ -1170,7 +1169,6 @@ MatchingDeclarationName:ReceiveAllFlowTests.kt$net.corda.coretests.flows.ReceiveAllFlowTests.kt MatchingDeclarationName:ReferenceInputStateTests.kt$net.corda.coretests.transactions.ReferenceInputStateTests.kt MatchingDeclarationName:SSLHelper.kt$net.corda.nodeapi.internal.protonwrapper.netty.SSLHelper.kt - MatchingDeclarationName:SampleData.kt$net.corda.deterministic.verifier.SampleData.kt MatchingDeclarationName:SerializationHelper.kt$net.corda.networkbuilder.serialization.SerializationHelper.kt MatchingDeclarationName:SerializationHelper.kt$net.corda.serialization.internal.amqp.SerializationHelper.kt MatchingDeclarationName:Specification.kt$net.corda.common.configuration.parsing.internal.Specification.kt @@ -1388,7 +1386,6 @@ ThrowsCount:RPCServer.kt$RPCServer$private fun invokeRpc(context: RpcAuthContext, inMethodName: String, arguments: List<Any?>): Try<Any> ThrowsCount:ServicesForResolutionImpl.kt$ServicesForResolutionImpl$// We may need to recursively chase transactions if there are notary changes. fun inner(stateRef: StateRef, forContractClassName: String?): Attachment ThrowsCount:SignedNodeInfo.kt$SignedNodeInfo$// TODO Add root cert param (or TrustAnchor) to make sure all the identities belong to the same root fun verified(): NodeInfo - ThrowsCount:SignedTransaction.kt$SignedTransaction$@DeleteForDJVM private fun resolveAndCheckNetworkParameters(services: ServiceHub) ThrowsCount:SingleThreadedStateMachineManager.kt$SingleThreadedStateMachineManager$private fun getInitiatedFlowFactory(message: InitialSessionMessage): InitiatedFlowFactory<*> ThrowsCount:StringToMethodCallParser.kt$StringToMethodCallParser$ @Throws(UnparseableCallException::class) fun parse(target: T?, command: String): ParsedMethodCall ThrowsCount:StringToMethodCallParser.kt$StringToMethodCallParser$ @Throws(UnparseableCallException::class) fun parseArguments(methodNameHint: String, parameters: List<Pair<String, Type>>, args: String): Array<Any?> @@ -1968,7 +1965,6 @@ WildcardImport:DBTransactionStorage.kt$import javax.persistence.* WildcardImport:DBTransactionStorage.kt$import net.corda.core.serialization.* WildcardImport:DBTransactionStorage.kt$import net.corda.nodeapi.internal.persistence.* - WildcardImport:DeleteForDJVM.kt$import kotlin.annotation.AnnotationTarget.* WildcardImport:DemoBench.kt$import tornadofx.* WildcardImport:DemoBenchNodeInfoFilesCopier.kt$import tornadofx.* WildcardImport:DemoBenchView.kt$import tornadofx.* @@ -2325,7 +2321,6 @@ WildcardImport:SpringDriver.kt$import net.corda.testing.node.internal.* WildcardImport:StartedFlowTransition.kt$import net.corda.node.services.statemachine.* WildcardImport:StatePointerSearchTests.kt$import net.corda.core.contracts.* - WildcardImport:StubOutForDJVM.kt$import kotlin.annotation.AnnotationTarget.* WildcardImport:SubFlow.kt$import net.corda.core.flows.* WildcardImport:SwapIdentitiesFlowTests.kt$import net.corda.testing.core.* WildcardImport:TLSAuthenticationTests.kt$import javax.net.ssl.* diff --git a/detekt-plugins/src/test/kotlin/net/corda/detekt/plugins/rules/TestWithMissingTimeoutTest.kt b/detekt-plugins/src/test/kotlin/net/corda/detekt/plugins/rules/TestWithMissingTimeoutTest.kt index bfeb1ab0ca..34b767b72a 100644 --- a/detekt-plugins/src/test/kotlin/net/corda/detekt/plugins/rules/TestWithMissingTimeoutTest.kt +++ b/detekt-plugins/src/test/kotlin/net/corda/detekt/plugins/rules/TestWithMissingTimeoutTest.kt @@ -20,11 +20,8 @@ class JUnit4Test { """.trimIndent() private val junit5Code = """ - package net.corda.serialization.djvm - import net.corda.core.serialization.internal._contextSerializationEnv import net.corda.core.serialization.serialize - import net.corda.serialization.djvm.SandboxType.KOTLIN import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith @@ -33,7 +30,7 @@ class JUnit4Test { import java.util.function.Function @ExtendWith(LocalSerialization::class) - class DeserializeInstantTest : TestBase(KOTLIN) { + class DeserializeInstantTest { @Test fun `test deserializing instant`() { val instant = Instant.now() diff --git a/deterministic.gradle b/deterministic.gradle deleted file mode 100644 index 751af8bfb2..0000000000 --- a/deterministic.gradle +++ /dev/null @@ -1,36 +0,0 @@ -import static org.gradle.api.JavaVersion.VERSION_1_8 - -/* - * Gradle script plugin: Configure a module such that the Java and Kotlin - * compilers use the deterministic rt.jar instead of the full JDK rt.jar. - */ -apply plugin: 'kotlin' - -evaluationDependsOn(':jdk8u-deterministic') - -def jdk8uDeterministic = project(':jdk8u-deterministic') - -ext { - jdkTask = jdk8uDeterministic.tasks.named('assemble') - deterministic_jdk_home = jdk8uDeterministic.jdk_home - deterministic_rt_jar = jdk8uDeterministic.rt_jar -} - -tasks.withType(AbstractCompile).configureEach { - dependsOn jdkTask - - // This is a bit ugly, but Gradle isn't recognising the KotlinCompile task - // as it does the built-in JavaCompile task. - if (it.class.name.startsWith('org.jetbrains.kotlin.gradle.tasks.KotlinCompile')) { - kotlinOptions { - jdkHome = deterministic_jdk_home - jvmTarget = VERSION_1_8 - } - } -} - -tasks.withType(JavaCompile).configureEach { - options.compilerArgs << '-bootclasspath' << deterministic_rt_jar - sourceCompatibility = VERSION_1_8 - targetCompatibility = VERSION_1_8 -} diff --git a/jdk8u-deterministic/.gitignore b/jdk8u-deterministic/.gitignore deleted file mode 100644 index 3216b4dc3d..0000000000 --- a/jdk8u-deterministic/.gitignore +++ /dev/null @@ -1 +0,0 @@ -jdk/ diff --git a/jdk8u-deterministic/build.gradle b/jdk8u-deterministic/build.gradle deleted file mode 100644 index 80804d15a8..0000000000 --- a/jdk8u-deterministic/build.gradle +++ /dev/null @@ -1,49 +0,0 @@ -repositories { - maven { - url "$publicArtifactURL/corda-dependencies" - } -} - -ext { - jdk_home = "$projectDir/jdk".toString() - rt_jar = "$jdk_home/jre/lib/rt.jar".toString() -} - -configurations { - jdk -} - -dependencies { - jdk "net.corda:deterministic-rt:$deterministic_rt_version:api" -} - -def copyJdk = tasks.register('copyJdk', Copy) { - outputs.dir jdk_home - - from(configurations.jdk) { - rename 'deterministic-rt-(.*).jar', 'rt.jar' - } - into "$jdk_home/jre/lib" - - doLast { - def eol = System.lineSeparator() - file("$jdk_home/release").write "JAVA_VERSION=\"1.8.0_172\"$eol" - mkdir "$jdk_home/bin" - file("$jdk_home/bin/javac").with { - write "#!/bin/sh\necho \"javac 1.8.0_172\"\n" - setExecutable true, false - return - } - } -} - -tasks.named('assemble') { - dependsOn copyJdk -} -tasks.named('jar', Jar) { - enabled = false -} - -artifacts { - jdk file: file(rt_jar), type: 'jar', builtBy: copyJdk -} diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/kryo/Kryo.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/kryo/Kryo.kt index 6d5b31d08e..166a10fdf3 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/kryo/Kryo.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/kryo/Kryo.kt @@ -11,7 +11,6 @@ import com.esotericsoftware.kryo.io.Output import com.esotericsoftware.kryo.serializers.CompatibleFieldSerializer import com.esotericsoftware.kryo.serializers.FieldSerializer import com.esotericsoftware.kryo.util.MapReferenceResolver -import net.corda.core.DeleteForDJVM import net.corda.core.contracts.PrivacySalt import net.corda.core.crypto.Crypto import net.corda.core.crypto.DigestService @@ -474,7 +473,6 @@ fun Kryo.serializationContext(): SerializeAsTokenContext? = context.get(serializ * unmodifiable collection to [java.lang.Throwable.suppressedExceptions] which will fail some sentinel identity checks * e.g. in [java.lang.Throwable.addSuppressed] */ -@DeleteForDJVM @ThreadSafe class ThrowableSerializer(kryo: Kryo, type: Class) : Serializer(false, true) { diff --git a/node/build.gradle b/node/build.gradle index a766182f87..81418360f7 100644 --- a/node/build.gradle +++ b/node/build.gradle @@ -20,11 +20,6 @@ ext { jolokia_version = constants.getProperty('jolokiaAgentVersion') } -evaluationDependsOn(':core-deterministic') -evaluationDependsOn(':serialization-deterministic') -evaluationDependsOn(':serialization-djvm:deserializers') -evaluationDependsOn(':node:djvm') - //noinspection GroovyAssignabilityCheck configurations { integrationTestCompile.extendsFrom testCompile @@ -32,9 +27,6 @@ configurations { slowIntegrationTestCompile.extendsFrom testCompile slowIntegrationTestRuntimeOnly.extendsFrom testRuntimeOnly - - jdkRt - deterministic } sourceSets { @@ -150,19 +142,6 @@ dependencies { // TypeSafe Config: for simple and human friendly config files. compile "com.typesafe:config:$typesafe_config_version" - // Sandbox for deterministic contract verification - compile "net.corda.djvm:corda-djvm:$djvm_version" - compile project(':serialization-djvm') - compile(project(':node:djvm')) { - transitive = false - } - jdkRt "net.corda:deterministic-rt:$deterministic_rt_version" - deterministic project(path: ':core-deterministic', configuration: 'deterministicArtifacts') - deterministic project(path: ':serialization-deterministic', configuration: 'deterministicArtifacts') - deterministic project(':serialization-djvm:deserializers') - deterministic project(':node:djvm') - deterministic "org.slf4j:slf4j-nop:$slf4j_version" - testImplementation "org.junit.jupiter:junit-jupiter-api:${junit_jupiter_version}" testImplementation "junit:junit:$junit_version" @@ -265,8 +244,6 @@ tasks.withType(Test).configureEach { if (JavaVersion.current() == JavaVersion.VERSION_11) { jvmArgs '-Djdk.attach.allowAttachSelf=true' } - systemProperty 'deterministic-rt.path', configurations.jdkRt.asPath - systemProperty 'deterministic-sources.path', configurations.deterministic.asPath } tasks.register('integrationTest', Test) { @@ -284,7 +261,6 @@ tasks.register('slowIntegrationTest', Test) { // quasar exclusions upon agent code instrumentation at run-time quasar { excludeClassLoaders.addAll( - 'net.corda.djvm.**', 'net.corda.core.serialization.internal.**' ) excludePackages.addAll( @@ -295,12 +271,10 @@ quasar { "com.google.**", "com.lmax.**", "com.zaxxer.**", - "djvm**", "net.bytebuddy**", "io.github.classgraph**", "io.netty*", "liquibase**", - "net.corda.djvm**", "net.i2p.crypto.**", "nonapi.io.github.classgraph.**", "org.apiguardian.**", @@ -319,15 +293,6 @@ quasar { jar { baseName 'corda-node' - exclude 'sandbox/java/**' - exclude 'sandbox/org/**' - exclude 'sandbox/net/corda/core/crypto/SecureHash.class' - exclude 'sandbox/net/corda/core/crypto/SignatureScheme.class' - exclude 'sandbox/net/corda/core/crypto/TransactionSignature.class' - manifest { - attributes('Corda-Deterministic-Runtime': configurations.jdkRt.singleFile.name) - attributes('Corda-Deterministic-Classpath': configurations.deterministic.collect { it.name }.join(' ')) - } } publish { diff --git a/node/capsule/build.gradle b/node/capsule/build.gradle index 2eb546be0d..8026e23ab9 100644 --- a/node/capsule/build.gradle +++ b/node/capsule/build.gradle @@ -40,8 +40,6 @@ def nodeProject = project(':node') task buildCordaJAR(type: FatCapsule, dependsOn: [ nodeProject.tasks.named('jar'), - project(':core-deterministic').tasks.named('assemble'), - project(':serialization-deterministic').tasks.named('assemble') ]) { applicationClass 'net.corda.node.Corda' archiveBaseName = 'corda' @@ -58,46 +56,18 @@ task buildCordaJAR(type: FatCapsule, dependsOn: [ from configurations.capsuleRuntime.files.collect { zipTree(it) } with jar - // The DJVM will share most of its dependencies with the node, but any extra ones that it needs - // are listed in the node's "deterministic" configuration and copied into a djvm subdirectory. - // - // Gradle may not resolve exactly the same transitive dependencies for both the runtimeClasspath - // and deterministic configurations - specifically, the artifacts' version numbers may differ slightly. - // And so we map the files by the resolved ModuleIdentifier objects instead, which just contain an - // artifact's group and name. - def cordaResolved = nodeProject.configurations['runtimeClasspath'].resolvedConfiguration.resolvedArtifacts.collectEntries { - [ (it.moduleVersion.id.module):it.file ] - } - def deterministicResolved = nodeProject.configurations['deterministic'].resolvedConfiguration.resolvedArtifacts.collectEntries { - [ (it.moduleVersion.id.module):it.file ] - } - def resolvedDifferences = deterministicResolved.keySet() - cordaResolved.keySet() - - cordaResolved.keySet().retainAll(deterministicResolved.keySet() - resolvedDifferences) - deterministicResolved.keySet().retainAll(resolvedDifferences) - manifest { - // These are the dependencies that the deterministic Corda libraries share with Corda. - attributes('Corda-DJVM-Dependencies': cordaResolved.values().collect { it.name }.join(' ')) - if (JavaVersion.current() == JavaVersion.VERSION_11) { attributes('Add-Opens': 'java.management/com.sun.jmx.mbeanserver java.base/java.lang') } - - } - - into('djvm') { - from nodeProject.configurations['jdkRt'].singleFile - from deterministicResolved.values() - fileMode = 0444 } capsuleManifest { applicationVersion = corda_release_version applicationId = "net.corda.node.Corda" // See experimental/quasar-hook/README.md for how to generate. - def quasarExcludeExpression = "x(antlr**;bftsmart**;co.paralleluniverse**;com.codahale**;com.esotericsoftware**;com.fasterxml**;com.google**;com.ibm**;com.intellij**;com.jcabi**;com.nhaarman**;com.opengamma**;com.typesafe**;com.zaxxer**;de.javakaffee**;groovy**;groovyjarjarantlr**;groovyjarjarasm**;io.atomix**;io.github**;io.netty**;jdk**;kotlin**;net.corda.djvm**;djvm**;net.bytebuddy**;net.i2p**;org.apache**;org.bouncycastle**;org.codehaus**;org.crsh**;org.dom4j**;org.fusesource**;org.h2**;org.hibernate**;org.jboss**;org.jcp**;org.joda**;org.objectweb**;org.objenesis**;org.slf4j**;org.w3c**;org.xml**;org.yaml**;reflectasm**;rx**;org.jolokia**;com.lmax**;picocli**;liquibase**;com.github.benmanes**;org.json**;org.postgresql**;nonapi.io.github.classgraph**;io.opentelemetry**)" - def quasarClassLoaderExclusion = "l(net.corda.djvm.**;net.corda.core.serialization.internal.**)" + def quasarExcludeExpression = "x(antlr**;bftsmart**;co.paralleluniverse**;com.codahale**;com.esotericsoftware**;com.fasterxml**;com.google**;com.ibm**;com.intellij**;com.jcabi**;com.nhaarman**;com.opengamma**;com.typesafe**;com.zaxxer**;de.javakaffee**;groovy**;groovyjarjarantlr**;groovyjarjarasm**;io.atomix**;io.github**;io.netty**;jdk**;kotlin**;net.bytebuddy**;net.i2p**;org.apache**;org.bouncycastle**;org.codehaus**;org.crsh**;org.dom4j**;org.fusesource**;org.h2**;org.hibernate**;org.jboss**;org.jcp**;org.joda**;org.objectweb**;org.objenesis**;org.slf4j**;org.w3c**;org.xml**;org.yaml**;reflectasm**;rx**;org.jolokia**;com.lmax**;picocli**;liquibase**;com.github.benmanes**;org.json**;org.postgresql**;nonapi.io.github.classgraph**;io.opentelemetry**)" + def quasarClassLoaderExclusion = "l(net.corda.core.serialization.internal.**)" javaAgents = quasar_classifier ? ["quasar-core-${quasar_version}-${quasar_classifier}.jar=${quasarExcludeExpression}${quasarClassLoaderExclusion}"] : ["quasar-core-${quasar_version}.jar=${quasarExcludeExpression}${quasarClassLoaderExclusion}"] systemProperties['visualvm.display.name'] = 'Corda' if (JavaVersion.current() == JavaVersion.VERSION_1_8) { diff --git a/node/capsule/src/main/java/CordaCaplet.java b/node/capsule/src/main/java/CordaCaplet.java index a2c73e0dd3..25e1b26d58 100644 --- a/node/capsule/src/main/java/CordaCaplet.java +++ b/node/capsule/src/main/java/CordaCaplet.java @@ -22,8 +22,6 @@ import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; import static java.util.stream.Collectors.toMap; public class CordaCaplet extends Capsule { - private static final String DJVM_DIR ="djvm"; - private Config nodeConfig = null; private String baseDir = null; @@ -90,76 +88,10 @@ public class CordaCaplet extends Capsule { return null; } - private void installDJVM() { - Path djvmDir = Paths.get(baseDir, DJVM_DIR); - if (!djvmDir.toFile().mkdir() && !Files.isDirectory(djvmDir)) { - log(LOG_VERBOSE, "DJVM directory could not be created"); - } else { - try { - Path sourceDir = appDir().resolve(DJVM_DIR); - if (Files.isDirectory(sourceDir)) { - installCordaDependenciesForDJVM(sourceDir, djvmDir); - installTransitiveDependenciesForDJVM(appDir(), djvmDir); - } - } catch (IOException e) { - log(LOG_VERBOSE, "Failed to populate directory " + djvmDir.toAbsolutePath()); - log(LOG_VERBOSE, e); - } - } - } - - private void installCordaDependenciesForDJVM(Path sourceDir, Path targetDir) throws IOException { - try (DirectoryStream directory = Files.newDirectoryStream(sourceDir, file -> Files.isRegularFile(file))) { - for (Path sourceFile : directory) { - Path targetFile = targetDir.resolve(sourceFile.getFileName()); - installFile(sourceFile, targetFile); - } - } - } - - private void installTransitiveDependenciesForDJVM(Path sourceDir, Path targetDir) throws IOException { - Manifest manifest = getManifest(); - String[] transitives = manifest.getMainAttributes().getValue("Corda-DJVM-Dependencies").split("\\s++", 0); - for (String transitive : transitives) { - Path source = sourceDir.resolve(transitive); - if (Files.isRegularFile(source)) { - installFile(source, targetDir.resolve(transitive)); - } - } - } - - private Manifest getManifest() throws IOException { - URL capsule = getClass().getProtectionDomain().getCodeSource().getLocation(); - try (JarInputStream jar = new JarInputStream(capsule.openStream())) { - return jar.getManifest(); - } - } - - private void installFile(Path source, Path target) { - try { - // Forcibly reinstall this dependency. - Files.deleteIfExists(target); - Files.createSymbolicLink(target, source); - } catch (UnsupportedOperationException | IOException e) { - copyFile(source, target); - } - } - - private void copyFile(Path source, Path target) { - try { - Files.copy(source, target, REPLACE_EXISTING); - } catch (IOException e) { - //noinspection ResultOfMethodCallIgnored - target.toFile().delete(); - log(LOG_VERBOSE, e); - } - } - @Override protected ProcessBuilder prelaunch(List jvmArgs, List args) { checkJavaVersion(); nodeConfig = parseConfigFile(args); - installDJVM(); return super.prelaunch(jvmArgs, args); } diff --git a/node/djvm/build.gradle b/node/djvm/build.gradle deleted file mode 100644 index 9c39b404d6..0000000000 --- a/node/djvm/build.gradle +++ /dev/null @@ -1,25 +0,0 @@ -apply from: '../../deterministic.gradle' -apply plugin: 'com.jfrog.artifactory' -apply plugin: 'net.corda.plugins.publish-utils' -apply plugin: 'java-library' - -description 'Internal Corda Node modules for deterministic contract verification.' - -dependencies { - api 'org.jetbrains.kotlin:kotlin-stdlib-jdk8' - api project(path: ':core-deterministic', configuration: 'deterministicArtifacts') - api project(path: ':serialization-deterministic', configuration: 'deterministicArtifacts') -} - -jar { - archiveBaseName = 'corda-node-djvm' - archiveClassifier = '' - manifest { - attributes('Automatic-Module-Name': 'net.corda.node.djvm') - attributes('Sealed': true) - } -} - -publish { - name jar.archiveBaseName.get() -} diff --git a/node/djvm/src/main/kotlin/net/corda/node/djvm/AttachmentBuilder.kt b/node/djvm/src/main/kotlin/net/corda/node/djvm/AttachmentBuilder.kt deleted file mode 100644 index 561b5bcd76..0000000000 --- a/node/djvm/src/main/kotlin/net/corda/node/djvm/AttachmentBuilder.kt +++ /dev/null @@ -1,88 +0,0 @@ -@file:JvmName("AttachmentConstants") -package net.corda.node.djvm - -import net.corda.core.contracts.Attachment -import net.corda.core.contracts.BrokenAttachmentException -import net.corda.core.contracts.ContractAttachment -import net.corda.core.crypto.SecureHash -import net.corda.core.identity.Party -import java.io.InputStream -import java.security.PublicKey -import java.util.Collections.unmodifiableList -import java.util.function.Function - -private const val SIGNER_KEYS_IDX = 0 -private const val SIZE_IDX = 1 -private const val ID_IDX = 2 -private const val ATTACHMENT_IDX = 3 -private const val STREAMER_IDX = 4 - -private const val CONTRACT_IDX = 5 -private const val ADDITIONAL_CONTRACT_IDX = 6 -private const val UPLOADER_IDX = 7 -private const val CONTRACT_SIGNER_KEYS_IDX = 8 -private const val VERSION_IDX = 9 - -class AttachmentBuilder : Function?, List?> { - private val attachments = mutableListOf() - - private fun unmodifiable(list: List): List { - return if (list.isEmpty()) { - emptyList() - } else { - unmodifiableList(list) - } - } - - override fun apply(inputs: Array?): List? { - @Suppress("unchecked_cast") - return if (inputs == null) { - unmodifiable(attachments) - } else { - var attachment: Attachment = SandboxAttachment( - signerKeys = inputs[SIGNER_KEYS_IDX] as List, - size = inputs[SIZE_IDX] as Int, - id = inputs[ID_IDX] as SecureHash, - attachment = inputs[ATTACHMENT_IDX], - streamer = inputs[STREAMER_IDX] as Function - ) - - if (inputs.size > VERSION_IDX) { - attachment = ContractAttachment.create( - attachment = attachment, - contract = inputs[CONTRACT_IDX] as String, - additionalContracts = (inputs[ADDITIONAL_CONTRACT_IDX] as Array).toSet(), - uploader = inputs[UPLOADER_IDX] as? String, - signerKeys = inputs[CONTRACT_SIGNER_KEYS_IDX] as List, - version = inputs[VERSION_IDX] as Int - ) - } - - attachments.add(attachment) - null - } - } -} - -/** - * This represents an [Attachment] from within the sandbox. - */ -private class SandboxAttachment( - override val signerKeys: List, - override val size: Int, - override val id: SecureHash, - private val attachment: Any, - private val streamer: Function -) : Attachment { - @Suppress("OverridingDeprecatedMember") - override val signers: List = emptyList() - - @Suppress("TooGenericExceptionCaught") - override fun open(): InputStream { - return try { - streamer.apply(attachment) - } catch (e: Exception) { - throw BrokenAttachmentException(id, e.message, e) - } - } -} diff --git a/node/djvm/src/main/kotlin/net/corda/node/djvm/CommandBuilder.kt b/node/djvm/src/main/kotlin/net/corda/node/djvm/CommandBuilder.kt deleted file mode 100644 index 247fef3ec6..0000000000 --- a/node/djvm/src/main/kotlin/net/corda/node/djvm/CommandBuilder.kt +++ /dev/null @@ -1,48 +0,0 @@ -package net.corda.node.djvm - -import net.corda.core.contracts.CommandData -import net.corda.core.contracts.CommandWithParties -import net.corda.core.internal.lazyMapped -import java.security.PublicKey -import java.util.function.Function -import java.util.function.Supplier - -class CommandBuilder : Function, Supplier>>> { - @Suppress("unchecked_cast") - override fun apply(inputs: Array): Supplier>> { - val signersProvider = inputs[0] as? Supplier>> ?: Supplier(::emptyList) - val commandsDataProvider = inputs[1] as? Supplier> ?: Supplier(::emptyList) - val partialMerkleLeafIndices = inputs[2] as? IntArray - - /** - * This logic has been lovingly reproduced from [net.corda.core.internal.deserialiseCommands]. - */ - return Supplier { - val signers = signersProvider.get() - val commandsData = commandsDataProvider.get() - - if (partialMerkleLeafIndices != null) { - check(commandsData.size <= signers.size) { - "Invalid Transaction. Fewer Signers (${signers.size}) than CommandData (${commandsData.size}) objects" - } - if (partialMerkleLeafIndices.isNotEmpty()) { - check(partialMerkleLeafIndices.max()!! < signers.size) { - "Invalid Transaction. A command with no corresponding signer detected" - } - } - commandsData.lazyMapped { commandData, index -> - // Deprecated signingParties property not supported. - CommandWithParties(signers[partialMerkleLeafIndices[index]], emptyList(), commandData) - } - } else { - check(commandsData.size == signers.size) { - "Invalid Transaction. Sizes of CommandData (${commandsData.size}) and Signers (${signers.size}) do not match" - } - commandsData.lazyMapped { commandData, index -> - // Deprecated signingParties property not supported. - CommandWithParties(signers[index], emptyList(), commandData) - } - } - } - } -} diff --git a/node/djvm/src/main/kotlin/net/corda/node/djvm/ComponentBuilder.kt b/node/djvm/src/main/kotlin/net/corda/node/djvm/ComponentBuilder.kt deleted file mode 100644 index f0e2e476aa..0000000000 --- a/node/djvm/src/main/kotlin/net/corda/node/djvm/ComponentBuilder.kt +++ /dev/null @@ -1,27 +0,0 @@ -package net.corda.node.djvm - -import net.corda.core.contracts.ComponentGroupEnum -import net.corda.core.internal.TransactionDeserialisationException -import net.corda.core.internal.lazyMapped -import net.corda.core.utilities.OpaqueBytes -import java.util.function.Function -import java.util.function.Supplier - -class ComponentBuilder : Function, Supplier>> { - @Suppress("unchecked_cast", "TooGenericExceptionCaught") - override fun apply(inputs: Array): Supplier> { - val deserializer = inputs[0] as Function - val groupType = inputs[1] as ComponentGroupEnum - val components = (inputs[2] as Array).map(::OpaqueBytes) - - return Supplier { - components.lazyMapped { component, index -> - try { - deserializer.apply(component.bytes) - } catch (e: Exception) { - throw TransactionDeserialisationException(groupType, index, e) - } - } - } - } -} diff --git a/node/djvm/src/main/kotlin/net/corda/node/djvm/LtxSupplierFactory.kt b/node/djvm/src/main/kotlin/net/corda/node/djvm/LtxSupplierFactory.kt deleted file mode 100644 index fd982d610a..0000000000 --- a/node/djvm/src/main/kotlin/net/corda/node/djvm/LtxSupplierFactory.kt +++ /dev/null @@ -1,73 +0,0 @@ -@file:JvmName("LtxTools") -package net.corda.node.djvm - -import net.corda.core.contracts.Attachment -import net.corda.core.contracts.CommandData -import net.corda.core.contracts.CommandWithParties -import net.corda.core.contracts.ContractState -import net.corda.core.contracts.PrivacySalt -import net.corda.core.contracts.StateAndRef -import net.corda.core.contracts.StateRef -import net.corda.core.contracts.TimeWindow -import net.corda.core.contracts.TransactionState -import net.corda.core.crypto.DigestService -import net.corda.core.crypto.SecureHash -import net.corda.core.identity.Party -import net.corda.core.node.NetworkParameters -import net.corda.core.transactions.LedgerTransaction -import java.util.function.Function -import java.util.function.Supplier - -private const val TX_INPUTS = 0 -private const val TX_OUTPUTS = 1 -private const val TX_COMMANDS = 2 -private const val TX_ATTACHMENTS = 3 -private const val TX_ID = 4 -private const val TX_NOTARY = 5 -private const val TX_TIME_WINDOW = 6 -private const val TX_PRIVACY_SALT = 7 -private const val TX_NETWORK_PARAMETERS = 8 -private const val TX_REFERENCES = 9 -private const val TX_DIGEST_SERVICE = 10 - -class LtxSupplierFactory : Function, Supplier> { - @Suppress("unchecked_cast") - override fun apply(txArgs: Array): Supplier { - val inputProvider = (txArgs[TX_INPUTS] as Function>>) - .andThen(Function(Array>::toContractStatesAndRef)) - .toSupplier() - val outputProvider = txArgs[TX_OUTPUTS] as? Supplier>> ?: Supplier(::emptyList) - val commandsProvider = txArgs[TX_COMMANDS] as Supplier>> - val referencesProvider = (txArgs[TX_REFERENCES] as Function>>) - .andThen(Function(Array>::toContractStatesAndRef)) - .toSupplier() - val networkParameters = (txArgs[TX_NETWORK_PARAMETERS] as? NetworkParameters)?.toImmutable() - return Supplier { - LedgerTransaction.createForContractVerify( - inputs = inputProvider.get(), - outputs = outputProvider.get(), - commands = commandsProvider.get(), - attachments = txArgs[TX_ATTACHMENTS] as? List ?: emptyList(), - id = txArgs[TX_ID] as SecureHash, - notary = txArgs[TX_NOTARY] as? Party, - timeWindow = txArgs[TX_TIME_WINDOW] as? TimeWindow, - privacySalt = txArgs[TX_PRIVACY_SALT] as PrivacySalt, - networkParameters = networkParameters, - references = referencesProvider.get(), - digestService = txArgs[TX_DIGEST_SERVICE] as DigestService - ) - } - } -} - -private fun Function.toSupplier(): Supplier { - return Supplier { apply(null) } -} - -private fun Array>.toContractStatesAndRef(): List> { - return map(Array::toStateAndRef) -} - -private fun Array<*>.toStateAndRef(): StateAndRef { - return StateAndRef(this[0] as TransactionState<*>, this[1] as StateRef) -} diff --git a/node/djvm/src/main/resources/META-INF/DJVM-preload b/node/djvm/src/main/resources/META-INF/DJVM-preload deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/node/src/integration-test/kotlin/net/corda/contracts/djvm/attachment/SandboxAttachmentContract.kt b/node/src/integration-test/kotlin/net/corda/contracts/djvm/attachment/SandboxAttachmentContract.kt deleted file mode 100644 index 4e96264a3d..0000000000 --- a/node/src/integration-test/kotlin/net/corda/contracts/djvm/attachment/SandboxAttachmentContract.kt +++ /dev/null @@ -1,38 +0,0 @@ -package net.corda.contracts.djvm.attachment - -import net.corda.core.contracts.CommandData -import net.corda.core.contracts.Contract -import net.corda.core.contracts.ContractState -import net.corda.core.identity.AbstractParty -import net.corda.core.transactions.LedgerTransaction -import java.io.ByteArrayOutputStream - -class SandboxAttachmentContract : Contract { - override fun verify(tx: LedgerTransaction) { - val attachments = tx.attachments - require(attachments.isNotEmpty()) { "Attachments are missing for TX=${tx.id}" } - - require(attachments.size == 1) { "Did not expect to find ${attachments.size} attachments for TX=${tx.id}" } - val attachment = attachments[0] - require(attachment.size > 0) { "Attachment ${attachment.id} has no contents for TX=${tx.id}" } - - val keyCount = attachment.signerKeys.size - require(keyCount == 1) { "Did not expect to find $keyCount signing keys for attachment ${attachment.id}, TX=${tx.id}" } - - tx.commandsOfType().forEach { extract -> - val fileName = extract.value.fileName - val contents = ByteArrayOutputStream().use { - attachment.extractFile(fileName, it) - it - }.toByteArray() - require(contents.isNotEmpty()) { "File $fileName has no contents for TX=${tx.id}" } - } - } - - @Suppress("CanBeParameter", "MemberVisibilityCanBePrivate") - class State(val issuer: AbstractParty) : ContractState { - override val participants: List = listOf(issuer) - } - - class ExtractFile(val fileName: String) : CommandData -} \ No newline at end of file diff --git a/node/src/integration-test/kotlin/net/corda/contracts/djvm/broken/NonDeterministicContract.kt b/node/src/integration-test/kotlin/net/corda/contracts/djvm/broken/NonDeterministicContract.kt deleted file mode 100644 index 9d7ffacf41..0000000000 --- a/node/src/integration-test/kotlin/net/corda/contracts/djvm/broken/NonDeterministicContract.kt +++ /dev/null @@ -1,50 +0,0 @@ -package net.corda.contracts.djvm.broken - -import net.corda.core.contracts.Contract -import net.corda.core.contracts.ContractState -import net.corda.core.contracts.TypeOnlyCommandData -import net.corda.core.identity.AbstractParty -import net.corda.core.transactions.LedgerTransaction -import java.time.Instant -import java.util.* - -class NonDeterministicContract : Contract { - override fun verify(tx: LedgerTransaction) { - when { - tx.commandsOfType().isNotEmpty() -> verifyInstantNow() - tx.commandsOfType().isNotEmpty() -> verifyCurrentTimeMillis() - tx.commandsOfType().isNotEmpty() -> verifyNanoTime() - tx.commandsOfType().isNotEmpty() -> UUID.randomUUID() - tx.commandsOfType().isNotEmpty() -> verifyNoReflection() - else -> {} - } - } - - private fun verifyInstantNow() { - Instant.now() - } - - private fun verifyCurrentTimeMillis() { - System.currentTimeMillis() - } - - private fun verifyNanoTime() { - System.nanoTime() - } - - private fun verifyNoReflection() { - Date::class.java.getDeclaredConstructor().newInstance() - } - - @Suppress("CanBeParameter", "MemberVisibilityCanBePrivate") - class State(val issuer: AbstractParty) : ContractState { - override val participants: List = listOf(issuer) - } - - class InstantNow : TypeOnlyCommandData() - class CurrentTimeMillis : TypeOnlyCommandData() - class NanoTime : TypeOnlyCommandData() - class RandomUUID : TypeOnlyCommandData() - class WithReflection : TypeOnlyCommandData() - class NoOperation : TypeOnlyCommandData() -} diff --git a/node/src/integration-test/kotlin/net/corda/contracts/djvm/crypto/DeterministicCryptoContract.kt b/node/src/integration-test/kotlin/net/corda/contracts/djvm/crypto/DeterministicCryptoContract.kt deleted file mode 100644 index d861a74d28..0000000000 --- a/node/src/integration-test/kotlin/net/corda/contracts/djvm/crypto/DeterministicCryptoContract.kt +++ /dev/null @@ -1,39 +0,0 @@ -package net.corda.contracts.djvm.crypto - -import net.corda.core.contracts.CommandData -import net.corda.core.contracts.Contract -import net.corda.core.contracts.ContractState -import net.corda.core.crypto.Crypto -import net.corda.core.identity.AbstractParty -import net.corda.core.transactions.LedgerTransaction -import net.corda.core.utilities.OpaqueBytes -import java.security.PublicKey - -class DeterministicCryptoContract : Contract { - override fun verify(tx: LedgerTransaction) { - val cryptoData = tx.outputsOfType() - val validators = tx.commandsOfType() - - val isValid = validators.all { validate -> - with (validate.value) { - cryptoData.all { crypto -> - Crypto.doVerify(schemeCodeName, publicKey, crypto.signature.bytes, crypto.original.bytes) - } - } - } - - require(cryptoData.isNotEmpty() && validators.isNotEmpty() && isValid) { - "Failed to validate signatures in command data" - } - } - - @Suppress("CanBeParameter", "MemberVisibilityCanBePrivate") - class CryptoState(val owner: AbstractParty, val original: OpaqueBytes, val signature: OpaqueBytes) : ContractState { - override val participants: List = listOf(owner) - } - - class Validate( - val schemeCodeName: String, - val publicKey: PublicKey - ) : CommandData -} \ No newline at end of file diff --git a/node/src/integration-test/kotlin/net/corda/contracts/djvm/whitelist/DeterministicWhitelistContract.kt b/node/src/integration-test/kotlin/net/corda/contracts/djvm/whitelist/DeterministicWhitelistContract.kt deleted file mode 100644 index 433165e986..0000000000 --- a/node/src/integration-test/kotlin/net/corda/contracts/djvm/whitelist/DeterministicWhitelistContract.kt +++ /dev/null @@ -1,38 +0,0 @@ -package net.corda.contracts.djvm.whitelist - -import net.corda.core.contracts.CommandData -import net.corda.core.contracts.Contract -import net.corda.core.contracts.ContractState -import net.corda.core.identity.AbstractParty -import net.corda.core.transactions.LedgerTransaction - -class DeterministicWhitelistContract : Contract { - companion object { - const val MAX_VALUE = 2000L - } - - override fun verify(tx: LedgerTransaction) { - val states = tx.outputsOfType() - require(states.isNotEmpty()) { - "Requires at least one custom data state" - } - - states.forEach { - require(it.whitelistData in WhitelistData(0)..WhitelistData(MAX_VALUE)) { - "WhitelistData $it exceeds maximum value!" - } - } - } - - @Suppress("CanBeParameter", "MemberVisibilityCanBePrivate") - class State(val owner: AbstractParty, val whitelistData: WhitelistData) : ContractState { - override val participants: List = listOf(owner) - - @Override - override fun toString(): String { - return whitelistData.toString() - } - } - - class Operate : CommandData -} \ No newline at end of file diff --git a/node/src/integration-test/kotlin/net/corda/contracts/djvm/whitelist/WhitelistData.kt b/node/src/integration-test/kotlin/net/corda/contracts/djvm/whitelist/WhitelistData.kt deleted file mode 100644 index c0d5f00f89..0000000000 --- a/node/src/integration-test/kotlin/net/corda/contracts/djvm/whitelist/WhitelistData.kt +++ /dev/null @@ -1,15 +0,0 @@ -package net.corda.contracts.djvm.whitelist - -import net.corda.core.serialization.SerializationWhitelist - -data class WhitelistData(val value: Long) : Comparable { - override fun compareTo(other: WhitelistData): Int { - return value.compareTo(other.value) - } - - override fun toString(): String = "$value things" -} - -class Whitelist : SerializationWhitelist { - override val whitelist = listOf(WhitelistData::class.java) -} diff --git a/node/src/integration-test/kotlin/net/corda/flows/djvm/attachment/SandboxAttachmentFlow.kt b/node/src/integration-test/kotlin/net/corda/flows/djvm/attachment/SandboxAttachmentFlow.kt deleted file mode 100644 index ae277dfda2..0000000000 --- a/node/src/integration-test/kotlin/net/corda/flows/djvm/attachment/SandboxAttachmentFlow.kt +++ /dev/null @@ -1,25 +0,0 @@ -package net.corda.flows.djvm.attachment - -import co.paralleluniverse.fibers.Suspendable -import net.corda.contracts.djvm.attachment.SandboxAttachmentContract -import net.corda.core.contracts.Command -import net.corda.core.contracts.CommandData -import net.corda.core.crypto.SecureHash -import net.corda.core.flows.FlowLogic -import net.corda.core.flows.StartableByRPC -import net.corda.core.transactions.TransactionBuilder - -@StartableByRPC -class SandboxAttachmentFlow(private val command: CommandData) : FlowLogic() { - @Suspendable - override fun call(): SecureHash { - val notary = serviceHub.networkMapCache.notaryIdentities[0] - val stx = serviceHub.signInitialTransaction( - TransactionBuilder(notary) - .addOutputState(SandboxAttachmentContract.State(ourIdentity)) - .addCommand(Command(command, ourIdentity.owningKey)) - ) - stx.verify(serviceHub, checkSufficientSignatures = false) - return stx.id - } -} diff --git a/node/src/integration-test/kotlin/net/corda/flows/djvm/broken/NonDeterministicFlow.kt b/node/src/integration-test/kotlin/net/corda/flows/djvm/broken/NonDeterministicFlow.kt deleted file mode 100644 index 076f17bdff..0000000000 --- a/node/src/integration-test/kotlin/net/corda/flows/djvm/broken/NonDeterministicFlow.kt +++ /dev/null @@ -1,25 +0,0 @@ -package net.corda.flows.djvm.broken - -import co.paralleluniverse.fibers.Suspendable -import net.corda.contracts.djvm.broken.NonDeterministicContract -import net.corda.core.contracts.Command -import net.corda.core.contracts.TypeOnlyCommandData -import net.corda.core.crypto.SecureHash -import net.corda.core.flows.FlowLogic -import net.corda.core.flows.StartableByRPC -import net.corda.core.transactions.TransactionBuilder - -@StartableByRPC -class NonDeterministicFlow(private val trouble: TypeOnlyCommandData) : FlowLogic() { - @Suspendable - override fun call(): SecureHash { - val notary = serviceHub.networkMapCache.notaryIdentities[0] - val stx = serviceHub.signInitialTransaction( - TransactionBuilder(notary) - .addOutputState(NonDeterministicContract.State(ourIdentity)) - .addCommand(Command(trouble, ourIdentity.owningKey)) - ) - stx.verify(serviceHub, checkSufficientSignatures = false) - return stx.id - } -} diff --git a/node/src/integration-test/kotlin/net/corda/flows/djvm/crypto/DeterministicCryptoFlow.kt b/node/src/integration-test/kotlin/net/corda/flows/djvm/crypto/DeterministicCryptoFlow.kt deleted file mode 100644 index 65e0065eb1..0000000000 --- a/node/src/integration-test/kotlin/net/corda/flows/djvm/crypto/DeterministicCryptoFlow.kt +++ /dev/null @@ -1,30 +0,0 @@ -package net.corda.flows.djvm.crypto - -import co.paralleluniverse.fibers.Suspendable -import net.corda.contracts.djvm.crypto.DeterministicCryptoContract -import net.corda.core.contracts.Command -import net.corda.core.contracts.CommandData -import net.corda.core.crypto.SecureHash -import net.corda.core.flows.FlowLogic -import net.corda.core.flows.StartableByRPC -import net.corda.core.transactions.TransactionBuilder -import net.corda.core.utilities.OpaqueBytes - -@StartableByRPC -class DeterministicCryptoFlow( - private val command: CommandData, - private val original: OpaqueBytes, - private val signature: OpaqueBytes -) : FlowLogic() { - @Suspendable - override fun call(): SecureHash { - val notary = serviceHub.networkMapCache.notaryIdentities[0] - val stx = serviceHub.signInitialTransaction( - TransactionBuilder(notary) - .addOutputState(DeterministicCryptoContract.CryptoState(ourIdentity, original, signature)) - .addCommand(Command(command, ourIdentity.owningKey)) - ) - stx.verify(serviceHub, checkSufficientSignatures = false) - return stx.id - } -} diff --git a/node/src/integration-test/kotlin/net/corda/flows/djvm/whitelist/DeterministicWhitelistFlow.kt b/node/src/integration-test/kotlin/net/corda/flows/djvm/whitelist/DeterministicWhitelistFlow.kt deleted file mode 100644 index fe8824e03c..0000000000 --- a/node/src/integration-test/kotlin/net/corda/flows/djvm/whitelist/DeterministicWhitelistFlow.kt +++ /dev/null @@ -1,26 +0,0 @@ -package net.corda.flows.djvm.whitelist - -import co.paralleluniverse.fibers.Suspendable -import net.corda.contracts.djvm.whitelist.DeterministicWhitelistContract.Operate -import net.corda.contracts.djvm.whitelist.DeterministicWhitelistContract.State -import net.corda.contracts.djvm.whitelist.WhitelistData -import net.corda.core.contracts.Command -import net.corda.core.crypto.SecureHash -import net.corda.core.flows.FlowLogic -import net.corda.core.flows.StartableByRPC -import net.corda.core.transactions.TransactionBuilder - -@StartableByRPC -class DeterministicWhitelistFlow(private val data: WhitelistData) : FlowLogic() { - @Suspendable - override fun call(): SecureHash { - val notary = serviceHub.networkMapCache.notaryIdentities[0] - val stx = serviceHub.signInitialTransaction( - TransactionBuilder(notary) - .addOutputState(State(ourIdentity, data)) - .addCommand(Command(Operate(), ourIdentity.owningKey)) - ) - stx.verify(serviceHub, checkSufficientSignatures = false) - return stx.id - } -} diff --git a/node/src/integration-test/kotlin/net/corda/node/DeterministicSourcesRule.kt b/node/src/integration-test/kotlin/net/corda/node/DeterministicSourcesRule.kt deleted file mode 100644 index 4171f40fda..0000000000 --- a/node/src/integration-test/kotlin/net/corda/node/DeterministicSourcesRule.kt +++ /dev/null @@ -1,31 +0,0 @@ -package net.corda.node - -import org.junit.rules.TestRule -import org.junit.runner.Description -import org.junit.runners.model.Statement -import java.io.File -import java.nio.file.Files -import java.nio.file.Path -import java.nio.file.Paths -import kotlin.test.fail - -class DeterministicSourcesRule : TestRule { - private var deterministicRt: Path? = null - private var deterministicSources: List? = null - - val bootstrap: Path get() = deterministicRt ?: fail("deterministic-rt.path property not set") - val corda: List get() = deterministicSources ?: fail("deterministic-sources.path property not set") - - override fun apply(statement: Statement, description: Description?): Statement { - deterministicRt = System.getProperty("deterministic-rt.path")?.run { Paths.get(this) } - deterministicSources = System.getProperty("deterministic-sources.path")?.split(File.pathSeparator) - ?.map { Paths.get(it) } - ?.filter { Files.exists(it) } - - return object : Statement() { - override fun evaluate() { - statement.evaluate() - } - } - } -} \ No newline at end of file diff --git a/node/src/integration-test/kotlin/net/corda/node/services/AttachmentLoadingTests.kt b/node/src/integration-test/kotlin/net/corda/node/services/AttachmentLoadingTests.kt index 7dda30ca06..e62b1f4883 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/AttachmentLoadingTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/AttachmentLoadingTests.kt @@ -45,7 +45,7 @@ class AttachmentLoadingTests { } @Test(timeout=300_000) - fun `contracts downloaded from the network are not executed without the DJVM`() { + fun `contracts downloaded from the network are not executed`() { driver(DriverParameters( startNodesInProcess = false, notarySpecs = listOf(NotarySpec(DUMMY_NOTARY_NAME, validating = false)), diff --git a/node/src/integration-test/kotlin/net/corda/node/services/DeterministicCashIssueAndPaymentTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/DeterministicCashIssueAndPaymentTest.kt deleted file mode 100644 index 9b3bdf77ef..0000000000 --- a/node/src/integration-test/kotlin/net/corda/node/services/DeterministicCashIssueAndPaymentTest.kt +++ /dev/null @@ -1,72 +0,0 @@ -package net.corda.node.services - -import net.corda.core.messaging.startFlow -import net.corda.core.utilities.OpaqueBytes -import net.corda.core.utilities.getOrThrow -import net.corda.core.utilities.loggerFor -import net.corda.finance.DOLLARS -import net.corda.finance.flows.CashIssueAndPaymentFlow -import net.corda.node.DeterministicSourcesRule -import net.corda.node.services.config.NodeConfiguration -import net.corda.testing.core.ALICE_NAME -import net.corda.testing.core.DUMMY_NOTARY_NAME -import net.corda.testing.core.singleIdentity -import net.corda.testing.driver.DriverParameters -import net.corda.testing.driver.driver -import net.corda.testing.driver.internal.incrementalPortAllocation -import net.corda.testing.node.NotarySpec -import net.corda.testing.node.internal.findCordapp -import org.junit.ClassRule -import org.junit.Test -import org.junit.jupiter.api.assertDoesNotThrow - -@Suppress("FunctionName") -class DeterministicCashIssueAndPaymentTest { - companion object { - private val logger = loggerFor() - - private val configOverrides = mapOf(NodeConfiguration::reloadCheckpointAfterSuspend.name to true) - private val CASH_AMOUNT = 500.DOLLARS - - @ClassRule - @JvmField - val djvmSources = DeterministicSourcesRule() - - fun parametersFor(djvmSources: DeterministicSourcesRule, runInProcess: Boolean = false): DriverParameters { - return DriverParameters( - portAllocation = incrementalPortAllocation(), - startNodesInProcess = runInProcess, - notarySpecs = listOf(NotarySpec(DUMMY_NOTARY_NAME, startInProcess = runInProcess, validating = true)), - notaryCustomOverrides = configOverrides, - cordappsForAllNodes = listOf( - findCordapp("net.corda.finance.contracts"), - findCordapp("net.corda.finance.workflows") - ), - djvmBootstrapSource = djvmSources.bootstrap, - djvmCordaSource = djvmSources.corda - ) - } - } - - @Test(timeout = 300_000) - fun `test DJVM can issue cash`() { - val reference = OpaqueBytes.of(0x01) - driver(parametersFor(djvmSources)) { - val alice = startNode(providedName = ALICE_NAME, customOverrides = configOverrides).getOrThrow() - val aliceParty = alice.nodeInfo.singleIdentity() - val notaryParty = notaryHandles.single().identity - val txId = assertDoesNotThrow { - alice.rpc.startFlow(::CashIssueAndPaymentFlow, - CASH_AMOUNT, - reference, - aliceParty, - false, - notaryParty - ).use { flowHandle -> - flowHandle.returnValue.getOrThrow() - } - } - logger.info("TX-ID: {}", txId) - } - } -} diff --git a/node/src/integration-test/kotlin/net/corda/node/services/DeterministicContractCannotMutateTransactionTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/DeterministicContractCannotMutateTransactionTest.kt deleted file mode 100644 index 41c80ea7d9..0000000000 --- a/node/src/integration-test/kotlin/net/corda/node/services/DeterministicContractCannotMutateTransactionTest.kt +++ /dev/null @@ -1,55 +0,0 @@ -package net.corda.node.services - -import net.corda.client.rpc.CordaRPCClient -import net.corda.core.messaging.startFlow -import net.corda.core.utilities.getOrThrow -import net.corda.core.utilities.loggerFor -import net.corda.flows.mutator.MutatorFlow -import net.corda.node.DeterministicSourcesRule -import net.corda.testing.core.ALICE_NAME -import net.corda.testing.core.DUMMY_NOTARY_NAME -import net.corda.testing.driver.DriverParameters -import net.corda.testing.driver.driver -import net.corda.testing.driver.internal.incrementalPortAllocation -import net.corda.testing.node.NotarySpec -import net.corda.testing.node.User -import net.corda.testing.node.internal.cordappWithPackages -import org.junit.ClassRule -import org.junit.Test - -class DeterministicContractCannotMutateTransactionTest { - companion object { - private val logger = loggerFor() - private val user = User("u", "p", setOf(Permissions.all())) - private val mutatorFlowCorDapp = cordappWithPackages("net.corda.flows.mutator").signed() - private val mutatorContractCorDapp = cordappWithPackages("net.corda.contracts.mutator").signed() - - @ClassRule - @JvmField - val djvmSources = DeterministicSourcesRule() - - fun driverParameters(runInProcess: Boolean = false): DriverParameters { - return DriverParameters( - portAllocation = incrementalPortAllocation(), - startNodesInProcess = runInProcess, - notarySpecs = listOf(NotarySpec(DUMMY_NOTARY_NAME, startInProcess = runInProcess, validating = true)), - cordappsForAllNodes = listOf(mutatorContractCorDapp, mutatorFlowCorDapp), - djvmBootstrapSource = djvmSources.bootstrap, - djvmCordaSource = djvmSources.corda - ) - } - } - - @Test(timeout = 300_000) - fun testContractCannotModifyTransaction() { - driver(driverParameters()) { - val alice = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user)).getOrThrow() - val txID = CordaRPCClient(hostAndPort = alice.rpcAddress) - .start(user.username, user.password) - .use { client -> - client.proxy.startFlow(::MutatorFlow).returnValue.getOrThrow() - } - logger.info("TX-ID: {}", txID) - } - } -} diff --git a/node/src/integration-test/kotlin/net/corda/node/services/DeterministicContractCryptoTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/DeterministicContractCryptoTest.kt deleted file mode 100644 index 5d28ae41b8..0000000000 --- a/node/src/integration-test/kotlin/net/corda/node/services/DeterministicContractCryptoTest.kt +++ /dev/null @@ -1,73 +0,0 @@ -package net.corda.node.services - -import net.corda.contracts.djvm.crypto.DeterministicCryptoContract.Validate -import net.corda.core.crypto.Crypto -import net.corda.core.crypto.Crypto.DEFAULT_SIGNATURE_SCHEME -import net.corda.core.messaging.startFlow -import net.corda.core.utilities.OpaqueBytes -import net.corda.core.utilities.getOrThrow -import net.corda.core.utilities.loggerFor -import net.corda.flows.djvm.crypto.DeterministicCryptoFlow -import net.corda.node.DeterministicSourcesRule -import net.corda.testing.core.ALICE_NAME -import net.corda.testing.core.DUMMY_NOTARY_NAME -import net.corda.testing.driver.DriverParameters -import net.corda.testing.driver.driver -import net.corda.testing.driver.internal.incrementalPortAllocation -import net.corda.testing.node.NotarySpec -import net.corda.testing.node.internal.CustomCordapp -import net.corda.testing.node.internal.cordappWithPackages -import org.junit.ClassRule -import org.junit.Test -import org.junit.jupiter.api.assertDoesNotThrow -import java.security.KeyPairGenerator - -@Suppress("FunctionName") -class DeterministicContractCryptoTest { - companion object { - const val MESSAGE = "Very Important Data! Do Not Change!" - val logger = loggerFor() - - @ClassRule - @JvmField - val djvmSources = DeterministicSourcesRule() - - fun parametersFor(djvmSources: DeterministicSourcesRule, runInProcess: Boolean = false): DriverParameters { - return DriverParameters( - portAllocation = incrementalPortAllocation(), - startNodesInProcess = runInProcess, - notarySpecs = listOf(NotarySpec(DUMMY_NOTARY_NAME, startInProcess = runInProcess, validating = true)), - cordappsForAllNodes = listOf( - cordappWithPackages("net.corda.flows.djvm.crypto"), - CustomCordapp( - packages = setOf("net.corda.contracts.djvm.crypto"), - name = "deterministic-crypto-contract" - ).signed() - ), - djvmBootstrapSource = djvmSources.bootstrap, - djvmCordaSource = djvmSources.corda - ) - } - } - - @Test(timeout=300_000) - fun `test DJVM can verify using crypto`() { - val keyPair = KeyPairGenerator.getInstance(DEFAULT_SIGNATURE_SCHEME.algorithmName).genKeyPair() - val importantData = OpaqueBytes(MESSAGE.toByteArray()) - val signature = OpaqueBytes(Crypto.doSign(DEFAULT_SIGNATURE_SCHEME, keyPair.`private`, importantData.bytes)) - - val validate = Validate( - schemeCodeName = DEFAULT_SIGNATURE_SCHEME.schemeCodeName, - publicKey = keyPair.`public` - ) - - driver(parametersFor(djvmSources)) { - val alice = startNode(providedName = ALICE_NAME).getOrThrow() - val txId = assertDoesNotThrow { - alice.rpc.startFlow(::DeterministicCryptoFlow, validate, importantData, signature) - .returnValue.getOrThrow() - } - logger.info("TX-ID: {}", txId) - } - } -} diff --git a/node/src/integration-test/kotlin/net/corda/node/services/DeterministicContractWithCustomSerializerTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/DeterministicContractWithCustomSerializerTest.kt deleted file mode 100644 index a6a8e4221e..0000000000 --- a/node/src/integration-test/kotlin/net/corda/node/services/DeterministicContractWithCustomSerializerTest.kt +++ /dev/null @@ -1,91 +0,0 @@ -package net.corda.node.services - -import net.corda.contracts.serialization.custom.Currantsy -import net.corda.contracts.serialization.custom.CustomSerializerContract -import net.corda.core.messaging.startFlow -import net.corda.core.utilities.getOrThrow -import net.corda.core.utilities.loggerFor -import net.corda.flows.serialization.custom.CustomSerializerFlow -import net.corda.node.DeterministicSourcesRule -import net.corda.node.assertNotCordaSerializable -import net.corda.node.internal.djvm.DeterministicVerificationException -import net.corda.testing.core.ALICE_NAME -import net.corda.testing.core.DUMMY_NOTARY_NAME -import net.corda.testing.driver.DriverParameters -import net.corda.testing.driver.driver -import net.corda.testing.driver.internal.incrementalPortAllocation -import net.corda.testing.node.NotarySpec -import net.corda.testing.node.TestCordapp -import net.corda.testing.node.internal.cordappWithPackages -import org.assertj.core.api.Assertions.assertThat -import org.junit.BeforeClass -import org.junit.ClassRule -import org.junit.Ignore -import org.junit.Test -import org.junit.jupiter.api.assertDoesNotThrow -import org.junit.jupiter.api.assertThrows - -@Suppress("FunctionName") -@Ignore("flaky tests in CI") -class DeterministicContractWithCustomSerializerTest { - companion object { - val logger = loggerFor() - const val GOOD_CURRANTS = 1201L - const val BAD_CURRANTS = 4703L - - @ClassRule - @JvmField - val djvmSources = DeterministicSourcesRule() - - @JvmField - val flowCordapp = cordappWithPackages("net.corda.flows.serialization.custom").signed() - - @JvmField - val contractCordapp = cordappWithPackages("net.corda.contracts.serialization.custom").signed() - - fun parametersFor(djvmSources: DeterministicSourcesRule, cordapps: List, runInProcess: Boolean = false): DriverParameters { - return DriverParameters( - portAllocation = incrementalPortAllocation(), - startNodesInProcess = runInProcess, - notarySpecs = listOf(NotarySpec(DUMMY_NOTARY_NAME, startInProcess = runInProcess, validating = true)), - cordappsForAllNodes = cordapps.toList(), - djvmBootstrapSource = djvmSources.bootstrap, - djvmCordaSource = djvmSources.corda - ) - } - - @BeforeClass - @JvmStatic - fun checkData() { - assertNotCordaSerializable() - } - } - - @Test(timeout=300_000) - fun `test DJVM can verify using custom serializer`() { - driver(parametersFor(djvmSources, listOf(flowCordapp, contractCordapp))) { - val alice = startNode(providedName = ALICE_NAME).getOrThrow() - val txId = assertDoesNotThrow { - alice.rpc.startFlow(::CustomSerializerFlow, Currantsy(GOOD_CURRANTS)) - .returnValue.getOrThrow() - } - logger.info("TX-ID: {}", txId) - } - } - - @Test(timeout=300_000) - fun `test DJVM can fail verify using custom serializer`() { - driver(parametersFor(djvmSources, listOf(flowCordapp, contractCordapp))) { - val alice = startNode(providedName = ALICE_NAME).getOrThrow() - val currantsy = Currantsy(BAD_CURRANTS) - val ex = assertThrows { - alice.rpc.startFlow(::CustomSerializerFlow, currantsy) - .returnValue.getOrThrow() - } - assertThat(ex) - .hasMessageStartingWith("sandbox.net.corda.core.contracts.TransactionVerificationException\$ContractRejection -> ") - .hasMessageContaining(" Contract verification failed: Too many currants! $currantsy is unraisinable!, ") - .hasMessageContaining(" contract: sandbox.${CustomSerializerContract::class.java.name}, ") - } - } -} diff --git a/node/src/integration-test/kotlin/net/corda/node/services/DeterministicContractWithGenericTypeTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/DeterministicContractWithGenericTypeTest.kt deleted file mode 100644 index d2cae60136..0000000000 --- a/node/src/integration-test/kotlin/net/corda/node/services/DeterministicContractWithGenericTypeTest.kt +++ /dev/null @@ -1,85 +0,0 @@ -package net.corda.node.services - -import net.corda.client.rpc.CordaRPCClient -import net.corda.contracts.serialization.generics.DataObject -import net.corda.core.messaging.startFlow -import net.corda.core.utilities.getOrThrow -import net.corda.core.utilities.loggerFor -import net.corda.flows.serialization.generics.GenericTypeFlow -import net.corda.node.DeterministicSourcesRule -import net.corda.node.internal.djvm.DeterministicVerificationException -import net.corda.testing.core.ALICE_NAME -import net.corda.testing.core.DUMMY_NOTARY_NAME -import net.corda.testing.driver.DriverParameters -import net.corda.testing.driver.driver -import net.corda.testing.driver.internal.incrementalPortAllocation -import net.corda.testing.node.NotarySpec -import net.corda.testing.node.User -import net.corda.testing.node.internal.cordappWithPackages -import org.assertj.core.api.Assertions.assertThat -import org.junit.ClassRule -import org.junit.Test -import org.junit.jupiter.api.assertThrows - -@Suppress("FunctionName") -class DeterministicContractWithGenericTypeTest { - companion object { - const val DATA_VALUE = 5000L - - @JvmField - val logger = loggerFor() - - @JvmField - val user = User("u", "p", setOf(Permissions.all())) - - @ClassRule - @JvmField - val djvmSources = DeterministicSourcesRule() - - fun parameters(runInProcess: Boolean = false): DriverParameters { - return DriverParameters( - portAllocation = incrementalPortAllocation(), - startNodesInProcess = runInProcess, - notarySpecs = listOf(NotarySpec(DUMMY_NOTARY_NAME, startInProcess = runInProcess, validating = true)), - cordappsForAllNodes = listOf( - cordappWithPackages("net.corda.flows.serialization.generics").signed(), - cordappWithPackages("net.corda.contracts.serialization.generics").signed() - ), - djvmBootstrapSource = djvmSources.bootstrap, - djvmCordaSource = djvmSources.corda - ) - } - } - - @Test(timeout = 300_000) - fun `test DJVM can deserialise command with generic type`() { - driver(parameters()) { - val alice = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user)).getOrThrow() - val txID = CordaRPCClient(hostAndPort = alice.rpcAddress) - .start(user.username, user.password) - .use { client -> - client.proxy.startFlow(::GenericTypeFlow, DataObject(DATA_VALUE)) - .returnValue - .getOrThrow() - } - logger.info("TX-ID=$txID") - } - } - - @Test(timeout = 300_000) - fun `test DJVM can deserialise command without value of generic type`() { - driver(parameters()) { - val alice = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user)).getOrThrow() - val ex = assertThrows { - CordaRPCClient(hostAndPort = alice.rpcAddress) - .start(user.username, user.password) - .use { client -> - client.proxy.startFlow(::GenericTypeFlow, null) - .returnValue - .getOrThrow() - } - } - assertThat(ex).hasMessageContaining("Contract verification failed: Failed requirement: Purchase has a price,") - } - } -} \ No newline at end of file diff --git a/node/src/integration-test/kotlin/net/corda/node/services/DeterministicContractWithSerializationWhitelistTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/DeterministicContractWithSerializationWhitelistTest.kt deleted file mode 100644 index af905d76f3..0000000000 --- a/node/src/integration-test/kotlin/net/corda/node/services/DeterministicContractWithSerializationWhitelistTest.kt +++ /dev/null @@ -1,91 +0,0 @@ -package net.corda.node.services - -import net.corda.contracts.djvm.whitelist.DeterministicWhitelistContract -import net.corda.contracts.djvm.whitelist.WhitelistData -import net.corda.core.messaging.startFlow -import net.corda.core.utilities.getOrThrow -import net.corda.core.utilities.loggerFor -import net.corda.flows.djvm.whitelist.DeterministicWhitelistFlow -import net.corda.node.DeterministicSourcesRule -import net.corda.node.assertNotCordaSerializable -import net.corda.node.internal.djvm.DeterministicVerificationException -import net.corda.testing.core.ALICE_NAME -import net.corda.testing.core.DUMMY_NOTARY_NAME -import net.corda.testing.driver.DriverParameters -import net.corda.testing.driver.driver -import net.corda.testing.driver.internal.incrementalPortAllocation -import net.corda.testing.node.NotarySpec -import net.corda.testing.node.TestCordapp -import net.corda.testing.node.internal.cordappWithPackages -import org.assertj.core.api.Assertions.assertThat -import org.junit.BeforeClass -import org.junit.ClassRule -import org.junit.Ignore -import org.junit.Test -import org.junit.jupiter.api.assertDoesNotThrow -import org.junit.jupiter.api.assertThrows - -@Suppress("FunctionName") -@Ignore -class DeterministicContractWithSerializationWhitelistTest { - companion object { - val logger = loggerFor() - const val GOOD_VALUE = 1201L - const val BAD_VALUE = 6333L - - @ClassRule - @JvmField - val djvmSources = DeterministicSourcesRule() - - @JvmField - val flowCordapp = cordappWithPackages("net.corda.flows.djvm.whitelist").signed() - - @JvmField - val contractCordapp = cordappWithPackages("net.corda.contracts.djvm.whitelist").signed() - - fun parametersFor(djvmSources: DeterministicSourcesRule, cordapps: List, runInProcess: Boolean = false): DriverParameters { - return DriverParameters( - portAllocation = incrementalPortAllocation(), - startNodesInProcess = runInProcess, - notarySpecs = listOf(NotarySpec(DUMMY_NOTARY_NAME, startInProcess = runInProcess, validating = true)), - cordappsForAllNodes = cordapps.toList(), - djvmBootstrapSource = djvmSources.bootstrap, - djvmCordaSource = djvmSources.corda - ) - } - - @BeforeClass - @JvmStatic - fun checkData() { - assertNotCordaSerializable() - } - } - - @Test(timeout=300_000) - fun `test DJVM can verify using whitelist`() { - driver(parametersFor(djvmSources, listOf(flowCordapp, contractCordapp))) { - val alice = startNode(providedName = ALICE_NAME).getOrThrow() - val txId = assertDoesNotThrow { - alice.rpc.startFlow(::DeterministicWhitelistFlow, WhitelistData(GOOD_VALUE)) - .returnValue.getOrThrow() - } - logger.info("TX-ID: {}", txId) - } - } - - @Test(timeout=300_000) - fun `test DJVM can fail verify using whitelist`() { - driver(parametersFor(djvmSources, listOf(flowCordapp, contractCordapp))) { - val alice = startNode(providedName = ALICE_NAME).getOrThrow() - val badData = WhitelistData(BAD_VALUE) - val ex = assertThrows { - alice.rpc.startFlow(::DeterministicWhitelistFlow, badData) - .returnValue.getOrThrow() - } - assertThat(ex) - .hasMessageStartingWith("sandbox.net.corda.core.contracts.TransactionVerificationException\$ContractRejection -> ") - .hasMessageContaining(" Contract verification failed: WhitelistData $badData exceeds maximum value!, ") - .hasMessageContaining(" contract: sandbox.${DeterministicWhitelistContract::class.java.name}, ") - } - } -} diff --git a/node/src/integration-test/kotlin/net/corda/node/services/DeterministicEvilContractCannotModifyStatesTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/DeterministicEvilContractCannotModifyStatesTest.kt deleted file mode 100644 index 5188f0b4fd..0000000000 --- a/node/src/integration-test/kotlin/net/corda/node/services/DeterministicEvilContractCannotModifyStatesTest.kt +++ /dev/null @@ -1,71 +0,0 @@ -package net.corda.node.services - -import net.corda.client.rpc.CordaRPCClient -import net.corda.contracts.multiple.vulnerable.MutableDataObject -import net.corda.contracts.multiple.vulnerable.VulnerablePaymentContract -import net.corda.core.messaging.startFlow -import net.corda.core.utilities.getOrThrow -import net.corda.flows.multiple.evil.EvilFlow -import net.corda.node.DeterministicSourcesRule -import net.corda.node.internal.djvm.DeterministicVerificationException -import net.corda.testing.core.ALICE_NAME -import net.corda.testing.core.DUMMY_NOTARY_NAME -import net.corda.testing.driver.DriverParameters -import net.corda.testing.driver.driver -import net.corda.testing.driver.internal.incrementalPortAllocation -import net.corda.testing.node.NotarySpec -import net.corda.testing.node.User -import net.corda.testing.node.internal.cordappWithPackages -import org.assertj.core.api.Assertions.assertThat -import org.junit.ClassRule -import org.junit.Test -import kotlin.test.assertFailsWith - -class DeterministicEvilContractCannotModifyStatesTest { - companion object { - private val user = User("u", "p", setOf(Permissions.all())) - private val evilFlowCorDapp = cordappWithPackages("net.corda.flows.multiple.evil").signed() - private val evilContractCorDapp = cordappWithPackages("net.corda.contracts.multiple.evil").signed() - private val vulnerableContractCorDapp = cordappWithPackages("net.corda.contracts.multiple.vulnerable").signed() - - private val NOTHING = MutableDataObject(0) - - @ClassRule - @JvmField - val djvmSources = DeterministicSourcesRule() - - fun driverParameters(runInProcess: Boolean = false): DriverParameters { - return DriverParameters( - portAllocation = incrementalPortAllocation(), - startNodesInProcess = runInProcess, - notarySpecs = listOf(NotarySpec(DUMMY_NOTARY_NAME, startInProcess = runInProcess, validating = true)), - cordappsForAllNodes = listOf( - vulnerableContractCorDapp, - evilContractCorDapp, - evilFlowCorDapp - ), - djvmBootstrapSource = djvmSources.bootstrap, - djvmCordaSource = djvmSources.corda - ) - } - } - - @Test(timeout = 300_000) - fun testContractThatTriesToModifyStates() { - val evilData = MutableDataObject(5000) - driver(driverParameters()) { - val alice = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user)).getOrThrow() - val ex = assertFailsWith { - CordaRPCClient(hostAndPort = alice.rpcAddress) - .start(user.username, user.password) - .use { client -> - client.proxy.startFlow(::EvilFlow, evilData).returnValue.getOrThrow() - } - } - assertThat(ex) - .hasMessageStartingWith("sandbox.net.corda.core.contracts.TransactionVerificationException\$ContractRejection -> ") - .hasMessageContaining(" Contract verification failed: Failed requirement: Purchase payment of $NOTHING should be at least ") - .hasMessageContaining(", contract: sandbox.${VulnerablePaymentContract::class.java.name}, ") - } - } -} diff --git a/node/src/integration-test/kotlin/net/corda/node/services/NonDeterministicContractVerifyTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/NonDeterministicContractVerifyTest.kt deleted file mode 100644 index a200d5db41..0000000000 --- a/node/src/integration-test/kotlin/net/corda/node/services/NonDeterministicContractVerifyTest.kt +++ /dev/null @@ -1,133 +0,0 @@ -package net.corda.node.services - -import net.corda.contracts.djvm.broken.NonDeterministicContract.CurrentTimeMillis -import net.corda.contracts.djvm.broken.NonDeterministicContract.InstantNow -import net.corda.contracts.djvm.broken.NonDeterministicContract.NanoTime -import net.corda.contracts.djvm.broken.NonDeterministicContract.NoOperation -import net.corda.contracts.djvm.broken.NonDeterministicContract.RandomUUID -import net.corda.contracts.djvm.broken.NonDeterministicContract.WithReflection -import net.corda.core.messaging.startFlow -import net.corda.core.utilities.getOrThrow -import net.corda.core.utilities.loggerFor -import net.corda.flows.djvm.broken.NonDeterministicFlow -import net.corda.node.DeterministicSourcesRule -import net.corda.node.internal.djvm.DeterministicVerificationException -import net.corda.testing.core.ALICE_NAME -import net.corda.testing.core.DUMMY_NOTARY_NAME -import net.corda.testing.driver.DriverParameters -import net.corda.testing.driver.driver -import net.corda.testing.driver.internal.incrementalPortAllocation -import net.corda.testing.node.NotarySpec -import net.corda.testing.node.internal.CustomCordapp -import net.corda.testing.node.internal.cordappWithPackages -import org.assertj.core.api.AssertionsForInterfaceTypes.assertThat -import org.junit.ClassRule -import org.junit.Test -import org.junit.jupiter.api.assertDoesNotThrow -import org.junit.jupiter.api.assertThrows - -@Suppress("FunctionName") -class NonDeterministicContractVerifyTest { - companion object { - val logger = loggerFor() - - @ClassRule - @JvmField - val djvmSources = DeterministicSourcesRule() - - fun parametersFor(djvmSources: DeterministicSourcesRule, runInProcess: Boolean = false): DriverParameters { - return DriverParameters( - portAllocation = incrementalPortAllocation(), - startNodesInProcess = runInProcess, - notarySpecs = listOf(NotarySpec(DUMMY_NOTARY_NAME, startInProcess = runInProcess, validating = true)), - cordappsForAllNodes = listOf( - cordappWithPackages("net.corda.flows.djvm.broken"), - CustomCordapp( - packages = setOf("net.corda.contracts.djvm.broken"), - name = "nondeterministic-contract" - ).signed() - ), - djvmBootstrapSource = djvmSources.bootstrap, - djvmCordaSource = djvmSources.corda - ) - } - } - - @Test(timeout=300_000) - fun `test DJVM rejects contract that uses Instant now`() { - driver(parametersFor(djvmSources)) { - val alice = startNode(providedName = ALICE_NAME).getOrThrow() - val ex = assertThrows { - alice.rpc.startFlow(::NonDeterministicFlow, InstantNow()) - .returnValue.getOrThrow() - } - assertThat(ex) - .hasMessageMatching("^NoSuchMethodError: .*[\\Qsandbox.java.time.Instant.now()\\E|\\Qsandbox.java/time/Instant/now()\\E].*\$") - } - } - - @Test(timeout=300_000) - fun `test DJVM rejects contract that uses System currentTimeMillis`() { - driver(parametersFor(djvmSources)) { - val alice = startNode(providedName = ALICE_NAME).getOrThrow() - val ex = assertThrows { - alice.rpc.startFlow(::NonDeterministicFlow, CurrentTimeMillis()) - .returnValue.getOrThrow() - } - assertThat(ex) - .hasMessageStartingWith("RuleViolationError: Disallowed reference to API; java.lang.System.currentTimeMillis(), ") - } - } - - @Test(timeout=300_000) - fun `test DJVM rejects contract that uses System nanoTime`() { - driver(parametersFor(djvmSources)) { - val alice = startNode(providedName = ALICE_NAME).getOrThrow() - val ex = assertThrows { - alice.rpc.startFlow(::NonDeterministicFlow, NanoTime()) - .returnValue.getOrThrow() - } - assertThat(ex) - .hasMessageStartingWith("RuleViolationError: Disallowed reference to API; java.lang.System.nanoTime(), ") - } - } - - @Test(timeout=300_000) - fun `test DJVM rejects contract that uses UUID randomUUID`() { - driver(parametersFor(djvmSources)) { - val alice = startNode(providedName = ALICE_NAME).getOrThrow() - val ex = assertThrows { - alice.rpc.startFlow(::NonDeterministicFlow, RandomUUID()) - .returnValue.getOrThrow() - } - assertThat(ex) - .hasMessageMatching("^NoSuchMethodError: .*[\\Qsandbox.java.util.UUID.randomUUID()\\E|\\Qsandbox/java/util/UUID/randomUUID()\\E].*\$") - } - } - - @Test(timeout=300_000) - fun `test DJVM rejects contract that uses reflection`() { - driver(parametersFor(djvmSources)) { - val alice = startNode(providedName = ALICE_NAME).getOrThrow() - val ex = assertThrows { - alice.rpc.startFlow(::NonDeterministicFlow, WithReflection()) - .returnValue.getOrThrow() - } - assertThat(ex).hasMessageStartingWith( - "RuleViolationError: Disallowed reference to API; java.lang.Class.getDeclaredConstructor(Class[]), " - ) - } - } - - @Test(timeout=300_000) - fun `test DJVM can succeed`() { - driver(parametersFor(djvmSources)) { - val alice = startNode(providedName = ALICE_NAME).getOrThrow() - val txId = assertDoesNotThrow { - alice.rpc.startFlow(::NonDeterministicFlow, NoOperation()) - .returnValue.getOrThrow() - } - logger.info("TX-ID: {}", txId) - } - } -} diff --git a/node/src/integration-test/kotlin/net/corda/node/services/SandboxAttachmentsTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/SandboxAttachmentsTest.kt deleted file mode 100644 index e868566f58..0000000000 --- a/node/src/integration-test/kotlin/net/corda/node/services/SandboxAttachmentsTest.kt +++ /dev/null @@ -1,80 +0,0 @@ -package net.corda.node.services - -import net.corda.contracts.djvm.attachment.SandboxAttachmentContract -import net.corda.contracts.djvm.attachment.SandboxAttachmentContract.ExtractFile -import net.corda.core.messaging.startFlow -import net.corda.core.utilities.getOrThrow -import net.corda.core.utilities.loggerFor -import net.corda.flows.djvm.attachment.SandboxAttachmentFlow -import net.corda.node.DeterministicSourcesRule -import net.corda.node.internal.djvm.DeterministicVerificationException -import net.corda.testing.core.ALICE_NAME -import net.corda.testing.core.DUMMY_NOTARY_NAME -import net.corda.testing.driver.DriverParameters -import net.corda.testing.driver.driver -import net.corda.testing.driver.internal.incrementalPortAllocation -import net.corda.testing.node.NotarySpec -import net.corda.testing.node.internal.CustomCordapp -import net.corda.testing.node.internal.cordappWithPackages -import org.assertj.core.api.Assertions.assertThat -import org.junit.ClassRule -import org.junit.Test -import org.junit.jupiter.api.assertDoesNotThrow -import org.junit.jupiter.api.assertThrows - -@Suppress("FunctionName") -class SandboxAttachmentsTest { - companion object { - val logger = loggerFor() - - @ClassRule - @JvmField - val djvmSources = DeterministicSourcesRule() - - fun parametersFor(djvmSources: DeterministicSourcesRule, runInProcess: Boolean = false): DriverParameters { - return DriverParameters( - portAllocation = incrementalPortAllocation(), - startNodesInProcess = runInProcess, - notarySpecs = listOf(NotarySpec(DUMMY_NOTARY_NAME, startInProcess = runInProcess, validating = true)), - cordappsForAllNodes = listOf( - cordappWithPackages("net.corda.flows.djvm.attachment"), - CustomCordapp( - packages = setOf("net.corda.contracts.djvm.attachment"), - name = "sandbox-attachment-contract" - ).signed() - ), - djvmBootstrapSource = djvmSources.bootstrap, - djvmCordaSource = djvmSources.corda - ) - } - } - - @Test(timeout=300_000) - fun `test attachment accessible within sandbox`() { - val extractFile = ExtractFile(SandboxAttachmentContract::class.java.name.replace('.', '/') + ".class") - driver(parametersFor(djvmSources)) { - val alice = startNode(providedName = ALICE_NAME).getOrThrow() - val txId = assertDoesNotThrow { - alice.rpc.startFlow(::SandboxAttachmentFlow, extractFile) - .returnValue.getOrThrow() - } - logger.info("TX-ID: {}", txId) - } - } - - @Test(timeout=300_000) - fun `test attachment file not found within sandbox`() { - val extractFile = ExtractFile("does/not/Exist.class") - driver(parametersFor(djvmSources)) { - val alice = startNode(providedName = ALICE_NAME).getOrThrow() - val ex = assertThrows { - alice.rpc.startFlow(::SandboxAttachmentFlow, extractFile) - .returnValue.getOrThrow() - } - assertThat(ex) - .hasMessageStartingWith("sandbox.net.corda.core.contracts.TransactionVerificationException\$ContractRejection -> ") - .hasMessageContaining(" Contract verification failed: does/not/Exist.class, ") - .hasMessageContaining(" contract: sandbox.net.corda.contracts.djvm.attachment.SandboxAttachmentContract, ") - } - } -} diff --git a/node/src/integration-test/kotlin/net/corda/node/services/identity/CertificateRotationTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/identity/CertificateRotationTest.kt index 3b59ffa55c..34ad626ef5 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/identity/CertificateRotationTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/identity/CertificateRotationTest.kt @@ -42,7 +42,9 @@ class CertificateRotationTest { @After fun tearDown() { - mockNet.stopNodes() + if (::mockNet.isInitialized) { + mockNet.stopNodes() + } } @Test(timeout = 300_000) diff --git a/node/src/main/java/sandbox/java/lang/CharSequence.java b/node/src/main/java/sandbox/java/lang/CharSequence.java deleted file mode 100644 index 9b62762a11..0000000000 --- a/node/src/main/java/sandbox/java/lang/CharSequence.java +++ /dev/null @@ -1,8 +0,0 @@ -package sandbox.java.lang; - -/** - * This is a dummy class that implements just enough of {@link java.lang.CharSequence} - * to allow us to compile {@link sandbox.net.corda.core.crypto.Crypto}. - */ -public interface CharSequence extends java.lang.CharSequence { -} diff --git a/node/src/main/java/sandbox/java/lang/Comparable.java b/node/src/main/java/sandbox/java/lang/Comparable.java deleted file mode 100644 index 5ca4f4871c..0000000000 --- a/node/src/main/java/sandbox/java/lang/Comparable.java +++ /dev/null @@ -1,8 +0,0 @@ -package sandbox.java.lang; - -/** - * This is a dummy class that implements just enough of {@link java.lang.Comparable} - * to allow us to compile {@link sandbox.net.corda.core.crypto.Crypto}. - */ -public interface Comparable extends java.lang.Comparable { -} diff --git a/node/src/main/java/sandbox/java/lang/Number.java b/node/src/main/java/sandbox/java/lang/Number.java deleted file mode 100644 index a98d60dbd6..0000000000 --- a/node/src/main/java/sandbox/java/lang/Number.java +++ /dev/null @@ -1,8 +0,0 @@ -package sandbox.java.lang; - -/** - * This is a dummy class that implements just enough of {@link java.lang.Number} - * to allow us to compile {@link sandbox.net.corda.core.crypto.Crypto}. - */ -public class Number extends Object { -} diff --git a/node/src/main/java/sandbox/java/math/BigInteger.java b/node/src/main/java/sandbox/java/math/BigInteger.java deleted file mode 100644 index d58328fd3c..0000000000 --- a/node/src/main/java/sandbox/java/math/BigInteger.java +++ /dev/null @@ -1,8 +0,0 @@ -package sandbox.java.math; - -/** - * This is a dummy class that implements just enough of {@link java.math.BigInteger} - * to allow us to compile {@link sandbox.net.corda.core.crypto.Crypto}. - */ -public class BigInteger extends sandbox.java.lang.Number { -} diff --git a/node/src/main/java/sandbox/java/security/Key.java b/node/src/main/java/sandbox/java/security/Key.java deleted file mode 100644 index 9c5c952852..0000000000 --- a/node/src/main/java/sandbox/java/security/Key.java +++ /dev/null @@ -1,13 +0,0 @@ -package sandbox.java.security; - -import sandbox.java.lang.String; - -/** - * This is a dummy class that implements just enough of {@link java.security.Key} - * to allow us to compile {@link sandbox.net.corda.core.crypto.Crypto}. - */ -public interface Key { - String getAlgorithm(); - String getFormat(); - byte[] getEncoded(); -} diff --git a/node/src/main/java/sandbox/java/security/KeyPair.java b/node/src/main/java/sandbox/java/security/KeyPair.java deleted file mode 100644 index 653e27de8e..0000000000 --- a/node/src/main/java/sandbox/java/security/KeyPair.java +++ /dev/null @@ -1,8 +0,0 @@ -package sandbox.java.security; - -/** - * This is a dummy class that implements just enough of {@link java.security.KeyPair} - * to allow us to compile {@link sandbox.net.corda.core.crypto.Crypto}. - */ -public final class KeyPair extends sandbox.java.lang.Object implements java.io.Serializable { -} \ No newline at end of file diff --git a/node/src/main/java/sandbox/java/security/PrivateKey.java b/node/src/main/java/sandbox/java/security/PrivateKey.java deleted file mode 100644 index a314aa6234..0000000000 --- a/node/src/main/java/sandbox/java/security/PrivateKey.java +++ /dev/null @@ -1,8 +0,0 @@ -package sandbox.java.security; - -/** - * This is a dummy class that implements just enough of {@link java.security.PrivateKey} - * to allow us to compile {@link sandbox.net.corda.core.crypto.Crypto}. - */ -public interface PrivateKey extends Key { -} diff --git a/node/src/main/java/sandbox/java/security/PublicKey.java b/node/src/main/java/sandbox/java/security/PublicKey.java deleted file mode 100644 index 451f33141a..0000000000 --- a/node/src/main/java/sandbox/java/security/PublicKey.java +++ /dev/null @@ -1,8 +0,0 @@ -package sandbox.java.security; - -/** - * This is a dummy class that implements just enough of {@link java.security.PublicKey} - * to allow us to compile {@link sandbox.net.corda.core.crypto.Crypto}. - */ -public interface PublicKey extends Key { -} diff --git a/node/src/main/java/sandbox/java/security/spec/AlgorithmParameterSpec.java b/node/src/main/java/sandbox/java/security/spec/AlgorithmParameterSpec.java deleted file mode 100644 index 7943cf4612..0000000000 --- a/node/src/main/java/sandbox/java/security/spec/AlgorithmParameterSpec.java +++ /dev/null @@ -1,8 +0,0 @@ -package sandbox.java.security.spec; - -/** - * This is a dummy class that implements just enough of {@link java.security.spec.AlgorithmParameterSpec} - * to allow us to compile {@link sandbox.net.corda.core.crypto.Crypto}. - */ -public interface AlgorithmParameterSpec { -} diff --git a/node/src/main/java/sandbox/java/util/ArrayList.java b/node/src/main/java/sandbox/java/util/ArrayList.java deleted file mode 100644 index 7eb5df9f09..0000000000 --- a/node/src/main/java/sandbox/java/util/ArrayList.java +++ /dev/null @@ -1,16 +0,0 @@ -package sandbox.java.util; - -/** - * This is a dummy class that implements just enough of {@link java.util.ArrayList} - * to allow us to compile {@link sandbox.net.corda.core.crypto.Crypto}. - */ -@SuppressWarnings("unused") -public class ArrayList extends sandbox.java.lang.Object implements List { - public ArrayList(int size) { - } - - @Override - public boolean add(T item) { - throw new UnsupportedOperationException("Dummy class - not implemented"); - } -} diff --git a/node/src/main/java/sandbox/java/util/List.java b/node/src/main/java/sandbox/java/util/List.java deleted file mode 100644 index 0f7dbfe22c..0000000000 --- a/node/src/main/java/sandbox/java/util/List.java +++ /dev/null @@ -1,9 +0,0 @@ -package sandbox.java.util; - -/** - * This is a dummy class that implements just enough of {@link java.util.List} - * to allow us to compile {@link sandbox.net.corda.core.crypto.Crypto}. - */ -public interface List { - boolean add(T item); -} diff --git a/node/src/main/java/sandbox/net/corda/core/crypto/DJVM.java b/node/src/main/java/sandbox/net/corda/core/crypto/DJVM.java deleted file mode 100644 index 8004d44666..0000000000 --- a/node/src/main/java/sandbox/net/corda/core/crypto/DJVM.java +++ /dev/null @@ -1,62 +0,0 @@ -package sandbox.net.corda.core.crypto; - -import org.jetbrains.annotations.NotNull; -import sandbox.java.lang.Integer; -import sandbox.java.lang.String; -import sandbox.java.util.ArrayList; -import sandbox.java.util.List; -import sandbox.org.bouncycastle.asn1.x509.AlgorithmIdentifier; - -import java.io.IOException; - -/** - * Helper class for {@link sandbox.net.corda.core.crypto.Crypto}. - * Deliberately package-private. - */ -final class DJVM { - private DJVM() {} - - @NotNull - static SignatureScheme toDJVM(@NotNull net.corda.core.crypto.SignatureScheme scheme) { - // The AlgorithmParameterSpec is deliberately left as null - // because it is computationally expensive to generate these - // objects inside the sandbox every time. - return new SignatureScheme( - scheme.getSchemeNumberID(), - String.toDJVM(scheme.getSchemeCodeName()), - toDJVM(scheme.getSignatureOID()), - toDJVM(scheme.getAlternativeOIDs()), - String.toDJVM(scheme.getProviderName()), - String.toDJVM(scheme.getAlgorithmName()), - String.toDJVM(scheme.getSignatureName()), - null, - Integer.toDJVM(scheme.getKeySize()), - String.toDJVM(scheme.getDesc()) - ); - } - - static org.bouncycastle.asn1.x509.AlgorithmIdentifier fromDJVM(@NotNull AlgorithmIdentifier oid) { - try { - return org.bouncycastle.asn1.x509.AlgorithmIdentifier.getInstance(oid.getEncoded()); - } catch (IOException e) { - throw sandbox.java.lang.DJVM.toRuleViolationError(e); - } - } - - static AlgorithmIdentifier toDJVM(@NotNull org.bouncycastle.asn1.x509.AlgorithmIdentifier oid) { - try { - return AlgorithmIdentifier.getInstance(oid.getEncoded()); - } catch (IOException e) { - throw sandbox.java.lang.DJVM.toRuleViolationError(e); - } - } - - @NotNull - static List toDJVM(@NotNull java.util.List list) { - ArrayList djvmList = new ArrayList<>(list.size()); - for (org.bouncycastle.asn1.x509.AlgorithmIdentifier oid : list) { - djvmList.add(toDJVM(oid)); - } - return djvmList; - } -} diff --git a/node/src/main/java/sandbox/net/corda/core/crypto/DJVMPublicKey.java b/node/src/main/java/sandbox/net/corda/core/crypto/DJVMPublicKey.java deleted file mode 100644 index 199acc39ce..0000000000 --- a/node/src/main/java/sandbox/net/corda/core/crypto/DJVMPublicKey.java +++ /dev/null @@ -1,61 +0,0 @@ -package sandbox.net.corda.core.crypto; - -import org.jetbrains.annotations.NotNull; -import sandbox.java.lang.Object; -import sandbox.java.lang.String; -import sandbox.java.security.PublicKey; - -/** - * We shall delegate as much cryptography as possible to Corda's - * underlying {@link net.corda.core.crypto.Crypto} object and its - * {@link java.security.Provider} classes. This wrapper only needs - * to implement {@link #equals} and {@link #hashCode}. - */ -final class DJVMPublicKey extends Object implements PublicKey { - private final java.security.PublicKey underlying; - private final String algorithm; - private final String format; - private final int hashCode; - - DJVMPublicKey(@NotNull java.security.PublicKey underlying) { - this.underlying = underlying; - this.algorithm = String.toDJVM(underlying.getAlgorithm()); - this.format = String.toDJVM(underlying.getFormat()); - this.hashCode = underlying.hashCode(); - } - - java.security.PublicKey getUnderlying() { - return underlying; - } - - @Override - public boolean equals(java.lang.Object other) { - if (this == other) { - return true; - } else if (!(other instanceof DJVMPublicKey)) { - return false; - } else { - return underlying.equals(((DJVMPublicKey) other).underlying); - } - } - - @Override - public int hashCode() { - return hashCode; - } - - @Override - public String getAlgorithm() { - return algorithm; - } - - @Override - public String getFormat() { - return format; - } - - @Override - public byte[] getEncoded() { - return underlying.getEncoded(); - } -} diff --git a/node/src/main/java/sandbox/org/bouncycastle/asn1/ASN1Encodable.java b/node/src/main/java/sandbox/org/bouncycastle/asn1/ASN1Encodable.java deleted file mode 100644 index e75ac2e6b4..0000000000 --- a/node/src/main/java/sandbox/org/bouncycastle/asn1/ASN1Encodable.java +++ /dev/null @@ -1,9 +0,0 @@ -package sandbox.org.bouncycastle.asn1; - -/** - * This is a dummy class that implements just enough of {@link org.bouncycastle.asn1.ASN1Encodable} - * to allow us to compile {@link sandbox.net.corda.core.crypto.Crypto}. - */ -@SuppressWarnings("WeakerAccess") -public interface ASN1Encodable { -} diff --git a/node/src/main/java/sandbox/org/bouncycastle/asn1/ASN1Object.java b/node/src/main/java/sandbox/org/bouncycastle/asn1/ASN1Object.java deleted file mode 100644 index d71661cc71..0000000000 --- a/node/src/main/java/sandbox/org/bouncycastle/asn1/ASN1Object.java +++ /dev/null @@ -1,16 +0,0 @@ -package sandbox.org.bouncycastle.asn1; - -import sandbox.java.lang.Object; - -import java.io.IOException; - -/** - * This is a dummy class that implements just enough of {@link org.bouncycastle.asn1.ASN1Object} - * to allow us to compile {@link sandbox.net.corda.core.crypto.Crypto}. - */ -@SuppressWarnings("RedundantThrows") -public class ASN1Object extends Object implements ASN1Encodable { - public byte[] getEncoded() throws IOException { - throw new UnsupportedOperationException("Dummy class - not implemented"); - } -} diff --git a/node/src/main/java/sandbox/org/bouncycastle/asn1/x509/AlgorithmIdentifier.java b/node/src/main/java/sandbox/org/bouncycastle/asn1/x509/AlgorithmIdentifier.java deleted file mode 100644 index 144b5f9260..0000000000 --- a/node/src/main/java/sandbox/org/bouncycastle/asn1/x509/AlgorithmIdentifier.java +++ /dev/null @@ -1,14 +0,0 @@ -package sandbox.org.bouncycastle.asn1.x509; - -import sandbox.org.bouncycastle.asn1.ASN1Object; - -/** - * This is a dummy class that implements just enough of {@link org.bouncycastle.asn1.x509.AlgorithmIdentifier} - * to allow us to compile {@link sandbox.net.corda.core.crypto.Crypto}. - */ -@SuppressWarnings("unused") -public class AlgorithmIdentifier extends ASN1Object { - public static AlgorithmIdentifier getInstance(Object obj) { - throw new UnsupportedOperationException("Dummy class - not implemented"); - } -} diff --git a/node/src/main/java/sandbox/org/bouncycastle/asn1/x509/SubjectPublicKeyInfo.java b/node/src/main/java/sandbox/org/bouncycastle/asn1/x509/SubjectPublicKeyInfo.java deleted file mode 100644 index a84fb60772..0000000000 --- a/node/src/main/java/sandbox/org/bouncycastle/asn1/x509/SubjectPublicKeyInfo.java +++ /dev/null @@ -1,10 +0,0 @@ -package sandbox.org.bouncycastle.asn1.x509; - -import sandbox.org.bouncycastle.asn1.ASN1Object; - -/** - * This is a dummy class that implements just enough of {@link org.bouncycastle.asn1.x509.SubjectPublicKeyInfo} - * to allow us to compile {@link sandbox.net.corda.core.crypto.Crypto}. - */ -public class SubjectPublicKeyInfo extends ASN1Object { -} diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index 198b158d24..51da915a64 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -75,9 +75,6 @@ import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.days import net.corda.core.utilities.millis import net.corda.core.utilities.minutes -import net.corda.djvm.source.ApiSource -import net.corda.djvm.source.EmptyApi -import net.corda.djvm.source.UserSource import net.corda.node.CordaClock import net.corda.node.VersionInfo import net.corda.node.internal.attachments.AttachmentTrustInfoRPCOpsImpl @@ -143,10 +140,7 @@ import net.corda.node.services.statemachine.FlowOperator import net.corda.node.services.statemachine.FlowStateMachineImpl import net.corda.node.services.statemachine.SingleThreadedStateMachineManager import net.corda.node.services.statemachine.StateMachineManager -import net.corda.node.services.transactions.BasicVerifierFactoryService -import net.corda.node.services.transactions.DeterministicVerifierFactoryService import net.corda.node.services.transactions.InMemoryTransactionVerifierService -import net.corda.node.services.transactions.VerifierFactoryService import net.corda.node.services.upgrade.ContractUpgradeServiceImpl import net.corda.node.services.vault.NodeVaultService import net.corda.node.utilities.AffinityExecutor @@ -211,8 +205,6 @@ abstract class AbstractNode(val configuration: NodeConfiguration, protected val flowManager: FlowManager, val serverThread: AffinityExecutor.ServiceAffinityExecutor, val busyNodeLatch: ReusableLatch = ReusableLatch(), - djvmBootstrapSource: ApiSource = EmptyApi, - djvmCordaSource: UserSource? = null, protected val allowHibernateToManageAppSchema: Boolean = false, private val allowAppSchemaUpgradeWithCheckpoints: Boolean = false) : SingletonSerializeAsToken() { @@ -321,18 +313,6 @@ abstract class AbstractNode(val configuration: NodeConfiguration, cordappProvider = cordappProvider, attachments = attachments ).tokenize() - val verifierFactoryService: VerifierFactoryService = if (djvmCordaSource != null) { - DeterministicVerifierFactoryService(djvmBootstrapSource, djvmCordaSource).apply { - log.info("DJVM sandbox enabled for deterministic contract verification.") - if (!configuration.devMode) { - log.info("Generating Corda classes for DJVM sandbox.") - generateSandbox() - } - tokenize() - } - } else { - BasicVerifierFactoryService() - } private val attachmentsClassLoaderCache: AttachmentsClassLoaderCache = AttachmentsClassLoaderCacheImpl(cacheFactory).tokenize() val contractUpgradeService = ContractUpgradeServiceImpl(cacheFactory).tokenize() val auditService = DummyAuditService().tokenize() @@ -1318,8 +1298,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, } override fun specialise(ltx: LedgerTransaction): LedgerTransaction { - val ledgerTransaction = servicesForResolution.specialise(ltx) - return verifierFactoryService.apply(ledgerTransaction) + return servicesForResolution.specialise(ltx) } override fun onNewNetworkParameters(networkParameters: NetworkParameters) { diff --git a/node/src/main/kotlin/net/corda/node/internal/Node.kt b/node/src/main/kotlin/net/corda/node/internal/Node.kt index f86bd83eda..963095597d 100644 --- a/node/src/main/kotlin/net/corda/node/internal/Node.kt +++ b/node/src/main/kotlin/net/corda/node/internal/Node.kt @@ -4,7 +4,6 @@ import com.codahale.metrics.MetricFilter import com.codahale.metrics.MetricRegistry import com.codahale.metrics.jmx.JmxReporter import com.github.benmanes.caffeine.cache.Caffeine -import com.jcabi.manifests.Manifests import com.palominolabs.metrics.newrelic.AllEnabledMetricAttributeFilter import com.palominolabs.metrics.newrelic.NewRelicReporter import io.netty.util.NettyRuntime @@ -20,7 +19,6 @@ import net.corda.core.internal.concurrent.thenMatch import net.corda.core.internal.div import net.corda.core.internal.errors.AddressBindingException import net.corda.core.internal.getJavaUpdateVersion -import net.corda.core.internal.isRegularFile import net.corda.core.internal.notary.NotaryService import net.corda.core.messaging.RPCOps import net.corda.core.node.NetworkParameters @@ -30,11 +28,6 @@ import net.corda.core.serialization.internal.SerializationEnvironment import net.corda.core.serialization.internal.nodeSerializationEnv import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.contextLogger -import net.corda.djvm.source.ApiSource -import net.corda.djvm.source.BootstrapClassLoader -import net.corda.djvm.source.EmptyApi -import net.corda.djvm.source.UserPathSource -import net.corda.djvm.source.UserSource import net.corda.node.CordaClock import net.corda.node.SimpleClock import net.corda.node.VersionInfo @@ -98,7 +91,6 @@ import java.lang.Long.max import java.lang.Long.min import java.net.BindException import java.net.InetAddress -import java.nio.file.Files import java.nio.file.Path import java.nio.file.Paths import java.time.Clock @@ -124,8 +116,6 @@ open class Node(configuration: NodeConfiguration, private val initialiseSerialization: Boolean = true, flowManager: FlowManager = NodeFlowManager(configuration.flowOverrides), cacheFactoryPrototype: BindableNamedCacheFactory = DefaultNamedCacheFactory(), - djvmBootstrapSource: ApiSource = createBootstrapSource(configuration), - djvmCordaSource: UserSource? = createCordaSource(configuration), allowHibernateToManageAppSchema: Boolean = false ) : AbstractNode( configuration, @@ -135,8 +125,6 @@ open class Node(configuration: NodeConfiguration, flowManager, // Under normal (non-test execution) it will always be "1" AffinityExecutor.ServiceAffinityExecutor("Node thread-${sameVmNodeCounter.incrementAndGet()}", 1), - djvmBootstrapSource = djvmBootstrapSource, - djvmCordaSource = djvmCordaSource, allowHibernateToManageAppSchema = allowHibernateToManageAppSchema ) { @@ -144,10 +132,6 @@ open class Node(configuration: NodeConfiguration, nodeInfo companion object { - private const val CORDA_DETERMINISTIC_RUNTIME_ATTR = "Corda-Deterministic-Runtime" - private const val CORDA_DETERMINISTIC_CLASSPATH_ATTR = "Corda-Deterministic-Classpath" - private const val CORDA_DJVM = "net.corda.djvm" - private val staticLog = contextLogger() var renderBasicInfoToConsole = true @@ -206,74 +190,6 @@ open class Node(configuration: NodeConfiguration, false } } - - private fun manifestValue(attrName: String): String? = if (Manifests.exists(attrName)) Manifests.read(attrName) else null - - private fun createManifestCordaSource(config: NodeConfiguration): UserSource? { - val classpathSource = config.baseDirectory.resolve("djvm") - val djvmClasspath = manifestValue(CORDA_DETERMINISTIC_CLASSPATH_ATTR) - - return if (djvmClasspath == null) { - staticLog.warn("{} missing from MANIFEST.MF - deterministic contract verification now impossible!", - CORDA_DETERMINISTIC_CLASSPATH_ATTR) - null - } else if (!Files.isDirectory(classpathSource)) { - staticLog.warn("{} directory does not exist - deterministic contract verification now impossible!", - classpathSource.toAbsolutePath()) - null - } else { - val files = djvmClasspath.split("\\s++".toRegex(), 0).map { classpathSource.resolve(it) } - .filter { Files.isRegularFile(it) || Files.isSymbolicLink(it) } - staticLog.info("Corda Deterministic Libraries: {}", files.map(Path::getFileName).joinToString()) - - val jars = files.map { it.toUri().toURL() }.toTypedArray() - UserPathSource(jars) - } - } - - private fun createManifestBootstrapSource(config: NodeConfiguration): ApiSource { - val deterministicRt = manifestValue(CORDA_DETERMINISTIC_RUNTIME_ATTR) - if (deterministicRt == null) { - staticLog.warn("{} missing from MANIFEST.MF - will use host JVM for deterministic runtime.", - CORDA_DETERMINISTIC_RUNTIME_ATTR) - return EmptyApi - } - - val bootstrapSource = config.baseDirectory.resolve("djvm").resolve(deterministicRt) - return if (bootstrapSource.isRegularFile()) { - staticLog.info("Deterministic Runtime: {}", bootstrapSource.fileName) - BootstrapClassLoader(bootstrapSource) - } else { - staticLog.warn("NO DETERMINISTIC RUNTIME FOUND - will use host JVM instead.") - EmptyApi - } - } - - private fun createBootstrapSource(config: NodeConfiguration): ApiSource { - val djvm = config.devModeOptions?.djvm - return if (config.devMode && djvm != null) { - djvm.bootstrapSource?.let { BootstrapClassLoader(Paths.get(it)) } ?: EmptyApi - } else if (java.lang.Boolean.getBoolean(CORDA_DJVM)) { - createManifestBootstrapSource(config) - } else { - EmptyApi - } - } - - private fun createCordaSource(config: NodeConfiguration): UserSource? { - val djvm = config.devModeOptions?.djvm - return if (config.devMode && djvm != null) { - if (djvm.cordaSource.isEmpty()) { - null - } else { - UserPathSource(djvm.cordaSource.map { Paths.get(it) }) - } - } else if (java.lang.Boolean.getBoolean(CORDA_DJVM)) { - createManifestCordaSource(config) - } else { - null - } - } } override val log: Logger get() = staticLog diff --git a/node/src/main/kotlin/net/corda/node/internal/djvm/AttachmentFactory.kt b/node/src/main/kotlin/net/corda/node/internal/djvm/AttachmentFactory.kt deleted file mode 100644 index d272a7428e..0000000000 --- a/node/src/main/kotlin/net/corda/node/internal/djvm/AttachmentFactory.kt +++ /dev/null @@ -1,49 +0,0 @@ -package net.corda.node.internal.djvm - -import net.corda.core.contracts.Attachment -import net.corda.core.contracts.ContractAttachment -import net.corda.core.serialization.serialize -import net.corda.djvm.rewiring.SandboxClassLoader -import net.corda.node.djvm.AttachmentBuilder -import java.util.function.Function - -class AttachmentFactory( - classLoader: SandboxClassLoader, - private val taskFactory: Function>, out Function>, - private val sandboxBasicInput: Function, - private val serializer: Serializer -) { - private val sandboxOpenAttachment: Function = classLoader.createForImport( - Function(Attachment::open).andThen(sandboxBasicInput) - ) - - fun toSandbox(attachments: List): Any? { - val builder = taskFactory.apply(AttachmentBuilder::class.java) - for (attachment in attachments) { - builder.apply(generateArgsFor(attachment)) - } - return builder.apply(null) - } - - private fun generateArgsFor(attachment: Attachment): Array { - val signerKeys = serializer.deserialize(attachment.signerKeys.serialize()) - val id = serializer.deserialize(attachment.id.serialize()) - val size = sandboxBasicInput.apply(attachment.size) - return if (attachment is ContractAttachment) { - val underlyingAttachment = attachment.attachment - arrayOf( - serializer.deserialize(underlyingAttachment.signerKeys.serialize()), - size, id, - underlyingAttachment, - sandboxOpenAttachment, - sandboxBasicInput.apply(attachment.contract), - sandboxBasicInput.apply(attachment.additionalContracts.toTypedArray()), - sandboxBasicInput.apply(attachment.uploader), - signerKeys, - sandboxBasicInput.apply(attachment.version) - ) - } else { - arrayOf(signerKeys, size, id, attachment, sandboxOpenAttachment) - } - } -} diff --git a/node/src/main/kotlin/net/corda/node/internal/djvm/CommandFactory.kt b/node/src/main/kotlin/net/corda/node/internal/djvm/CommandFactory.kt deleted file mode 100644 index 3727b1b0b6..0000000000 --- a/node/src/main/kotlin/net/corda/node/internal/djvm/CommandFactory.kt +++ /dev/null @@ -1,17 +0,0 @@ -package net.corda.node.internal.djvm - -import net.corda.node.djvm.CommandBuilder -import java.util.function.Function - -class CommandFactory( - private val taskFactory: Function>, out Function> -) { - fun toSandbox(signers: Any?, commands: Any?, partialMerkleLeafIndices: IntArray?): Any? { - val builder = taskFactory.apply(CommandBuilder::class.java) - return builder.apply(arrayOf( - signers, - commands, - partialMerkleLeafIndices - )) - } -} diff --git a/node/src/main/kotlin/net/corda/node/internal/djvm/ComponentFactory.kt b/node/src/main/kotlin/net/corda/node/internal/djvm/ComponentFactory.kt deleted file mode 100644 index adba2daa13..0000000000 --- a/node/src/main/kotlin/net/corda/node/internal/djvm/ComponentFactory.kt +++ /dev/null @@ -1,43 +0,0 @@ -@file:JvmName("ComponentUtils") -package net.corda.node.internal.djvm - -import net.corda.core.contracts.ComponentGroupEnum -import net.corda.core.crypto.DigestService -import net.corda.core.transactions.ComponentGroup -import net.corda.core.transactions.FilteredComponentGroup -import net.corda.djvm.rewiring.SandboxClassLoader -import net.corda.node.djvm.ComponentBuilder -import java.util.function.Function - -class ComponentFactory( - private val classLoader: SandboxClassLoader, - private val taskFactory: Function>, out Function>, - private val sandboxBasicInput: Function, - private val serializer: Serializer, - private val componentGroups: List -) { - fun toSandbox( - groupType: ComponentGroupEnum, - clazz: Class<*> - ): Any? { - val components = (componentGroups.firstOrNull(groupType::isSameType) ?: return null).components - val componentBytes = Array(components.size) { idx -> components[idx].bytes } - return taskFactory.apply(ComponentBuilder::class.java).apply(arrayOf( - classLoader.createForImport(serializer.deserializerFor(clazz)), - sandboxBasicInput.apply(groupType), - componentBytes - )) - } - - fun calculateLeafIndicesFor(groupType: ComponentGroupEnum, digestService: DigestService): IntArray? { - val componentGroup = componentGroups.firstOrNull(groupType::isSameType) as? FilteredComponentGroup ?: return null - val componentHashes = componentGroup.components.mapIndexed { index, component -> - digestService.componentHash(componentGroup.nonces[index], component) - } - return componentHashes.map { componentGroup.partialMerkleTree.leafIndex(it) }.toIntArray() - } -} - -private fun ComponentGroupEnum.isSameType(group: ComponentGroup): Boolean { - return group.groupIndex == ordinal -} diff --git a/node/src/main/kotlin/net/corda/node/internal/djvm/DeterministicVerifier.kt b/node/src/main/kotlin/net/corda/node/internal/djvm/DeterministicVerifier.kt deleted file mode 100644 index 3263868aa8..0000000000 --- a/node/src/main/kotlin/net/corda/node/internal/djvm/DeterministicVerifier.kt +++ /dev/null @@ -1,147 +0,0 @@ -package net.corda.node.internal.djvm - -import net.corda.core.contracts.CommandData -import net.corda.core.contracts.ComponentGroupEnum.COMMANDS_GROUP -import net.corda.core.contracts.ComponentGroupEnum.OUTPUTS_GROUP -import net.corda.core.contracts.ComponentGroupEnum.SIGNERS_GROUP -import net.corda.core.contracts.TransactionState -import net.corda.core.contracts.TransactionVerificationException -import net.corda.core.crypto.SecureHash -import net.corda.core.internal.TransactionVerifier -import net.corda.core.internal.Verifier -import net.corda.core.internal.getNamesOfClassesImplementing -import net.corda.core.serialization.SerializationCustomSerializer -import net.corda.core.serialization.SerializationWhitelist -import net.corda.core.serialization.serialize -import net.corda.core.transactions.LedgerTransaction -import net.corda.core.utilities.contextLogger -import net.corda.djvm.SandboxConfiguration -import net.corda.djvm.execution.ExecutionSummary -import net.corda.djvm.execution.IsolatedTask -import net.corda.djvm.execution.SandboxException -import net.corda.djvm.messages.Message -import net.corda.djvm.rewiring.SandboxClassLoader -import net.corda.djvm.source.ClassSource -import net.corda.node.djvm.LtxSupplierFactory -import java.util.function.Function -import kotlin.collections.LinkedHashSet - -class DeterministicVerifier( - private val ltx: LedgerTransaction, - private val transactionClassLoader: ClassLoader, - private val sandboxConfiguration: SandboxConfiguration -) : Verifier { - private companion object { - private val logger = contextLogger() - } - - /** - * Read the whitelisted classes without using the [java.util.ServiceLoader] mechanism - * because the whitelists themselves are untrusted. - */ - private fun getSerializationWhitelistNames(classLoader: ClassLoader): Set { - return classLoader.getResources("META-INF/services/${SerializationWhitelist::class.java.name}").asSequence() - .flatMapTo(LinkedHashSet()) { url -> - url.openStream().bufferedReader().useLines { lines -> - // Parse file format, as documented for java.util.ServiceLoader: - // - Remove everything after comment character '#'. - // - Strip whitespace. - // - Ignore empty lines. - lines.map { it.substringBefore('#') }.map(String::trim).filterNot(String::isEmpty).toList() - }.asSequence() - } - } - - override fun verify() { - val customSerializerNames = getNamesOfClassesImplementing(transactionClassLoader, SerializationCustomSerializer::class.java) - val serializationWhitelistNames = getSerializationWhitelistNames(transactionClassLoader) - val result = IsolatedTask(ltx.id.toString(), sandboxConfiguration).run(Function { classLoader -> - (classLoader.parent as? SandboxClassLoader)?.apply { - /** - * We don't need to add either Java APIs or Corda's own classes - * into the external cache because these are already being cached - * more efficiently inside the [SandboxConfiguration]. - * - * The external cache is for this Nodes's CorDapps, where classes - * with the same names may appear in multiple different jars. - */ - externalCaching = false - } - - val taskFactory = classLoader.createRawTaskFactory().compose(classLoader.createSandboxFunction()) - val sandboxBasicInput = classLoader.createBasicInput() - - /** - * Deserialise the [LedgerTransaction] again into something - * that we can execute inside the DJVM's sandbox. - */ - val sandboxTx = ltx.transform { componentGroups, serializedInputs, serializedReferences -> - val serializer = Serializer(classLoader, customSerializerNames, serializationWhitelistNames) - val componentFactory = ComponentFactory( - classLoader, - taskFactory, - sandboxBasicInput, - serializer, - componentGroups - ) - val attachmentFactory = AttachmentFactory( - classLoader, - taskFactory, - sandboxBasicInput, - serializer - ) - - val idData = ltx.id.serialize() - val notaryData = ltx.notary?.serialize() - val timeWindowData = ltx.timeWindow?.serialize() - val privacySaltData = ltx.privacySalt.serialize() - val networkingParametersData = ltx.networkParameters?.serialize() - val digestServiceData = ltx.digestService.serialize() - - val createSandboxTx = taskFactory.apply(LtxSupplierFactory::class.java) - createSandboxTx.apply(arrayOf( - classLoader.createForImport(Function { serializer.deserialize(serializedInputs) }), - componentFactory.toSandbox(OUTPUTS_GROUP, TransactionState::class.java), - CommandFactory(taskFactory).toSandbox( - componentFactory.toSandbox(SIGNERS_GROUP, List::class.java), - componentFactory.toSandbox(COMMANDS_GROUP, CommandData::class.java), - componentFactory.calculateLeafIndicesFor(COMMANDS_GROUP, ltx.digestService) - ), - attachmentFactory.toSandbox(ltx.attachments), - serializer.deserialize(idData), - serializer.deserialize(notaryData), - serializer.deserialize(timeWindowData), - serializer.deserialize(privacySaltData), - serializer.deserialize(networkingParametersData), - classLoader.createForImport(Function { serializer.deserialize(serializedReferences) }), - serializer.deserialize(digestServiceData) - )) - } - - val verifier = taskFactory.apply(TransactionVerifier::class.java) - - // Now execute the contract verifier task within the sandbox... - verifier.apply(sandboxTx) - }) - - with (result.costs) { - logger.info("Verify {} complete: allocations={}, invocations={}, jumps={}, throws={}", - ltx.id, allocations, invocations, jumps, throws) - } - - result.exception?.run { - val sandboxEx = SandboxException( - Message.getMessageFromException(this), - result.identifier, - ClassSource.fromClassName(TransactionVerifier::class.java.name), - ExecutionSummary(result.costs), - this - ) - logger.error("Error validating transaction ${ltx.id}.", sandboxEx) - throw DeterministicVerificationException(ltx.id, sandboxEx.message ?: "", sandboxEx) - } - } -} - -class DeterministicVerificationException(txId: SecureHash, message: String, cause: Throwable) - : TransactionVerificationException(txId, message, cause) diff --git a/node/src/main/kotlin/net/corda/node/internal/djvm/Serializer.kt b/node/src/main/kotlin/net/corda/node/internal/djvm/Serializer.kt deleted file mode 100644 index 32fedf52fa..0000000000 --- a/node/src/main/kotlin/net/corda/node/internal/djvm/Serializer.kt +++ /dev/null @@ -1,71 +0,0 @@ -package net.corda.node.internal.djvm - -import net.corda.core.internal.SerializedStateAndRef -import net.corda.core.serialization.AMQP_ENVELOPE_CACHE_INITIAL_CAPACITY -import net.corda.core.serialization.AMQP_ENVELOPE_CACHE_PROPERTY -import net.corda.core.serialization.DESERIALIZATION_CACHE_PROPERTY -import net.corda.core.serialization.SerializationContext -import net.corda.core.serialization.SerializationFactory -import net.corda.core.serialization.SerializedBytes -import net.corda.core.serialization.serialize -import net.corda.core.utilities.ByteSequence -import net.corda.djvm.rewiring.SandboxClassLoader -import net.corda.node.djvm.ComponentBuilder -import net.corda.serialization.djvm.createSandboxSerializationEnv -import java.util.function.Function - -class Serializer( - private val classLoader: SandboxClassLoader, - customSerializerNames: Set, - serializationWhitelists: Set -) { - private val factory: SerializationFactory - private val context: SerializationContext - - init { - val env = createSandboxSerializationEnv(classLoader, customSerializerNames, serializationWhitelists) - factory = env.serializationFactory - context = env.p2pContext.withProperties(mapOf( - // Duplicate the P2P SerializationContext and give it - // these extra properties, just for this transaction. - AMQP_ENVELOPE_CACHE_PROPERTY to HashMap(AMQP_ENVELOPE_CACHE_INITIAL_CAPACITY), - DESERIALIZATION_CACHE_PROPERTY to HashMap() - )) - } - - /** - * Convert a list of [SerializedStateAndRef] objects into arrays - * of deserialized sandbox objects. We will pass this array into - * [LtxSupplierFactory][net.corda.node.djvm.LtxSupplierFactory] - * to be transformed finally to a list of - * [StateAndRef][net.corda.core.contracts.StateAndRef] objects, - */ - fun deserialize(stateRefs: List): Array> { - return stateRefs.map { - arrayOf(deserialize(it.serializedState), deserialize(it.ref.serialize())) - }.toTypedArray() - } - - /** - * Generate a [Function] that deserializes a [ByteArray] into an instance - * of the given sandbox class. We import this [Function] into the sandbox - * so that [ComponentBuilder] can deserialize objects lazily. - */ - fun deserializerFor(clazz: Class<*>): Function { - val sandboxClass = classLoader.toSandboxClass(clazz) - return Function { bytes -> - bytes?.run { - factory.deserialize(ByteSequence.of(this), sandboxClass, context) - } - } - } - - fun deserializeTo(clazz: Class<*>, bytes: ByteSequence): Any { - val sandboxClass = classLoader.toSandboxClass(clazz) - return factory.deserialize(bytes, sandboxClass, context) - } - - inline fun deserialize(bytes: SerializedBytes?): Any? { - return deserializeTo(T::class.java, bytes ?: return null) - } -} diff --git a/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt b/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt index 3b80d91e33..a5cf742a9e 100644 --- a/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt +++ b/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt @@ -134,8 +134,7 @@ data class DevModeOptions( "Use [NodeConfiguration.disableReloadCheckpointAfterSuspend] instead." ) val disableCheckpointChecker: Boolean = Defaults.disableCheckpointChecker, - val allowCompatibilityZone: Boolean = Defaults.allowCompatibilityZone, - val djvm: DJVMOptions? = null + val allowCompatibilityZone: Boolean = Defaults.allowCompatibilityZone ) { internal object Defaults { val disableCheckpointChecker = false @@ -143,11 +142,6 @@ data class DevModeOptions( } } -data class DJVMOptions( - val bootstrapSource: String?, - val cordaSource: List -) - fun NodeConfiguration.shouldStartSSHDaemon() = this.sshd != null fun NodeConfiguration.shouldStartLocalShell() = !this.noLocalShell && System.console() != null && this.devMode fun NodeConfiguration.shouldInitCrashShell() = shouldStartLocalShell() || shouldStartSSHDaemon() diff --git a/node/src/main/kotlin/net/corda/node/services/config/schema/v1/ConfigSections.kt b/node/src/main/kotlin/net/corda/node/services/config/schema/v1/ConfigSections.kt index c28b1b25c9..6aeb54b1b1 100644 --- a/node/src/main/kotlin/net/corda/node/services/config/schema/v1/ConfigSections.kt +++ b/node/src/main/kotlin/net/corda/node/services/config/schema/v1/ConfigSections.kt @@ -17,7 +17,6 @@ import net.corda.core.internal.notary.NotaryServiceFlow import net.corda.node.services.config.AuthDataSourceType import net.corda.node.services.config.CertChainPolicyConfig import net.corda.node.services.config.CertChainPolicyType -import net.corda.node.services.config.DJVMOptions import net.corda.node.services.config.DevModeOptions import net.corda.node.services.config.FlowOverride import net.corda.node.services.config.FlowOverrideConfig @@ -158,21 +157,10 @@ internal object SecurityConfigurationSpec : Configuration.Specification("DevModeOptions") { private val disableCheckpointChecker by boolean().optional().withDefaultValue(DevModeOptions.Defaults.disableCheckpointChecker) private val allowCompatibilityZone by boolean().optional().withDefaultValue(DevModeOptions.Defaults.allowCompatibilityZone) - private val djvm by nested(DJVMOptionsSpec).optional() - - private object DJVMOptionsSpec : Configuration.Specification("DJVMOptions") { - private val bootstrapSource by string().optional() - private val cordaSource by string().list() - - override fun parseValid(configuration: Config, options: Configuration.Options): Valid { - val config = configuration.withOptions(options) - return valid(DJVMOptions(config[bootstrapSource], config[cordaSource])) - } - } override fun parseValid(configuration: Config, options: Configuration.Options): Valid { val config = configuration.withOptions(options) - return valid(DevModeOptions(config[disableCheckpointChecker], config[allowCompatibilityZone], config[djvm])) + return valid(DevModeOptions(config[disableCheckpointChecker], config[allowCompatibilityZone])) } } diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/DeterministicVerifierFactoryService.kt b/node/src/main/kotlin/net/corda/node/services/transactions/DeterministicVerifierFactoryService.kt deleted file mode 100644 index 5b016d5734..0000000000 --- a/node/src/main/kotlin/net/corda/node/services/transactions/DeterministicVerifierFactoryService.kt +++ /dev/null @@ -1,117 +0,0 @@ -package net.corda.node.services.transactions - -import net.corda.core.internal.Verifier -import net.corda.core.serialization.ConstructorForDeserialization -import net.corda.core.serialization.CordaSerializable -import net.corda.core.serialization.CordaSerializationTransformEnumDefault -import net.corda.core.serialization.CordaSerializationTransformEnumDefaults -import net.corda.core.serialization.CordaSerializationTransformRename -import net.corda.core.serialization.CordaSerializationTransformRenames -import net.corda.core.serialization.DeprecatedConstructorForDeserialization -import net.corda.core.serialization.SerializationContext -import net.corda.core.serialization.SingletonSerializeAsToken -import net.corda.core.transactions.LedgerTransaction -import net.corda.djvm.SandboxConfiguration -import net.corda.djvm.analysis.AnalysisConfiguration -import net.corda.djvm.execution.ExecutionProfile -import net.corda.djvm.rewiring.ByteCode -import net.corda.djvm.rewiring.ByteCodeKey -import net.corda.djvm.source.ApiSource -import net.corda.djvm.source.UserPathSource -import net.corda.djvm.source.UserSource -import net.corda.node.internal.djvm.DeterministicVerifier -import java.net.URL -import java.net.URLClassLoader -import java.util.concurrent.ConcurrentHashMap -import java.util.function.Consumer -import java.util.function.UnaryOperator - -interface VerifierFactoryService : UnaryOperator, AutoCloseable - -class DeterministicVerifierFactoryService( - private val bootstrapSource: ApiSource, - private val cordaSource: UserSource -) : SingletonSerializeAsToken(), VerifierFactoryService { - private val baseSandboxConfiguration: SandboxConfiguration - private val cordappByteCodeCache = ConcurrentHashMap() - - init { - val baseAnalysisConfiguration = AnalysisConfiguration.createRoot( - userSource = cordaSource, - visibleAnnotations = setOf( - CordaSerializable::class.java, - CordaSerializationTransformEnumDefault::class.java, - CordaSerializationTransformEnumDefaults::class.java, - CordaSerializationTransformRename::class.java, - CordaSerializationTransformRenames::class.java, - ConstructorForDeserialization::class.java, - DeprecatedConstructorForDeserialization::class.java - ), - bootstrapSource = bootstrapSource, - overrideClasses = setOf( - /** - * These classes are all duplicated into the sandbox - * without the DJVM modifying their byte-code first. - * The goal is to delegate cryptographic operations - * out to the Node rather than perform them inside - * the sandbox, because this is MUCH FASTER. - */ - sandbox.net.corda.core.crypto.Crypto::class.java.name, - "sandbox.net.corda.core.crypto.DJVM", - "sandbox.net.corda.core.crypto.DJVMPublicKey", - "sandbox.net.corda.core.crypto.internal.ProviderMapKt" - ) - ) - - baseSandboxConfiguration = SandboxConfiguration.createFor( - analysisConfiguration = baseAnalysisConfiguration, - profile = NODE_PROFILE - ) - } - - /** - * Generate sandbox classes for every Corda jar with META-INF/DJVM-preload. - */ - fun generateSandbox(): DeterministicVerifierFactoryService { - baseSandboxConfiguration.preload() - return this - } - - override fun apply(ledgerTransaction: LedgerTransaction): LedgerTransaction { - // Specialise the LedgerTransaction here so that - // contracts are verified inside the DJVM! - return ledgerTransaction.specialise(::createDeterministicVerifier) - } - - private fun createDeterministicVerifier(ltx: LedgerTransaction, serializationContext: SerializationContext): Verifier { - return (serializationContext.deserializationClassLoader as? URLClassLoader)?.let { classLoader -> - DeterministicVerifier(ltx, classLoader, createSandbox(classLoader.urLs)) - } ?: throw IllegalStateException("Unsupported deserialization classloader type") - } - - private fun createSandbox(userSource: Array): SandboxConfiguration { - return baseSandboxConfiguration.createChild(UserPathSource(userSource), Consumer { - it.setExternalCache(cordappByteCodeCache) - }) - } - - override fun close() { - bootstrapSource.use { - cordaSource.close() - } - } - - private companion object { - private val NODE_PROFILE = ExecutionProfile( - allocationCostThreshold = 1024 * 1024 * 1024, - invocationCostThreshold = 100_000_000, - jumpCostThreshold = 500_000_000, - throwCostThreshold = 1_000_000 - ) - } -} - -class BasicVerifierFactoryService : VerifierFactoryService { - override fun apply(ledgerTransaction: LedgerTransaction)= ledgerTransaction - override fun close() {} -} diff --git a/node/src/main/kotlin/sandbox/net/corda/core/crypto/Crypto.kt b/node/src/main/kotlin/sandbox/net/corda/core/crypto/Crypto.kt deleted file mode 100644 index 1045902baa..0000000000 --- a/node/src/main/kotlin/sandbox/net/corda/core/crypto/Crypto.kt +++ /dev/null @@ -1,266 +0,0 @@ -package sandbox.net.corda.core.crypto - -import sandbox.java.lang.Object -import sandbox.java.lang.String -import sandbox.java.lang.doCatch -import sandbox.java.math.BigInteger -import sandbox.java.security.KeyPair -import sandbox.java.security.PrivateKey -import sandbox.java.security.PublicKey -import sandbox.java.util.ArrayList -import sandbox.java.util.List -import sandbox.net.corda.core.crypto.DJVM.fromDJVM -import sandbox.net.corda.core.crypto.DJVM.toDJVM -import sandbox.org.bouncycastle.asn1.x509.AlgorithmIdentifier -import sandbox.org.bouncycastle.asn1.x509.SubjectPublicKeyInfo -import java.security.GeneralSecurityException -import java.security.SignatureException -import java.security.spec.InvalidKeySpecException - -/** - * This is a hand-written "drop-in" replacement for the version of - * [net.corda.core.crypto.Crypto] found inside core-deterministic. - * This class is used in the DJVM sandbox instead of transforming - * the byte-code for [net.corda.core.crypto.Crypto], with the goal - * of not needing to instantiate some EXPENSIVE elliptic curve - * cryptography classes inside every single sandbox. - * - * The downside is that this class MUST manually be kept consistent - * with the DJVM's byte-code rewriting rules. - */ -@Suppress("unused", "unused_parameter", "TooManyFunctions") -object Crypto : Object() { - @JvmField - val RSA_SHA256: SignatureScheme = toDJVM(net.corda.core.crypto.Crypto.RSA_SHA256) - - @JvmField - val ECDSA_SECP256K1_SHA256: SignatureScheme = toDJVM(net.corda.core.crypto.Crypto.ECDSA_SECP256K1_SHA256) - - @JvmField - val ECDSA_SECP256R1_SHA256: SignatureScheme = toDJVM(net.corda.core.crypto.Crypto.ECDSA_SECP256R1_SHA256) - - @JvmField - val EDDSA_ED25519_SHA512: SignatureScheme = toDJVM(net.corda.core.crypto.Crypto.EDDSA_ED25519_SHA512) - - @JvmField - val SPHINCS256_SHA256: SignatureScheme = toDJVM(net.corda.core.crypto.Crypto.SPHINCS256_SHA256) - - @JvmField - val COMPOSITE_KEY: SignatureScheme = toDJVM(net.corda.core.crypto.Crypto.COMPOSITE_KEY) - - @JvmField - val DEFAULT_SIGNATURE_SCHEME = EDDSA_ED25519_SHA512 - - /** - * We can use the unsandboxed versions of [Map] and [List] here - * because the [underlyingSchemes] and [djvmSchemes] fields are - * private and not exposed to the rest of the sandbox. - */ - private val underlyingSchemes: Map - = net.corda.core.crypto.Crypto.supportedSignatureSchemes() - .associateBy(net.corda.core.crypto.SignatureScheme::schemeCodeName) - private val djvmSchemes: Map = listOf( - RSA_SHA256, - ECDSA_SECP256K1_SHA256, - ECDSA_SECP256R1_SHA256, - EDDSA_ED25519_SHA512, - SPHINCS256_SHA256, - COMPOSITE_KEY - ).associateByTo(LinkedHashMap(), SignatureScheme::schemeCodeName) - - private fun findUnderlyingSignatureScheme(signatureScheme: SignatureScheme): net.corda.core.crypto.SignatureScheme { - return net.corda.core.crypto.Crypto.findSignatureScheme(String.fromDJVM(signatureScheme.schemeCodeName)) - } - - private fun PublicKey.toUnderlyingKey(): java.security.PublicKey { - return (this as? DJVMPublicKey ?: throw sandbox.java.lang.fail("Unsupported key ${this::class.java.name}")).underlying - } - - @JvmStatic - fun supportedSignatureSchemes(): List { - val schemes = ArrayList(djvmSchemes.size) - for (scheme in djvmSchemes.values) { - schemes.add(scheme) - } - return schemes - } - - @JvmStatic - fun isSupportedSignatureScheme(signatureScheme: SignatureScheme): Boolean { - return String.fromDJVM(signatureScheme.schemeCodeName) in underlyingSchemes - } - - @JvmStatic - fun findSignatureScheme(schemeCodeName: String): SignatureScheme { - return djvmSchemes[schemeCodeName] - ?: throw IllegalArgumentException("Unsupported key/algorithm for schemeCodeName: $schemeCodeName") - } - - @JvmStatic - fun findSignatureScheme(schemeNumberID: Int): SignatureScheme { - val underlyingScheme = net.corda.core.crypto.Crypto.findSignatureScheme(schemeNumberID) - return findSignatureScheme(String.toDJVM(underlyingScheme.schemeCodeName)) - } - - @JvmStatic - fun findSignatureScheme(key: PublicKey): SignatureScheme { - val underlyingScheme = net.corda.core.crypto.Crypto.findSignatureScheme(key.toUnderlyingKey()) - return findSignatureScheme(String.toDJVM(underlyingScheme.schemeCodeName)) - } - - @JvmStatic - fun findSignatureScheme(algorithm: AlgorithmIdentifier): SignatureScheme { - val underlyingScheme = net.corda.core.crypto.Crypto.findSignatureScheme(fromDJVM(algorithm)) - return findSignatureScheme(String.toDJVM(underlyingScheme.schemeCodeName)) - } - - @JvmStatic - fun findSignatureScheme(key: PrivateKey): SignatureScheme { - throw sandbox.java.lang.failApi("Crypto.findSignatureScheme(PrivateKey)") - } - - @JvmStatic - fun decodePrivateKey(signatureScheme: SignatureScheme, encodedKey: ByteArray): PrivateKey { - throw sandbox.java.lang.failApi("Crypto.decodePrivateKey(SignatureScheme, byte[])") - } - - @JvmStatic - fun decodePublicKey(encodedKey: ByteArray): PublicKey { - val underlying = try { - net.corda.core.crypto.Crypto.decodePublicKey(encodedKey) - } catch (e: InvalidKeySpecException) { - throw sandbox.java.lang.fromDJVM(doCatch(e)) - } - return DJVMPublicKey(underlying) - } - - @JvmStatic - fun decodePublicKey(schemeCodeName: String, encodedKey: ByteArray): PublicKey { - val underlying = try { - net.corda.core.crypto.Crypto.decodePublicKey(String.fromDJVM(schemeCodeName), encodedKey) - } catch (e: InvalidKeySpecException) { - throw sandbox.java.lang.fromDJVM(doCatch(e)) - } - return DJVMPublicKey(underlying) - } - - @JvmStatic - fun decodePublicKey(signatureScheme: SignatureScheme, encodedKey: ByteArray): PublicKey { - return decodePublicKey(signatureScheme.schemeCodeName, encodedKey) - } - - @JvmStatic - fun encodePublicKey(key: java.security.PublicKey): ByteArray { - return key.encoded - } - - @JvmStatic - fun deriveKeyPair(signatureScheme: SignatureScheme, privateKey: PrivateKey, seed: ByteArray): KeyPair { - throw sandbox.java.lang.failApi("Crypto.deriveKeyPair(SignatureScheme, PrivateKey, byte[])") - } - - @JvmStatic - fun deriveKeyPair(privateKey: PrivateKey, seed: ByteArray): KeyPair { - throw sandbox.java.lang.failApi("Crypto.deriveKeyPair(PrivateKey, byte[])") - } - - @JvmStatic - fun deriveKeyPairFromEntropy(signatureScheme: SignatureScheme, entropy: BigInteger): KeyPair { - throw sandbox.java.lang.failApi("Crypto.deriveKeyPairFromEntropy(SignatureScheme, BigInteger)") - } - - @JvmStatic - fun deriveKeyPairFromEntropy(entropy: BigInteger): KeyPair { - throw sandbox.java.lang.failApi("Crypto.deriveKeyPairFromEntropy(BigInteger)") - } - - @JvmStatic - fun doVerify(schemeCodeName: String, publicKey: PublicKey, signatureData: ByteArray, clearData: ByteArray): Boolean { - val underlyingKey = publicKey.toUnderlyingKey() - return try { - net.corda.core.crypto.Crypto.doVerify(String.fromDJVM(schemeCodeName), underlyingKey, signatureData, clearData) - } catch (e: GeneralSecurityException) { - throw sandbox.java.lang.fromDJVM(doCatch(e)) - } - } - - @JvmStatic - fun doVerify(publicKey: PublicKey, signatureData: ByteArray, clearData: ByteArray): Boolean { - val underlyingKey = publicKey.toUnderlyingKey() - return try { - net.corda.core.crypto.Crypto.doVerify(underlyingKey, signatureData, clearData) - } catch (e: GeneralSecurityException) { - throw sandbox.java.lang.fromDJVM(doCatch(e)) - } - } - - @JvmStatic - fun doVerify(signatureScheme: SignatureScheme, publicKey: PublicKey, signatureData: ByteArray, clearData: ByteArray): Boolean { - val underlyingScheme = findUnderlyingSignatureScheme(signatureScheme) - val underlyingKey = publicKey.toUnderlyingKey() - return try { - net.corda.core.crypto.Crypto.doVerify(underlyingScheme, underlyingKey, signatureData, clearData) - } catch (e: GeneralSecurityException) { - throw sandbox.java.lang.fromDJVM(doCatch(e)) - } - } - - @JvmStatic - fun doVerify(txId: SecureHash, transactionSignature: TransactionSignature): Boolean { - throw sandbox.java.lang.failApi("Crypto.doVerify(SecureHash, TransactionSignature)") - } - - @JvmStatic - fun isValid(txId: SecureHash, transactionSignature: TransactionSignature): Boolean { - throw sandbox.java.lang.failApi("Crypto.isValid(SecureHash, TransactionSignature)") - } - - @JvmStatic - fun isValid(publicKey: PublicKey, signatureData: ByteArray, clearData: ByteArray): Boolean { - val underlyingKey = publicKey.toUnderlyingKey() - return try { - net.corda.core.crypto.Crypto.isValid(underlyingKey, signatureData, clearData) - } catch (e: SignatureException) { - throw sandbox.java.lang.fromDJVM(doCatch(e)) - } - } - - @JvmStatic - fun isValid(signatureScheme: SignatureScheme, publicKey: PublicKey, signatureData: ByteArray, clearData: ByteArray): Boolean { - val underlyingScheme = findUnderlyingSignatureScheme(signatureScheme) - val underlyingKey = publicKey.toUnderlyingKey() - return try { - net.corda.core.crypto.Crypto.isValid(underlyingScheme, underlyingKey, signatureData, clearData) - } catch (e: SignatureException) { - throw sandbox.java.lang.fromDJVM(doCatch(e)) - } - } - - @JvmStatic - fun publicKeyOnCurve(signatureScheme: SignatureScheme, publicKey: PublicKey): Boolean { - val underlyingScheme = findUnderlyingSignatureScheme(signatureScheme) - val underlyingKey = publicKey.toUnderlyingKey() - return net.corda.core.crypto.Crypto.publicKeyOnCurve(underlyingScheme, underlyingKey) - } - - @JvmStatic - fun validatePublicKey(key: PublicKey): Boolean { - return net.corda.core.crypto.Crypto.validatePublicKey(key.toUnderlyingKey()) - } - - @JvmStatic - fun toSupportedPublicKey(key: SubjectPublicKeyInfo): PublicKey { - return decodePublicKey(key.encoded) - } - - @JvmStatic - fun toSupportedPublicKey(key: PublicKey): PublicKey { - val underlyingKey = key.toUnderlyingKey() - val supportedKey = net.corda.core.crypto.Crypto.toSupportedPublicKey(underlyingKey) - return if (supportedKey === underlyingKey) { - key - } else { - DJVMPublicKey(supportedKey) - } - } -} diff --git a/node/src/main/kotlin/sandbox/net/corda/core/crypto/SecureHash.kt b/node/src/main/kotlin/sandbox/net/corda/core/crypto/SecureHash.kt deleted file mode 100644 index ac8f733511..0000000000 --- a/node/src/main/kotlin/sandbox/net/corda/core/crypto/SecureHash.kt +++ /dev/null @@ -1,8 +0,0 @@ -package sandbox.net.corda.core.crypto - -/** - * This is a dummy class that implements just enough of [net.corda.core.crypto.SecureHash] - * to allow us to compile [sandbox.net.corda.core.crypto.Crypto]. - */ -@Suppress("unused_parameter") -sealed class SecureHash(bytes: ByteArray) : sandbox.java.lang.Object() diff --git a/node/src/main/kotlin/sandbox/net/corda/core/crypto/SignatureScheme.kt b/node/src/main/kotlin/sandbox/net/corda/core/crypto/SignatureScheme.kt deleted file mode 100644 index dd315252df..0000000000 --- a/node/src/main/kotlin/sandbox/net/corda/core/crypto/SignatureScheme.kt +++ /dev/null @@ -1,26 +0,0 @@ -package sandbox.net.corda.core.crypto - -import sandbox.java.lang.String -import sandbox.java.lang.Integer -import sandbox.java.lang.Object -import sandbox.java.security.spec.AlgorithmParameterSpec -import sandbox.java.util.List -import sandbox.org.bouncycastle.asn1.x509.AlgorithmIdentifier - -/** - * This is a dummy class that implements just enough of [net.corda.core.crypto.SignatureScheme] - * to allow us to compile [sandbox.net.corda.core.crypto.Crypto]. - */ -@Suppress("unused") -class SignatureScheme( - val schemeNumberID: Int, - val schemeCodeName: String, - val signatureOID: AlgorithmIdentifier, - val alternativeOIDs: List, - val providerName: String, - val algorithmName: String, - val signatureName: String, - val algSpec: AlgorithmParameterSpec?, - val keySize: Integer?, - val desc: String -) : Object() diff --git a/node/src/main/kotlin/sandbox/net/corda/core/crypto/TransactionSignature.kt b/node/src/main/kotlin/sandbox/net/corda/core/crypto/TransactionSignature.kt deleted file mode 100644 index 2d3c3e8b90..0000000000 --- a/node/src/main/kotlin/sandbox/net/corda/core/crypto/TransactionSignature.kt +++ /dev/null @@ -1,7 +0,0 @@ -package sandbox.net.corda.core.crypto - -/** - * This is a dummy class that implements just enough of [net.corda.core.crypto.TransactionSignature] - * to allow us to compile [sandbox.net.corda.core.crypto.Crypto]. - */ -class TransactionSignature : sandbox.java.lang.Object() diff --git a/node/src/main/kotlin/sandbox/net/corda/core/crypto/internal/ProviderMap.kt b/node/src/main/kotlin/sandbox/net/corda/core/crypto/internal/ProviderMap.kt deleted file mode 100644 index 5a0ebb4065..0000000000 --- a/node/src/main/kotlin/sandbox/net/corda/core/crypto/internal/ProviderMap.kt +++ /dev/null @@ -1,8 +0,0 @@ -package sandbox.net.corda.core.crypto.internal - -/** - * THIS FILE IS DELIBERATELY EMPTY, APART FROM A SINGLE DUMMY VALUE. - * KOTLIN WILL NOT CREATE A CLASS IF THIS FILE IS COMPLETELY EMPTY. - */ -@Suppress("unused") -private const val DUMMY_VALUE = 0 diff --git a/serialization-deterministic/README.md b/serialization-deterministic/README.md deleted file mode 100644 index abd4a19f0c..0000000000 --- a/serialization-deterministic/README.md +++ /dev/null @@ -1,2 +0,0 @@ -## corda-serialization-deterministic. -This artifact is a deterministic subset of the binary contents of `corda-serialization`. diff --git a/serialization-deterministic/build.gradle b/serialization-deterministic/build.gradle deleted file mode 100644 index 7822eb3b23..0000000000 --- a/serialization-deterministic/build.gradle +++ /dev/null @@ -1,228 +0,0 @@ -import net.corda.gradle.jarfilter.JarFilterTask -import net.corda.gradle.jarfilter.MetaFixerTask -import proguard.gradle.ProGuardTask -import static org.gradle.api.JavaVersion.VERSION_1_8 - -plugins { - id 'org.jetbrains.kotlin.jvm' - id 'net.corda.plugins.publish-utils' - id 'com.jfrog.artifactory' - id 'java-library' - id 'idea' -} -apply from: "${rootProject.projectDir}/deterministic.gradle" - -description 'Corda serialization (deterministic)' - -evaluationDependsOn(":serialization") - -// required by DJVM and Avian JVM (for running inside the SGX enclave) which only supports Java 8. -targetCompatibility = VERSION_1_8 - -def javaHome = System.getProperty('java.home') -def jarBaseName = "corda-${project.name}".toString() - -configurations { - deterministicLibraries { - canBeConsumed = false - extendsFrom implementation - } - deterministicArtifacts.extendsFrom deterministicLibraries -} - -dependencies { - compileOnly project(':serialization') - - // Configure these by hand. It should be a minimal subset of dependencies, - // and without any obviously non-deterministic ones such as Hibernate. - - // These dependencies will become "compile" scoped in our published POM. - // See publish.dependenciesFrom.defaultScope. - deterministicLibraries project(path: ':core-deterministic', configuration: 'deterministicArtifacts') - deterministicLibraries "org.apache.qpid:proton-j:$protonj_version" - - // These "implementation" dependencies will become "runtime" scoped in our published POM. - implementation "org.iq80.snappy:snappy:$snappy_version" - implementation "com.google.guava:guava:$guava_version" -} - -tasks.named('jar', Jar) { - archiveBaseName = 'DOES-NOT-EXIST' - // Don't build a jar here because it would be the wrong one. - // The jar we really want will be built by the metafix task. - enabled = false -} - -def serializationJarTask = project(':serialization').tasks.named('jar', Jar) -def originalJar = serializationJarTask.map { it.outputs.files.singleFile } - -def patchSerialization = tasks.register('patchSerialization', Zip) { - dependsOn serializationJarTask - destinationDirectory = layout.buildDirectory.dir('source-libs') - metadataCharset 'UTF-8' - archiveClassifier = 'transient' - archiveExtension = 'jar' - - from(compileKotlin) - from(processResources) - from(zipTree(originalJar)) { - exclude 'net/corda/serialization/internal/AttachmentsClassLoaderBuilder*' - exclude 'net/corda/serialization/internal/ByteBufferStreams*' - exclude 'net/corda/serialization/internal/DefaultWhitelist*' - exclude 'net/corda/serialization/internal/amqp/AMQPSerializerFactories*' - exclude 'net/corda/serialization/internal/amqp/AMQPStreams*' - exclude 'net/corda/serialization/internal/amqp/AMQPSerializationThreadContext*' - exclude 'net/corda/serialization/internal/model/DefaultCacheProvider*' - } - - reproducibleFileOrder = true - includeEmptyDirs = false -} - -def predeterminise = tasks.register('predeterminise', ProGuardTask) { - dependsOn project(':core-deterministic').tasks.named('assemble') - injars patchSerialization - outjars file("$buildDir/proguard/pre-deterministic-${project.version}.jar") - - if (JavaVersion.current().isJava9Compatible()) { - libraryjars "$javaHome/jmods" - } else { - libraryjars file("$javaHome/lib/rt.jar") - libraryjars file("$javaHome/lib/jce.jar") - libraryjars file("$javaHome/lib/ext/sunec.jar") - } - configurations.compileClasspath.forEach { - if (originalJar != it) { - libraryjars it, filter: '!META-INF/versions/**' - } - } - - keepattributes '*' - keepdirectories - dontpreverify - dontobfuscate - dontoptimize - dontnote - printseeds - verbose - - keep '@net.corda.core.KeepForDJVM class * { *; }', includedescriptorclasses:true - keepclassmembers 'class net.corda.serialization.** { public synthetic ; }' -} - -def jarFilter = tasks.register('jarFilter', JarFilterTask) { - jars predeterminise - annotations { - forDelete = [ - "net.corda.core.DeleteForDJVM" - ] - forStub = [ - "net.corda.core.StubOutForDJVM" - ] - forRemove = [ - "co.paralleluniverse.fibers.Suspendable" - ] - forSanitise = [ - "net.corda.core.DeleteForDJVM" - ] - } -} - -def determinise = tasks.register('determinise', ProGuardTask) { - injars jarFilter - outjars file("$buildDir/proguard/$jarBaseName-${project.version}.jar") - - if (JavaVersion.current().isJava9Compatible()) { - libraryjars "$javaHome/jmods" - } else { - libraryjars file("$javaHome/lib/rt.jar") - libraryjars file("$javaHome/lib/jce.jar") - } - configurations.deterministicLibraries.forEach { - libraryjars it, filter: '!META-INF/versions/**' - } - - // Analyse the JAR for dead code, and remove (some of) it. - optimizations 'code/removal/simple,code/removal/advanced' - printconfiguration - - keepattributes '*' - keepdirectories - dontobfuscate - dontnote - printseeds - verbose - - keep '@net.corda.core.KeepForDJVM class * { *; }', includedescriptorclasses:true - keepclassmembers 'class net.corda.serialization.** { public synthetic ; }' -} - -def checkDeterminism = tasks.register('checkDeterminism', ProGuardTask) - -def metafix = tasks.register('metafix', MetaFixerTask) { - outputDir = layout.buildDirectory.dir('libs') - jars determinise - suffix "" - - // Strip timestamps from the JAR to make it reproducible. - preserveTimestamps = false - finalizedBy checkDeterminism -} - -checkDeterminism.configure { - dependsOn jdkTask - injars metafix - - libraryjars deterministic_rt_jar - - configurations.deterministicLibraries.forEach { - libraryjars it, filter: '!META-INF/versions/**' - } - - keepattributes '*' - dontpreverify - dontobfuscate - dontoptimize - verbose - - keep 'class *' -} - -defaultTasks "determinise" -determinise.configure { - finalizedBy metafix -} -tasks.named('assemble') { - dependsOn checkDeterminism -} - -def deterministicJar = metafix.map { it.outputs.files.singleFile } -artifacts { - deterministicArtifacts deterministicJar - publish deterministicJar -} - -tasks.named('sourceJar', Jar) { - from 'README.md' - include 'README.md' -} - -tasks.named('javadocJar', Jar) { - from 'README.md' - include 'README.md' -} - -publish { - dependenciesFrom(configurations.deterministicArtifacts) { - defaultScope = 'compile' - } - name jarBaseName -} - -idea { - module { - if (project.hasProperty("deterministic_idea_sdk")) { - jdkName project.property("deterministic_idea_sdk") as String - } - } -} diff --git a/serialization-deterministic/src/main/kotlin/net/corda/serialization/internal/ByteBufferStreams.kt b/serialization-deterministic/src/main/kotlin/net/corda/serialization/internal/ByteBufferStreams.kt deleted file mode 100644 index 4a93bcb5f8..0000000000 --- a/serialization-deterministic/src/main/kotlin/net/corda/serialization/internal/ByteBufferStreams.kt +++ /dev/null @@ -1,15 +0,0 @@ -@file:JvmName("ByteBufferStreams") -package net.corda.serialization.internal - -const val DEFAULT_BYTEBUFFER_SIZE = 64 * 1024 - -/** - * Drop-in replacement for [byteArrayOutput] in the serialization module. - * This version does not use a [net.corda.core.internal.LazyPool]. - */ -internal fun byteArrayOutput(task: (ByteBufferOutputStream) -> T): ByteArray { - return ByteBufferOutputStream(DEFAULT_BYTEBUFFER_SIZE).let { underlying -> - task(underlying) - underlying.toByteArray() // Must happen after close, to allow ZIP footer to be written for example. - } -} diff --git a/serialization-deterministic/src/main/kotlin/net/corda/serialization/internal/DefaultWhitelist.kt b/serialization-deterministic/src/main/kotlin/net/corda/serialization/internal/DefaultWhitelist.kt deleted file mode 100644 index b1435f6c2f..0000000000 --- a/serialization-deterministic/src/main/kotlin/net/corda/serialization/internal/DefaultWhitelist.kt +++ /dev/null @@ -1,10 +0,0 @@ -package net.corda.serialization.internal - -import net.corda.core.serialization.SerializationWhitelist - -/** - * The DJVM does not need whitelisting, by definition. - */ -object DefaultWhitelist : SerializationWhitelist { - override val whitelist: List> get() = emptyList() -} diff --git a/serialization-deterministic/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPSerializationThreadContext.kt b/serialization-deterministic/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPSerializationThreadContext.kt deleted file mode 100644 index 723dda762d..0000000000 --- a/serialization-deterministic/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPSerializationThreadContext.kt +++ /dev/null @@ -1,6 +0,0 @@ -@file:JvmName("AMQPSerializationThreadContext") -package net.corda.serialization.internal.amqp - -fun getContextClassLoader(): ClassLoader { - return ClassLoader.getSystemClassLoader() -} diff --git a/serialization-deterministic/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPSerializerFactories.kt b/serialization-deterministic/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPSerializerFactories.kt deleted file mode 100644 index 19a59bc9ba..0000000000 --- a/serialization-deterministic/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPSerializerFactories.kt +++ /dev/null @@ -1,34 +0,0 @@ -@file:JvmName("AMQPSerializerFactories") -package net.corda.serialization.internal.amqp - -import net.corda.core.serialization.ClassWhitelist -import net.corda.core.serialization.SerializationContext -import net.corda.serialization.internal.carpenter.ClassCarpenter -import net.corda.serialization.internal.carpenter.Schema - -/** - * Creates a [SerializerFactoryFactory] suitable for the DJVM, - * i.e. one without a [ClassCarpenter] implementation. - */ -@Suppress("UNUSED") -fun createSerializerFactoryFactory(): SerializerFactoryFactory = DeterministicSerializerFactoryFactory() - -/** - * Creates a [ClassCarpenter] suitable for the DJVM, i.e. one that doesn't work. - */ -fun createClassCarpenter(context: SerializationContext): ClassCarpenter = DummyClassCarpenter(context.whitelist, context.deserializationClassLoader) - -private class DeterministicSerializerFactoryFactory : SerializerFactoryFactory { - override fun make(context: SerializationContext) = - SerializerFactoryBuilder.build( - whitelist = context.whitelist, - classCarpenter = createClassCarpenter(context)) -} - -private class DummyClassCarpenter( - override val whitelist: ClassWhitelist, - override val classloader: ClassLoader -) : ClassCarpenter { - override fun build(schema: Schema): Class<*> - = throw UnsupportedOperationException("ClassCarpentry not supported") -} \ No newline at end of file diff --git a/serialization-deterministic/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPStreams.kt b/serialization-deterministic/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPStreams.kt deleted file mode 100644 index 1f4d9af591..0000000000 --- a/serialization-deterministic/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPStreams.kt +++ /dev/null @@ -1,40 +0,0 @@ -@file:JvmName("AMQPStreams") -package net.corda.serialization.internal.amqp - -import net.corda.serialization.internal.ByteBufferInputStream -import net.corda.serialization.internal.ByteBufferOutputStream -import net.corda.serialization.internal.DEFAULT_BYTEBUFFER_SIZE -import java.io.InputStream -import java.io.OutputStream -import java.nio.ByteBuffer - -/** - * Drop-in replacement for [InputStream.asByteBuffer] in the serialization module. - * This version does not use a [net.corda.core.internal.LazyPool]. - */ -fun InputStream.asByteBuffer(): ByteBuffer { - return if (this is ByteBufferInputStream) { - byteBuffer // BBIS has no other state, so this is perfectly safe. - } else { - ByteBuffer.wrap(ByteBufferOutputStream(DEFAULT_BYTEBUFFER_SIZE).let { - copyTo(it) - it.toByteArray() - }) - } -} - -/** - * Drop-in replacement for [OutputStream.alsoAsByteBuffer] in the serialization module. - * This version does not use a [net.corda.core.internal.LazyPool]. - */ -fun OutputStream.alsoAsByteBuffer(remaining: Int, task: (ByteBuffer) -> T): T { - return if (this is ByteBufferOutputStream) { - alsoAsByteBuffer(remaining, task) - } else { - ByteBufferOutputStream(DEFAULT_BYTEBUFFER_SIZE).let { - val result = it.alsoAsByteBuffer(remaining, task) - it.copyTo(this) - result - } - } -} diff --git a/serialization-deterministic/src/main/kotlin/net/corda/serialization/internal/model/DefaultCacheProvider.kt b/serialization-deterministic/src/main/kotlin/net/corda/serialization/internal/model/DefaultCacheProvider.kt deleted file mode 100644 index 022c045e87..0000000000 --- a/serialization-deterministic/src/main/kotlin/net/corda/serialization/internal/model/DefaultCacheProvider.kt +++ /dev/null @@ -1,9 +0,0 @@ -package net.corda.serialization.internal.model - -/** - * We can't have [ConcurrentHashMap]s in the DJVM, so it must supply its own version of this object which returns - * plain old [MutableMap]s instead. - */ -object DefaultCacheProvider { - fun createCache(): MutableMap = mutableMapOf() -} \ No newline at end of file diff --git a/serialization-deterministic/src/main/resources/META-INF/DJVM-preload b/serialization-deterministic/src/main/resources/META-INF/DJVM-preload deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/serialization-djvm/build.gradle b/serialization-djvm/build.gradle deleted file mode 100644 index 8e8870398e..0000000000 --- a/serialization-djvm/build.gradle +++ /dev/null @@ -1,83 +0,0 @@ -plugins { - id 'org.jetbrains.kotlin.jvm' - id 'net.corda.plugins.publish-utils' - id 'com.jfrog.artifactory' - id 'java-library' - id 'idea' -} - -// The DJVM only supports Java 8 byte-code, so the tests must -// be compiled for Java 8. The main artifact is only compiled -// for Java 8 because it belongs to "Open Core". -apply from: "${rootProject.projectDir}/java8.gradle" - -description 'Serialization support for the DJVM' - -configurations { - sandboxTesting { - canBeConsumed = false - } - jdkRt { - canBeConsumed = false - } -} - -dependencies { - api project(':core') - api project(':serialization') - api "net.corda.djvm:corda-djvm:$djvm_version" - api 'org.jetbrains.kotlin:kotlin-stdlib-jdk8' - implementation 'org.jetbrains.kotlin:kotlin-reflect' - implementation(project(':serialization-djvm:deserializers')) { - transitive = false - } - - testImplementation "org.junit.jupiter:junit-jupiter-api:$junit_jupiter_version" - testImplementation "org.junit.jupiter:junit-jupiter-params:$junit_jupiter_version" - testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junit_jupiter_version" - - // Test utilities - testImplementation "org.assertj:assertj-core:$assertj_version" - testRuntimeOnly "org.apache.logging.log4j:log4j-slf4j-impl:$log4j_version" - jdkRt "net.corda:deterministic-rt:$deterministic_rt_version" - - // The DJVM will need this classpath to run the unit tests. - sandboxTesting files(sourceSets.getByName("test").output) - sandboxTesting project(':serialization-djvm:deserializers') - sandboxTesting project(path: ':serialization-deterministic', configuration: 'deterministicArtifacts') - sandboxTesting "org.slf4j:slf4j-nop:$slf4j_version" -} - -jar { - archiveBaseName = 'corda-serialization-djvm' - archiveClassifier = '' - manifest { - attributes('Automatic-Module-Name': 'net.corda.serialization.djvm') - attributes('Sealed': true) - } -} - -tasks.withType(Javadoc).configureEach { - // We have no public or protected Java classes to document. - enabled = false -} - -tasks.withType(Test).configureEach { - useJUnitPlatform() - systemProperty 'deterministic-rt.path', configurations.jdkRt.asPath - systemProperty 'sandbox-libraries.path', configurations.sandboxTesting.asPath - - // Configure the host timezone to match the DJVM's. - systemProperty 'user.timezone', 'UTC' -} - -publish { - name jar.archiveBaseName -} - -idea { - module { - downloadJavadoc = true - downloadSources = true - } -} diff --git a/serialization-djvm/deserializers/build.gradle b/serialization-djvm/deserializers/build.gradle deleted file mode 100644 index d98513c8d5..0000000000 --- a/serialization-djvm/deserializers/build.gradle +++ /dev/null @@ -1,40 +0,0 @@ -plugins { - id 'org.jetbrains.kotlin.jvm' - id 'net.corda.plugins.publish-utils' - id 'com.jfrog.artifactory' - id 'java-library' - id 'idea' -} -apply from: "${rootProject.projectDir}/deterministic.gradle" - -description 'Deserializers for the DJVM' - -dependencies { - api project(path: ':core-deterministic', configuration: 'deterministicArtifacts') - api project(path: ':serialization-deterministic', configuration: 'deterministicArtifacts') - api 'org.jetbrains.kotlin:kotlin-stdlib-jdk8' -} - -jar { - archiveBaseName = 'corda-deserializers-djvm' - archiveClassifier = '' - manifest { - attributes('Automatic-Module-Name': 'net.corda.serialization.djvm.deserializers') - attributes('Sealed': true) - } -} - -publish { - name jar.archiveBaseName.get() -} - -idea { - module { - downloadJavadoc = true - downloadSources = true - - if (project.hasProperty("deterministic_idea_sdk")) { - jdkName project.property("deterministic_idea_sdk") as String - } - } -} diff --git a/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/BitSetDeserializer.kt b/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/BitSetDeserializer.kt deleted file mode 100644 index 718018e8b5..0000000000 --- a/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/BitSetDeserializer.kt +++ /dev/null @@ -1,11 +0,0 @@ -package net.corda.serialization.djvm.deserializers - -import net.corda.serialization.internal.amqp.custom.BitSetSerializer.BitSetProxy -import java.util.BitSet -import java.util.function.Function - -class BitSetDeserializer : Function { - override fun apply(proxy: BitSetProxy): BitSet { - return BitSet.valueOf(proxy.bytes) - } -} diff --git a/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/CertPathDeserializer.kt b/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/CertPathDeserializer.kt deleted file mode 100644 index 04d03a2109..0000000000 --- a/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/CertPathDeserializer.kt +++ /dev/null @@ -1,13 +0,0 @@ -package net.corda.serialization.djvm.deserializers - -import net.corda.serialization.internal.amqp.custom.CertPathSerializer.CertPathProxy -import java.security.cert.CertPath -import java.security.cert.CertificateFactory -import java.util.function.Function - -class CertPathDeserializer : Function { - override fun apply(proxy: CertPathProxy): CertPath { - val factory = CertificateFactory.getInstance(proxy.type) - return factory.generateCertPath(proxy.encoded.inputStream()) - } -} diff --git a/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/CheckEnum.kt b/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/CheckEnum.kt deleted file mode 100644 index 5734e1b3ba..0000000000 --- a/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/CheckEnum.kt +++ /dev/null @@ -1,9 +0,0 @@ -package net.corda.serialization.djvm.deserializers - -import java.util.function.Predicate - -class CheckEnum : Predicate> { - override fun test(clazz: Class<*>): Boolean { - return clazz.isEnum - } -} diff --git a/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/ClassDeserializer.kt b/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/ClassDeserializer.kt deleted file mode 100644 index 6f8c03a33c..0000000000 --- a/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/ClassDeserializer.kt +++ /dev/null @@ -1,11 +0,0 @@ -package net.corda.serialization.djvm.deserializers - -import net.corda.serialization.internal.amqp.custom.ClassSerializer.ClassProxy - -import java.util.function.Function - -class ClassDeserializer : Function> { - override fun apply(proxy: ClassProxy): Class<*> { - return Class.forName(proxy.className) - } -} diff --git a/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/CorDappCustomDeserializer.kt b/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/CorDappCustomDeserializer.kt deleted file mode 100644 index e32ef4d52c..0000000000 --- a/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/CorDappCustomDeserializer.kt +++ /dev/null @@ -1,10 +0,0 @@ -package net.corda.serialization.djvm.deserializers - -import net.corda.core.serialization.SerializationCustomSerializer -import java.util.function.Function - -class CorDappCustomDeserializer(private val serializer: SerializationCustomSerializer) : Function { - override fun apply(input: Any?): Any? { - return serializer.fromProxy(input) - } -} diff --git a/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/CreateCollection.kt b/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/CreateCollection.kt deleted file mode 100644 index be69a73970..0000000000 --- a/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/CreateCollection.kt +++ /dev/null @@ -1,54 +0,0 @@ -package net.corda.serialization.djvm.deserializers - -import net.corda.core.utilities.NonEmptySet -import java.util.Collections.unmodifiableCollection -import java.util.Collections.unmodifiableList -import java.util.Collections.unmodifiableNavigableSet -import java.util.Collections.unmodifiableSet -import java.util.Collections.unmodifiableSortedSet -import java.util.NavigableSet -import java.util.SortedSet -import java.util.TreeSet -import java.util.function.Function - -class CreateCollection : Function, Collection> { - private val concreteConstructors: Map>, (Array) -> Collection> = mapOf( - List::class.java to ::createList, - Set::class.java to ::createSet, - SortedSet::class.java to ::createSortedSet, - NavigableSet::class.java to ::createNavigableSet, - Collection::class.java to ::createCollection, - NonEmptySet::class.java to ::createNonEmptySet - ) - - private fun createList(values: Array): List { - return unmodifiableList(values.toCollection(ArrayList())) - } - - private fun createSet(values: Array): Set { - return unmodifiableSet(values.toCollection(LinkedHashSet())) - } - - private fun createSortedSet(values: Array): SortedSet { - return unmodifiableSortedSet(values.toCollection(TreeSet())) - } - - private fun createNavigableSet(values: Array): NavigableSet { - return unmodifiableNavigableSet(values.toCollection(TreeSet())) - } - - private fun createCollection(values: Array): Collection { - return unmodifiableCollection(values.toCollection(ArrayList())) - } - - private fun createNonEmptySet(values: Array): NonEmptySet { - return NonEmptySet.copyOf(values.toCollection(ArrayList())) - } - - @Suppress("unchecked_cast") - override fun apply(inputs: Array): Collection { - val collectionClass = inputs[0] as Class> - val args = inputs[1] as Array - return concreteConstructors[collectionClass]?.invoke(args)!! - } -} diff --git a/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/CreateCurrency.kt b/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/CreateCurrency.kt deleted file mode 100644 index 0fc21d8750..0000000000 --- a/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/CreateCurrency.kt +++ /dev/null @@ -1,10 +0,0 @@ -package net.corda.serialization.djvm.deserializers - -import java.util.Currency -import java.util.function.Function - -class CreateCurrency : Function { - override fun apply(currencyCode: String): Currency { - return Currency.getInstance(currencyCode) - } -} diff --git a/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/CreateMap.kt b/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/CreateMap.kt deleted file mode 100644 index 6759bc8d2c..0000000000 --- a/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/CreateMap.kt +++ /dev/null @@ -1,54 +0,0 @@ -package net.corda.serialization.djvm.deserializers - -import java.util.Collections.unmodifiableMap -import java.util.Collections.unmodifiableNavigableMap -import java.util.Collections.unmodifiableSortedMap -import java.util.EnumMap -import java.util.NavigableMap -import java.util.SortedMap -import java.util.TreeMap -import java.util.function.Function - -class CreateMap : Function, Map> { - private val concreteConstructors: Map>, (Array>) -> Map> = mapOf( - Map::class.java to ::createMap, - SortedMap::class.java to ::createSortedMap, - LinkedHashMap::class.java to ::createLinkedHashMap, - NavigableMap::class.java to ::createNavigableMap, - TreeMap::class.java to ::createTreeMap, - EnumMap::class.java to ::createEnumMap - ) - - private fun createMap(values: Array>): Map { - return unmodifiableMap(values.associate { it[0] to it[1] }) - } - - private fun createSortedMap(values: Array>): SortedMap { - return unmodifiableSortedMap(createTreeMap(values)) - } - - private fun createNavigableMap(values: Array>): NavigableMap { - return unmodifiableNavigableMap(createTreeMap(values)) - } - - private fun createLinkedHashMap(values: Array>): LinkedHashMap { - return values.associateTo(LinkedHashMap()) { it[0] to it[1] } - } - - private fun createTreeMap(values: Array>): TreeMap { - return values.associateTo(TreeMap()) { it[0] to it[1] } - } - - private fun createEnumMap(values: Array>): Map { - val map = values.associate { it[0] to it[1] } - @Suppress("unchecked_cast") - return EnumMap(map as Map) as Map - } - - @Suppress("unchecked_cast") - override fun apply(inputs: Array): Map { - val mapClass = inputs[0] as Class> - val args = inputs[1] as Array> - return concreteConstructors[mapClass]?.invoke(args)!! - } -} diff --git a/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/Decimal128Deserializer.kt b/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/Decimal128Deserializer.kt deleted file mode 100644 index 6fec4a512f..0000000000 --- a/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/Decimal128Deserializer.kt +++ /dev/null @@ -1,10 +0,0 @@ -package net.corda.serialization.djvm.deserializers - -import org.apache.qpid.proton.amqp.Decimal128 -import java.util.function.Function - -class Decimal128Deserializer : Function { - override fun apply(underlying: LongArray): Decimal128 { - return Decimal128(underlying[0], underlying[1]) - } -} diff --git a/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/Decimal32Deserializer.kt b/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/Decimal32Deserializer.kt deleted file mode 100644 index cdf0ac17d9..0000000000 --- a/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/Decimal32Deserializer.kt +++ /dev/null @@ -1,10 +0,0 @@ -package net.corda.serialization.djvm.deserializers - -import org.apache.qpid.proton.amqp.Decimal32 -import java.util.function.Function - -class Decimal32Deserializer : Function { - override fun apply(underlying: IntArray): Decimal32 { - return Decimal32(underlying[0]) - } -} diff --git a/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/Decimal64Deserializer.kt b/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/Decimal64Deserializer.kt deleted file mode 100644 index b481278218..0000000000 --- a/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/Decimal64Deserializer.kt +++ /dev/null @@ -1,10 +0,0 @@ -package net.corda.serialization.djvm.deserializers - -import org.apache.qpid.proton.amqp.Decimal64 -import java.util.function.Function - -class Decimal64Deserializer : Function { - override fun apply(underlying: LongArray): Decimal64 { - return Decimal64(underlying[0]) - } -} diff --git a/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/DescribeEnum.kt b/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/DescribeEnum.kt deleted file mode 100644 index 4e0303f897..0000000000 --- a/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/DescribeEnum.kt +++ /dev/null @@ -1,9 +0,0 @@ -package net.corda.serialization.djvm.deserializers - -import java.util.function.Function - -class DescribeEnum : Function, Array> { - override fun apply(enumClass: Class<*>): Array { - return enumClass.enumConstants - } -} diff --git a/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/DurationDeserializer.kt b/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/DurationDeserializer.kt deleted file mode 100644 index e75d197a76..0000000000 --- a/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/DurationDeserializer.kt +++ /dev/null @@ -1,11 +0,0 @@ -package net.corda.serialization.djvm.deserializers - -import net.corda.serialization.internal.amqp.custom.DurationSerializer.DurationProxy -import java.time.Duration -import java.util.function.Function - -class DurationDeserializer : Function { - override fun apply(proxy: DurationProxy): Duration { - return Duration.ofSeconds(proxy.seconds, proxy.nanos.toLong()) - } -} diff --git a/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/EnumSetDeserializer.kt b/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/EnumSetDeserializer.kt deleted file mode 100644 index 4020c3cd3b..0000000000 --- a/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/EnumSetDeserializer.kt +++ /dev/null @@ -1,16 +0,0 @@ -package net.corda.serialization.djvm.deserializers - -import net.corda.core.internal.uncheckedCast -import net.corda.serialization.internal.amqp.custom.EnumSetSerializer.EnumSetProxy -import java.util.EnumSet -import java.util.function.Function - -class EnumSetDeserializer : Function> { - override fun apply(proxy: EnumSetProxy): EnumSet<*> { - return if (proxy.elements.isEmpty()) { - EnumSet.noneOf(uncheckedCast, Class>(proxy.clazz)) - } else { - EnumSet.copyOf(uncheckedCast, List>(proxy.elements)) - } - } -} diff --git a/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/GetEnumNames.kt b/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/GetEnumNames.kt deleted file mode 100644 index 5e60f530b4..0000000000 --- a/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/GetEnumNames.kt +++ /dev/null @@ -1,9 +0,0 @@ -package net.corda.serialization.djvm.deserializers - -import java.util.function.Function - -class GetEnumNames : Function>, Array> { - override fun apply(enumValues: Array>): Array { - return enumValues.map(Enum<*>::name).toTypedArray() - } -} diff --git a/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/InputStreamDeserializer.kt b/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/InputStreamDeserializer.kt deleted file mode 100644 index 1aabb9fbe4..0000000000 --- a/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/InputStreamDeserializer.kt +++ /dev/null @@ -1,11 +0,0 @@ -package net.corda.serialization.djvm.deserializers - -import java.io.ByteArrayInputStream -import java.io.InputStream -import java.util.function.Function - -class InputStreamDeserializer : Function { - override fun apply(bytes: ByteArray): InputStream? { - return ByteArrayInputStream(bytes) - } -} diff --git a/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/InstantDeserializer.kt b/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/InstantDeserializer.kt deleted file mode 100644 index bd5e61810b..0000000000 --- a/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/InstantDeserializer.kt +++ /dev/null @@ -1,11 +0,0 @@ -package net.corda.serialization.djvm.deserializers - -import net.corda.serialization.internal.amqp.custom.InstantSerializer.InstantProxy -import java.time.Instant -import java.util.function.Function - -class InstantDeserializer : Function { - override fun apply(proxy: InstantProxy): Instant { - return Instant.ofEpochSecond(proxy.epochSeconds, proxy.nanos.toLong()) - } -} diff --git a/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/JustForCasting.kt b/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/JustForCasting.kt deleted file mode 100644 index 2eb1b07acf..0000000000 --- a/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/JustForCasting.kt +++ /dev/null @@ -1,6 +0,0 @@ -package net.corda.serialization.djvm.deserializers - -@Suppress("unused") -enum class JustForCasting { - UNUSED -} diff --git a/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/LocalDateDeserializer.kt b/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/LocalDateDeserializer.kt deleted file mode 100644 index 5da4fb3b83..0000000000 --- a/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/LocalDateDeserializer.kt +++ /dev/null @@ -1,11 +0,0 @@ -package net.corda.serialization.djvm.deserializers - -import net.corda.serialization.internal.amqp.custom.LocalDateSerializer.LocalDateProxy -import java.time.LocalDate -import java.util.function.Function - -class LocalDateDeserializer : Function { - override fun apply(proxy: LocalDateProxy): LocalDate { - return LocalDate.of(proxy.year, proxy.month.toInt(), proxy.day.toInt()) - } -} diff --git a/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/LocalDateTimeDeserializer.kt b/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/LocalDateTimeDeserializer.kt deleted file mode 100644 index 2123dc81e4..0000000000 --- a/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/LocalDateTimeDeserializer.kt +++ /dev/null @@ -1,11 +0,0 @@ -package net.corda.serialization.djvm.deserializers - -import net.corda.serialization.internal.amqp.custom.LocalDateTimeSerializer.LocalDateTimeProxy -import java.time.LocalDateTime -import java.util.function.Function - -class LocalDateTimeDeserializer : Function { - override fun apply(proxy: LocalDateTimeProxy): LocalDateTime { - return LocalDateTime.of(proxy.date, proxy.time) - } -} diff --git a/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/LocalTimeDeserializer.kt b/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/LocalTimeDeserializer.kt deleted file mode 100644 index e0d7c76d2a..0000000000 --- a/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/LocalTimeDeserializer.kt +++ /dev/null @@ -1,11 +0,0 @@ -package net.corda.serialization.djvm.deserializers - -import net.corda.serialization.internal.amqp.custom.LocalTimeSerializer.LocalTimeProxy -import java.time.LocalTime -import java.util.function.Function - -class LocalTimeDeserializer : Function { - override fun apply(proxy: LocalTimeProxy): LocalTime { - return LocalTime.of(proxy.hour.toInt(), proxy.minute.toInt(), proxy.second.toInt(), proxy.nano) - } -} diff --git a/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/MergeWhitelists.kt b/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/MergeWhitelists.kt deleted file mode 100644 index b603aca408..0000000000 --- a/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/MergeWhitelists.kt +++ /dev/null @@ -1,10 +0,0 @@ -package net.corda.serialization.djvm.deserializers - -import net.corda.core.serialization.SerializationWhitelist -import java.util.function.Function - -class MergeWhitelists : Function, Array>> { - override fun apply(whitelists: Array): Array> { - return whitelists.flatMapTo(LinkedHashSet(), SerializationWhitelist::whitelist).toTypedArray() - } -} \ No newline at end of file diff --git a/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/MonthDayDeserializer.kt b/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/MonthDayDeserializer.kt deleted file mode 100644 index 59195d0729..0000000000 --- a/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/MonthDayDeserializer.kt +++ /dev/null @@ -1,11 +0,0 @@ -package net.corda.serialization.djvm.deserializers - -import net.corda.serialization.internal.amqp.custom.MonthDaySerializer.MonthDayProxy -import java.time.MonthDay -import java.util.function.Function - -class MonthDayDeserializer : Function { - override fun apply(proxy: MonthDayProxy): MonthDay { - return MonthDay.of(proxy.month.toInt(), proxy.day.toInt()) - } -} diff --git a/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/OffsetDateTimeDeserializer.kt b/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/OffsetDateTimeDeserializer.kt deleted file mode 100644 index 275ddd8fa9..0000000000 --- a/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/OffsetDateTimeDeserializer.kt +++ /dev/null @@ -1,11 +0,0 @@ -package net.corda.serialization.djvm.deserializers - -import net.corda.serialization.internal.amqp.custom.OffsetDateTimeSerializer.OffsetDateTimeProxy -import java.time.OffsetDateTime -import java.util.function.Function - -class OffsetDateTimeDeserializer : Function { - override fun apply(proxy: OffsetDateTimeProxy): OffsetDateTime { - return OffsetDateTime.of(proxy.dateTime, proxy.offset) - } -} diff --git a/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/OffsetTimeDeserializer.kt b/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/OffsetTimeDeserializer.kt deleted file mode 100644 index 550c565355..0000000000 --- a/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/OffsetTimeDeserializer.kt +++ /dev/null @@ -1,11 +0,0 @@ -package net.corda.serialization.djvm.deserializers - -import net.corda.serialization.internal.amqp.custom.OffsetTimeSerializer.OffsetTimeProxy -import java.time.OffsetTime -import java.util.function.Function - -class OffsetTimeDeserializer : Function { - override fun apply(proxy: OffsetTimeProxy): OffsetTime { - return OffsetTime.of(proxy.time, proxy.offset) - } -} diff --git a/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/OpaqueBytesSubSequenceDeserializer.kt b/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/OpaqueBytesSubSequenceDeserializer.kt deleted file mode 100644 index 9755d1dae0..0000000000 --- a/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/OpaqueBytesSubSequenceDeserializer.kt +++ /dev/null @@ -1,11 +0,0 @@ -package net.corda.serialization.djvm.deserializers - -import net.corda.core.utilities.OpaqueBytes -import net.corda.core.utilities.OpaqueBytesSubSequence -import java.util.function.Function - -class OpaqueBytesSubSequenceDeserializer : Function { - override fun apply(proxy: OpaqueBytes): OpaqueBytesSubSequence { - return OpaqueBytesSubSequence(proxy.bytes, proxy.offset, proxy.size) - } -} diff --git a/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/OptionalDeserializer.kt b/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/OptionalDeserializer.kt deleted file mode 100644 index 0d8d511952..0000000000 --- a/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/OptionalDeserializer.kt +++ /dev/null @@ -1,11 +0,0 @@ -package net.corda.serialization.djvm.deserializers - -import net.corda.serialization.internal.amqp.custom.OptionalSerializer.OptionalProxy -import java.util.Optional -import java.util.function.Function - -class OptionalDeserializer : Function> { - override fun apply(proxy: OptionalProxy): Optional { - return Optional.ofNullable(proxy.item) - } -} diff --git a/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/PeriodDeserializer.kt b/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/PeriodDeserializer.kt deleted file mode 100644 index 25e6381455..0000000000 --- a/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/PeriodDeserializer.kt +++ /dev/null @@ -1,11 +0,0 @@ -package net.corda.serialization.djvm.deserializers - -import net.corda.serialization.internal.amqp.custom.PeriodSerializer.PeriodProxy -import java.time.Period -import java.util.function.Function - -class PeriodDeserializer : Function { - override fun apply(proxy: PeriodProxy): Period { - return Period.of(proxy.years, proxy.months, proxy.days) - } -} diff --git a/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/PublicKeyDecoder.kt b/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/PublicKeyDecoder.kt deleted file mode 100644 index 1be04bd88a..0000000000 --- a/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/PublicKeyDecoder.kt +++ /dev/null @@ -1,11 +0,0 @@ -package net.corda.serialization.djvm.deserializers - -import net.corda.core.crypto.Crypto -import java.security.PublicKey -import java.util.function.Function - -class PublicKeyDecoder : Function { - override fun apply(encoded: ByteArray): PublicKey { - return Crypto.decodePublicKey(encoded) - } -} diff --git a/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/SymbolDeserializer.kt b/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/SymbolDeserializer.kt deleted file mode 100644 index 83f058b231..0000000000 --- a/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/SymbolDeserializer.kt +++ /dev/null @@ -1,10 +0,0 @@ -package net.corda.serialization.djvm.deserializers - -import org.apache.qpid.proton.amqp.Symbol -import java.util.function.Function - -class SymbolDeserializer : Function { - override fun apply(value: String): Symbol { - return Symbol.valueOf(value) - } -} diff --git a/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/UnsignedByteDeserializer.kt b/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/UnsignedByteDeserializer.kt deleted file mode 100644 index facf725c4a..0000000000 --- a/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/UnsignedByteDeserializer.kt +++ /dev/null @@ -1,10 +0,0 @@ -package net.corda.serialization.djvm.deserializers - -import org.apache.qpid.proton.amqp.UnsignedByte -import java.util.function.Function - -class UnsignedByteDeserializer : Function { - override fun apply(underlying: ByteArray): UnsignedByte { - return UnsignedByte(underlying[0]) - } -} diff --git a/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/UnsignedIntegerDeserializer.kt b/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/UnsignedIntegerDeserializer.kt deleted file mode 100644 index 69732c58ff..0000000000 --- a/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/UnsignedIntegerDeserializer.kt +++ /dev/null @@ -1,10 +0,0 @@ -package net.corda.serialization.djvm.deserializers - -import org.apache.qpid.proton.amqp.UnsignedInteger -import java.util.function.Function - -class UnsignedIntegerDeserializer : Function { - override fun apply(underlying: IntArray): UnsignedInteger { - return UnsignedInteger(underlying[0]) - } -} diff --git a/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/UnsignedLongDeserializer.kt b/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/UnsignedLongDeserializer.kt deleted file mode 100644 index b628b033e8..0000000000 --- a/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/UnsignedLongDeserializer.kt +++ /dev/null @@ -1,10 +0,0 @@ -package net.corda.serialization.djvm.deserializers - -import org.apache.qpid.proton.amqp.UnsignedLong -import java.util.function.Function - -class UnsignedLongDeserializer : Function { - override fun apply(underlying: LongArray): UnsignedLong { - return UnsignedLong(underlying[0]) - } -} diff --git a/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/UnsignedShortDeserializer.kt b/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/UnsignedShortDeserializer.kt deleted file mode 100644 index 5251225f21..0000000000 --- a/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/UnsignedShortDeserializer.kt +++ /dev/null @@ -1,10 +0,0 @@ -package net.corda.serialization.djvm.deserializers - -import org.apache.qpid.proton.amqp.UnsignedShort -import java.util.function.Function - -class UnsignedShortDeserializer : Function { - override fun apply(underlying: ShortArray): UnsignedShort { - return UnsignedShort(underlying[0]) - } -} diff --git a/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/X509CRLDeserializer.kt b/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/X509CRLDeserializer.kt deleted file mode 100644 index 81309fcfbf..0000000000 --- a/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/X509CRLDeserializer.kt +++ /dev/null @@ -1,12 +0,0 @@ -package net.corda.serialization.djvm.deserializers - -import java.security.cert.CertificateFactory -import java.security.cert.X509CRL -import java.util.function.Function - -class X509CRLDeserializer : Function { - override fun apply(bytes: ByteArray): X509CRL { - val factory = CertificateFactory.getInstance("X.509") - return factory.generateCRL(bytes.inputStream()) as X509CRL - } -} diff --git a/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/X509CertificateDeserializer.kt b/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/X509CertificateDeserializer.kt deleted file mode 100644 index 9f3423eaed..0000000000 --- a/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/X509CertificateDeserializer.kt +++ /dev/null @@ -1,12 +0,0 @@ -package net.corda.serialization.djvm.deserializers - -import java.security.cert.CertificateFactory -import java.security.cert.X509Certificate -import java.util.function.Function - -class X509CertificateDeserializer : Function { - override fun apply(bits: ByteArray): X509Certificate { - val factory = CertificateFactory.getInstance("X.509") - return factory.generateCertificate(bits.inputStream()) as X509Certificate - } -} diff --git a/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/YearDeserializer.kt b/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/YearDeserializer.kt deleted file mode 100644 index 2787e587a2..0000000000 --- a/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/YearDeserializer.kt +++ /dev/null @@ -1,11 +0,0 @@ -package net.corda.serialization.djvm.deserializers - -import net.corda.serialization.internal.amqp.custom.YearSerializer.YearProxy -import java.time.Year -import java.util.function.Function - -class YearDeserializer : Function { - override fun apply(proxy: YearProxy): Year { - return Year.of(proxy.year) - } -} diff --git a/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/YearMonthDeserializer.kt b/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/YearMonthDeserializer.kt deleted file mode 100644 index 6876aa4082..0000000000 --- a/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/YearMonthDeserializer.kt +++ /dev/null @@ -1,11 +0,0 @@ -package net.corda.serialization.djvm.deserializers - -import net.corda.serialization.internal.amqp.custom.YearMonthSerializer.YearMonthProxy -import java.time.YearMonth -import java.util.function.Function - -class YearMonthDeserializer : Function { - override fun apply(proxy: YearMonthProxy): YearMonth { - return YearMonth.of(proxy.year, proxy.month.toInt()) - } -} diff --git a/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/ZoneIdDeserializer.kt b/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/ZoneIdDeserializer.kt deleted file mode 100644 index c28dbcab24..0000000000 --- a/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/ZoneIdDeserializer.kt +++ /dev/null @@ -1,11 +0,0 @@ -package net.corda.serialization.djvm.deserializers - -import net.corda.serialization.internal.amqp.custom.ZoneIdSerializer.ZoneIdProxy -import java.time.ZoneId -import java.util.function.Function - -class ZoneIdDeserializer : Function { - override fun apply(proxy: ZoneIdProxy): ZoneId { - return ZoneId.of(proxy.id) - } -} diff --git a/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/ZonedDateTimeDeserializer.kt b/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/ZonedDateTimeDeserializer.kt deleted file mode 100644 index e1cf9485ee..0000000000 --- a/serialization-djvm/deserializers/src/main/kotlin/net/corda/serialization/djvm/deserializers/ZonedDateTimeDeserializer.kt +++ /dev/null @@ -1,10 +0,0 @@ -package net.corda.serialization.djvm.deserializers - -import net.corda.serialization.internal.amqp.custom.ZonedDateTimeSerializer.ZonedDateTimeProxy -import java.util.function.Function - -class ZonedDateTimeDeserializer : Function?> { - override fun apply(proxy: ZonedDateTimeProxy): Array? { - return arrayOf(proxy.dateTime, proxy.offset, proxy.zone) - } -} diff --git a/serialization-djvm/deserializers/src/main/resources/META-INF/DJVM-preload b/serialization-djvm/deserializers/src/main/resources/META-INF/DJVM-preload deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/serialization-djvm/src/main/java/net/corda/serialization/djvm/serializers/CacheKey.java b/serialization-djvm/src/main/java/net/corda/serialization/djvm/serializers/CacheKey.java deleted file mode 100644 index 5ef3728e91..0000000000 --- a/serialization-djvm/src/main/java/net/corda/serialization/djvm/serializers/CacheKey.java +++ /dev/null @@ -1,35 +0,0 @@ -package net.corda.serialization.djvm.serializers; - -import org.jetbrains.annotations.NotNull; - -import java.util.Arrays; - -/** - * This class is deliberately written in Java so - * that it can be package private. - */ -final class CacheKey { - private final byte[] bytes; - private final int hashValue; - - CacheKey(@NotNull byte[] bytes) { - this.bytes = bytes; - this.hashValue = Arrays.hashCode(bytes); - } - - @NotNull - byte[] getBytes() { - return bytes; - } - - @Override - public boolean equals(Object other) { - return (this == other) - || (other instanceof CacheKey && Arrays.equals(bytes, ((CacheKey) other).bytes)); - } - - @Override - public int hashCode() { - return hashValue; - } -} diff --git a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/AMQPSerializationScheme.kt b/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/AMQPSerializationScheme.kt deleted file mode 100644 index 91250fee68..0000000000 --- a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/AMQPSerializationScheme.kt +++ /dev/null @@ -1,34 +0,0 @@ -package net.corda.serialization.djvm - -import net.corda.core.serialization.SerializationContext -import net.corda.core.serialization.SerializationContext.UseCase -import net.corda.core.serialization.SerializedBytes -import net.corda.core.utilities.ByteSequence -import net.corda.serialization.internal.CordaSerializationMagic -import net.corda.serialization.internal.SerializationScheme -import net.corda.serialization.internal.amqp.DeserializationInput -import net.corda.serialization.internal.amqp.SerializationOutput -import net.corda.serialization.internal.amqp.SerializerFactory -import net.corda.serialization.internal.amqp.amqpMagic - -/** - * This is an ephemeral [SerializationScheme] that will only ever - * support a single [SerializerFactory]. The [ClassLoader] that - * underpins everything this scheme is deserializing is not expected - * to be long-lived either. - */ -class AMQPSerializationScheme( - val serializerFactory: SerializerFactory -) : SerializationScheme { - override fun deserialize(byteSequence: ByteSequence, clazz: Class, context: SerializationContext): T { - return DeserializationInput(serializerFactory).deserialize(byteSequence, clazz, context) - } - - override fun serialize(obj: T, context: SerializationContext): SerializedBytes { - return SerializationOutput(serializerFactory).serialize(obj, context) - } - - override fun canDeserializeVersion(magic: CordaSerializationMagic, target: UseCase): Boolean { - return magic == amqpMagic && target == UseCase.P2P - } -} diff --git a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/DelegatingClassLoader.kt b/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/DelegatingClassLoader.kt deleted file mode 100644 index ce916bd2ff..0000000000 --- a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/DelegatingClassLoader.kt +++ /dev/null @@ -1,10 +0,0 @@ -package net.corda.serialization.djvm - -import net.corda.djvm.rewiring.SandboxClassLoader - -class DelegatingClassLoader(private val delegate: SandboxClassLoader) : ClassLoader(null) { - @Throws(ClassNotFoundException::class) - override fun loadClass(name: String, resolve: Boolean): Class<*> { - return delegate.toSandboxClass(name) - } -} diff --git a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/SandboxSerializationSchemeBuilder.kt b/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/SandboxSerializationSchemeBuilder.kt deleted file mode 100644 index d0406e2e4f..0000000000 --- a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/SandboxSerializationSchemeBuilder.kt +++ /dev/null @@ -1,150 +0,0 @@ -package net.corda.serialization.djvm - -import net.corda.core.internal.objectOrNewInstance -import net.corda.core.serialization.SerializationContext -import net.corda.core.serialization.SerializationWhitelist -import net.corda.djvm.rewiring.SandboxClassLoader -import net.corda.serialization.djvm.deserializers.MergeWhitelists -import net.corda.serialization.djvm.serializers.SandboxBitSetSerializer -import net.corda.serialization.djvm.serializers.SandboxCertPathSerializer -import net.corda.serialization.djvm.serializers.SandboxCharacterSerializer -import net.corda.serialization.djvm.serializers.SandboxCollectionSerializer -import net.corda.serialization.djvm.serializers.SandboxCorDappCustomSerializer -import net.corda.serialization.djvm.serializers.SandboxCurrencySerializer -import net.corda.serialization.djvm.serializers.SandboxDecimal128Serializer -import net.corda.serialization.djvm.serializers.SandboxDecimal32Serializer -import net.corda.serialization.djvm.serializers.SandboxDecimal64Serializer -import net.corda.serialization.djvm.serializers.SandboxDurationSerializer -import net.corda.serialization.djvm.serializers.SandboxEnumSerializer -import net.corda.serialization.djvm.serializers.SandboxEnumSetSerializer -import net.corda.serialization.djvm.serializers.SandboxInputStreamSerializer -import net.corda.serialization.djvm.serializers.SandboxInstantSerializer -import net.corda.serialization.djvm.serializers.SandboxLocalDateSerializer -import net.corda.serialization.djvm.serializers.SandboxLocalDateTimeSerializer -import net.corda.serialization.djvm.serializers.SandboxLocalTimeSerializer -import net.corda.serialization.djvm.serializers.SandboxMapSerializer -import net.corda.serialization.djvm.serializers.SandboxMonthDaySerializer -import net.corda.serialization.djvm.serializers.SandboxOffsetDateTimeSerializer -import net.corda.serialization.djvm.serializers.SandboxOffsetTimeSerializer -import net.corda.serialization.djvm.serializers.SandboxOpaqueBytesSubSequenceSerializer -import net.corda.serialization.djvm.serializers.SandboxOptionalSerializer -import net.corda.serialization.djvm.serializers.SandboxPeriodSerializer -import net.corda.serialization.djvm.serializers.SandboxPrimitiveSerializer -import net.corda.serialization.djvm.serializers.SandboxPublicKeySerializer -import net.corda.serialization.djvm.serializers.SandboxSymbolSerializer -import net.corda.serialization.djvm.serializers.SandboxToStringSerializer -import net.corda.serialization.djvm.serializers.SandboxUnsignedByteSerializer -import net.corda.serialization.djvm.serializers.SandboxUnsignedIntegerSerializer -import net.corda.serialization.djvm.serializers.SandboxUnsignedLongSerializer -import net.corda.serialization.djvm.serializers.SandboxUnsignedShortSerializer -import net.corda.serialization.djvm.serializers.SandboxX509CRLSerializer -import net.corda.serialization.djvm.serializers.SandboxX509CertificateSerializer -import net.corda.serialization.djvm.serializers.SandboxYearMonthSerializer -import net.corda.serialization.djvm.serializers.SandboxYearSerializer -import net.corda.serialization.djvm.serializers.SandboxZoneIdSerializer -import net.corda.serialization.djvm.serializers.SandboxZonedDateTimeSerializer -import net.corda.serialization.internal.SerializationScheme -import net.corda.serialization.internal.amqp.SerializerFactory -import net.corda.serialization.internal.amqp.SerializerFactoryFactory -import net.corda.serialization.internal.amqp.addToWhitelist -import java.math.BigDecimal -import java.math.BigInteger -import java.util.Date -import java.util.UUID -import java.util.function.Function -import java.util.function.Predicate - -class SandboxSerializationSchemeBuilder( - private val classLoader: SandboxClassLoader, - private val sandboxBasicInput: Function, - private val rawTaskFactory: Function>, - private val taskFactory: Function>, out Function>, - private val predicateFactory: Function>, out Predicate>, - private val customSerializerClassNames: Set, - private val serializationWhitelistNames: Set, - private val serializerFactoryFactory: SerializerFactoryFactory -) { - fun buildFor(context: SerializationContext): SerializationScheme { - return AMQPSerializationScheme(getSerializerFactory(context)) - } - - private fun getSerializerFactory(context: SerializationContext): SerializerFactory { - return serializerFactoryFactory.make(context).apply { - register(SandboxBitSetSerializer(classLoader, taskFactory, this)) - register(SandboxCertPathSerializer(classLoader, taskFactory, this)) - register(SandboxDurationSerializer(classLoader, taskFactory, this)) - register(SandboxEnumSetSerializer(classLoader, taskFactory, this)) - register(SandboxInputStreamSerializer(classLoader, taskFactory)) - register(SandboxInstantSerializer(classLoader, taskFactory, this)) - register(SandboxLocalDateSerializer(classLoader, taskFactory, this)) - register(SandboxLocalDateTimeSerializer(classLoader, taskFactory, this)) - register(SandboxLocalTimeSerializer(classLoader, taskFactory, this)) - register(SandboxMonthDaySerializer(classLoader, taskFactory, this)) - register(SandboxOffsetDateTimeSerializer(classLoader, taskFactory, this)) - register(SandboxOffsetTimeSerializer(classLoader, taskFactory, this)) - register(SandboxPeriodSerializer(classLoader, taskFactory, this)) - register(SandboxYearMonthSerializer(classLoader, taskFactory, this)) - register(SandboxYearSerializer(classLoader, taskFactory, this)) - register(SandboxZonedDateTimeSerializer(classLoader, taskFactory, this)) - register(SandboxZoneIdSerializer(classLoader, taskFactory, this)) - register(SandboxOpaqueBytesSubSequenceSerializer(classLoader, taskFactory, this)) - register(SandboxOptionalSerializer(classLoader, taskFactory, this)) - register(SandboxPrimitiveSerializer(UUID::class.java, classLoader, sandboxBasicInput)) - register(SandboxPrimitiveSerializer(String::class.java, classLoader, sandboxBasicInput)) - register(SandboxPrimitiveSerializer(Byte::class.javaObjectType, classLoader, sandboxBasicInput)) - register(SandboxPrimitiveSerializer(Short::class.javaObjectType, classLoader, sandboxBasicInput)) - register(SandboxPrimitiveSerializer(Int::class.javaObjectType, classLoader, sandboxBasicInput)) - register(SandboxPrimitiveSerializer(Long::class.javaObjectType, classLoader, sandboxBasicInput)) - register(SandboxPrimitiveSerializer(Float::class.javaObjectType, classLoader, sandboxBasicInput)) - register(SandboxPrimitiveSerializer(Double::class.javaObjectType, classLoader, sandboxBasicInput)) - register(SandboxPrimitiveSerializer(Boolean::class.javaObjectType, classLoader, sandboxBasicInput)) - register(SandboxPrimitiveSerializer(Date::class.javaObjectType, classLoader, sandboxBasicInput)) - register(SandboxCharacterSerializer(classLoader, sandboxBasicInput)) - register(SandboxCollectionSerializer(classLoader, taskFactory, this)) - register(SandboxMapSerializer(classLoader, taskFactory, this)) - register(SandboxEnumSerializer(classLoader, taskFactory, predicateFactory, this)) - register(SandboxPublicKeySerializer(classLoader, taskFactory)) - register(SandboxToStringSerializer(BigDecimal::class.java, classLoader, sandboxBasicInput)) - register(SandboxToStringSerializer(BigInteger::class.java, classLoader, sandboxBasicInput)) - register(SandboxToStringSerializer(StringBuffer::class.java, classLoader, sandboxBasicInput)) - register(SandboxCurrencySerializer(classLoader, taskFactory, sandboxBasicInput)) - register(SandboxX509CertificateSerializer(classLoader, taskFactory)) - register(SandboxX509CRLSerializer(classLoader, taskFactory)) - register(SandboxUnsignedLongSerializer(classLoader, taskFactory)) - register(SandboxUnsignedIntegerSerializer(classLoader, taskFactory)) - register(SandboxUnsignedShortSerializer(classLoader, taskFactory)) - register(SandboxUnsignedByteSerializer(classLoader, taskFactory)) - register(SandboxDecimal128Serializer(classLoader, taskFactory)) - register(SandboxDecimal64Serializer(classLoader, taskFactory)) - register(SandboxDecimal32Serializer(classLoader, taskFactory)) - register(SandboxSymbolSerializer(classLoader, taskFactory, sandboxBasicInput)) - - for (customSerializerName in customSerializerClassNames) { - register(SandboxCorDappCustomSerializer(customSerializerName, classLoader, rawTaskFactory, this)) - } - registerWhitelists(taskFactory, this) - } - } - - private fun registerWhitelists( - taskFactory: Function>, out Function>, - factory: SerializerFactory - ) { - if (serializationWhitelistNames.isEmpty()) { - return - } - - val serializationWhitelists = serializationWhitelistNames.map { whitelistClass -> - classLoader.toSandboxClass(whitelistClass).kotlin.objectOrNewInstance() - }.toArrayOf(classLoader.toSandboxClass(SerializationWhitelist::class.java)) - @Suppress("unchecked_cast") - val mergeTask = taskFactory.apply(MergeWhitelists::class.java) as Function, out Array>> - factory.addToWhitelist(mergeTask.apply(serializationWhitelists).toSet()) - } - - @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") - private fun Collection<*>.toArrayOf(type: Class<*>): Array<*> { - val typedArray = java.lang.reflect.Array.newInstance(type, 0) as Array<*> - return (this as java.util.Collection<*>).toArray(typedArray) - } -} diff --git a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/SandboxSerializerFactoryFactory.kt b/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/SandboxSerializerFactoryFactory.kt deleted file mode 100644 index 96f8c44a03..0000000000 --- a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/SandboxSerializerFactoryFactory.kt +++ /dev/null @@ -1,116 +0,0 @@ -@file:Suppress("platform_class_mapped_to_kotlin") -package net.corda.serialization.djvm - -import net.corda.core.serialization.SerializationContext -import net.corda.serialization.internal.amqp.AMQPRemoteTypeModel -import net.corda.serialization.internal.amqp.AMQPSerializer -import net.corda.serialization.internal.amqp.CachingCustomSerializerRegistry -import net.corda.serialization.internal.amqp.ComposedSerializerFactory -import net.corda.serialization.internal.amqp.DefaultDescriptorBasedSerializerRegistry -import net.corda.serialization.internal.amqp.DefaultEvolutionSerializerFactory -import net.corda.serialization.internal.amqp.DefaultLocalSerializerFactory -import net.corda.serialization.internal.amqp.DefaultRemoteSerializerFactory -import net.corda.serialization.internal.amqp.SerializerFactory -import net.corda.serialization.internal.amqp.SerializerFactoryFactory -import net.corda.serialization.internal.amqp.WhitelistBasedTypeModelConfiguration -import net.corda.serialization.internal.amqp.createClassCarpenter -import net.corda.serialization.internal.model.BaseLocalTypes -import net.corda.serialization.internal.model.ClassCarpentingTypeLoader -import net.corda.serialization.internal.model.ConfigurableLocalTypeModel -import net.corda.serialization.internal.model.SchemaBuildingRemoteTypeCarpenter -import net.corda.serialization.internal.amqp.SerializerFactoryBuilder -import net.corda.serialization.internal.model.TypeLoader -import net.corda.serialization.internal.model.TypeModellingFingerPrinter -import java.lang.Boolean -import java.lang.Byte -import java.lang.Double -import java.lang.Float -import java.lang.Long -import java.lang.Short -import java.util.Collections.singleton -import java.util.Collections.unmodifiableMap -import java.util.Date -import java.util.UUID -import java.util.function.Function -import java.util.function.Predicate - -/** - * This has all been lovingly copied from [SerializerFactoryBuilder]. - */ -class SandboxSerializerFactoryFactory( - private val primitiveSerializerFactory: Function, AMQPSerializer>, - private val localTypes: BaseLocalTypes -) : SerializerFactoryFactory { - - override fun make(context: SerializationContext): SerializerFactory { - val classLoader = context.deserializationClassLoader - - val primitiveTypes = unmodifiableMap(mapOf, Class<*>>( - classLoader.loadClass("sandbox.java.lang.Boolean") to Boolean.TYPE, - classLoader.loadClass("sandbox.java.lang.Byte") to Byte.TYPE, - classLoader.loadClass("sandbox.java.lang.Character") to Character.TYPE, - classLoader.loadClass("sandbox.java.lang.Double") to Double.TYPE, - classLoader.loadClass("sandbox.java.lang.Float") to Float.TYPE, - classLoader.loadClass("sandbox.java.lang.Integer") to Integer.TYPE, - classLoader.loadClass("sandbox.java.lang.Long") to Long.TYPE, - classLoader.loadClass("sandbox.java.lang.Short") to Short.TYPE, - classLoader.loadClass("sandbox.java.lang.String") to String::class.java, - classLoader.loadClass("sandbox.java.util.Date") to Date::class.java, - classLoader.loadClass("sandbox.java.util.UUID") to UUID::class.java, - Void::class.java to Void.TYPE - )) - - val classCarpenter = createClassCarpenter(context) - val descriptorBasedSerializerRegistry = DefaultDescriptorBasedSerializerRegistry() - val customSerializerRegistry = CachingCustomSerializerRegistry( - descriptorBasedSerializerRegistry = descriptorBasedSerializerRegistry, - allowedFor = singleton(classLoader.loadClass("sandbox.java.lang.Object")) - ) - - val localTypeModel = ConfigurableLocalTypeModel( - WhitelistBasedTypeModelConfiguration( - whitelist = context.whitelist, - customSerializerRegistry = customSerializerRegistry, - baseTypes = localTypes - ) - ) - - val fingerPrinter = TypeModellingFingerPrinter(customSerializerRegistry, classLoader) - - val localSerializerFactory = DefaultLocalSerializerFactory( - whitelist = context.whitelist, - typeModel = localTypeModel, - fingerPrinter = fingerPrinter, - classloader = classLoader, - descriptorBasedSerializerRegistry = descriptorBasedSerializerRegistry, - primitiveSerializerFactory = primitiveSerializerFactory, - isPrimitiveType = Predicate { clazz -> clazz.isPrimitive || clazz in primitiveTypes.keys }, - customSerializerRegistry = customSerializerRegistry, - onlyCustomSerializers = false - ) - - val typeLoader: TypeLoader = ClassCarpentingTypeLoader( - carpenter = SchemaBuildingRemoteTypeCarpenter(classCarpenter), - classLoader = classLoader - ) - - val evolutionSerializerFactory = DefaultEvolutionSerializerFactory( - localSerializerFactory = localSerializerFactory, - classLoader = classLoader, - mustPreserveDataWhenEvolving = context.preventDataLoss, - primitiveTypes = primitiveTypes, - baseTypes = localTypes - ) - - val remoteSerializerFactory = DefaultRemoteSerializerFactory( - evolutionSerializerFactory = evolutionSerializerFactory, - descriptorBasedSerializerRegistry = descriptorBasedSerializerRegistry, - remoteTypeModel = AMQPRemoteTypeModel(), - localTypeModel = localTypeModel, - typeLoader = typeLoader, - localSerializerFactory = localSerializerFactory - ) - - return ComposedSerializerFactory(localSerializerFactory, remoteSerializerFactory, customSerializerRegistry) - } -} diff --git a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/SandboxWhitelist.kt b/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/SandboxWhitelist.kt deleted file mode 100644 index 9df1b34cb0..0000000000 --- a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/SandboxWhitelist.kt +++ /dev/null @@ -1,13 +0,0 @@ -package net.corda.serialization.djvm - -import net.corda.core.serialization.ClassWhitelist - -class SandboxWhitelist : ClassWhitelist { - companion object { - private val packageName = "^sandbox\\.(?:java|kotlin)(?:[.]|$)".toRegex() - } - - override fun hasListed(type: Class<*>): Boolean { - return packageName.containsMatchIn(type.`package`.name) - } -} diff --git a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/Serialization.kt b/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/Serialization.kt deleted file mode 100644 index 6b73fa6e61..0000000000 --- a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/Serialization.kt +++ /dev/null @@ -1,117 +0,0 @@ -@file:JvmName("Serialization") -package net.corda.serialization.djvm - -import net.corda.core.serialization.SerializationContext -import net.corda.core.serialization.SerializationContext.UseCase -import net.corda.core.serialization.SerializationFactory -import net.corda.core.serialization.SerializedBytes -import net.corda.core.serialization.internal.SerializationEnvironment -import net.corda.core.utilities.ByteSequence -import net.corda.djvm.rewiring.createRawPredicateFactory -import net.corda.djvm.rewiring.createSandboxPredicate -import net.corda.djvm.rewiring.SandboxClassLoader -import net.corda.serialization.djvm.deserializers.CheckEnum -import net.corda.serialization.djvm.deserializers.DescribeEnum -import net.corda.serialization.djvm.deserializers.GetEnumNames -import net.corda.serialization.djvm.serializers.PrimitiveSerializer -import net.corda.serialization.internal.GlobalTransientClassWhiteList -import net.corda.serialization.internal.SerializationContextImpl -import net.corda.serialization.internal.SerializationFactoryImpl -import net.corda.serialization.internal.amqp.AMQPSerializer -import net.corda.serialization.internal.amqp.amqpMagic -import net.corda.serialization.internal.model.BaseLocalTypes -import java.util.EnumSet -import java.util.function.Function -import java.util.function.Predicate - -@Suppress("NOTHING_TO_INLINE") -inline fun SandboxClassLoader.toSandboxAnyClass(clazz: Class<*>): Class { - @Suppress("unchecked_cast") - return toSandboxClass(clazz) as Class -} - -fun createSandboxSerializationEnv(classLoader: SandboxClassLoader): SerializationEnvironment { - return createSandboxSerializationEnv(classLoader, emptySet(), emptySet()) -} - -fun createSandboxSerializationEnv( - classLoader: SandboxClassLoader, - customSerializerClassNames: Set, - serializationWhitelistNames: Set -): SerializationEnvironment { - val p2pContext: SerializationContext = SerializationContextImpl( - preferredSerializationVersion = amqpMagic, - deserializationClassLoader = DelegatingClassLoader(classLoader), - whitelist = GlobalTransientClassWhiteList(SandboxWhitelist()), - properties = emptyMap(), - objectReferencesEnabled = true, - carpenterDisabled = true, - useCase = UseCase.P2P, - encoding = null - ) - - val sandboxBasicInput = classLoader.createBasicInput() - val rawTaskFactory = classLoader.createRawTaskFactory() - val taskFactory = rawTaskFactory.compose(classLoader.createSandboxFunction()) - val predicateFactory = classLoader.createRawPredicateFactory().compose(classLoader.createSandboxPredicate()) - - val primitiveSerializerFactory: Function, AMQPSerializer> = Function { clazz -> - PrimitiveSerializer(clazz, sandboxBasicInput) - } - @Suppress("unchecked_cast") - val isEnumPredicate = predicateFactory.apply(CheckEnum::class.java) as Predicate> - @Suppress("unchecked_cast") - val enumConstants = taskFactory.apply(DescribeEnum::class.java) as Function, Array> - @Suppress("unchecked_cast") - val enumConstantNames = enumConstants.andThen(taskFactory.apply(GetEnumNames::class.java)) - .andThen { (it as Array).map(Any::toString) } as Function, List> - - val sandboxLocalTypes = BaseLocalTypes( - collectionClass = classLoader.toSandboxClass(Collection::class.java), - enumSetClass = classLoader.toSandboxClass(EnumSet::class.java), - exceptionClass = classLoader.toSandboxClass(Exception::class.java), - mapClass = classLoader.toSandboxClass(Map::class.java), - stringClass = classLoader.toSandboxClass(String::class.java), - isEnum = isEnumPredicate, - enumConstants = enumConstants, - enumConstantNames = enumConstantNames - ) - val schemeBuilder = SandboxSerializationSchemeBuilder( - classLoader = classLoader, - sandboxBasicInput = sandboxBasicInput, - rawTaskFactory = rawTaskFactory, - taskFactory = taskFactory, - predicateFactory = predicateFactory, - customSerializerClassNames = customSerializerClassNames, - serializationWhitelistNames = serializationWhitelistNames, - serializerFactoryFactory = SandboxSerializerFactoryFactory( - primitiveSerializerFactory = primitiveSerializerFactory, - localTypes = sandboxLocalTypes - ) - ) - val factory = SerializationFactoryImpl(mutableMapOf()).apply { - registerScheme(schemeBuilder.buildFor(p2pContext)) - } - return SerializationEnvironment.with(factory, p2pContext = p2pContext) -} - -inline fun SerializedBytes.deserializeFor(classLoader: SandboxClassLoader): Any { - return deserializeTo(T::class.java, classLoader) -} - -inline fun ByteSequence.deserializeTypeFor(classLoader: SandboxClassLoader): Any { - return deserializeTo(T::class.java, classLoader) -} - -fun ByteSequence.deserializeTo(clazz: Class, classLoader: SandboxClassLoader): Any { - val sandboxClazz = classLoader.toSandboxClass(clazz) - return deserializeTo(sandboxClazz) -} - -fun ByteSequence.deserializeTo(clazz: Class<*>): Any { - return deserializeTo(clazz, SerializationFactory.defaultFactory) -} - -fun ByteSequence.deserializeTo(clazz: Class<*>, factory: SerializationFactory): Any { - return factory.deserialize(this, clazz, factory.defaultContext) -} diff --git a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/ExceptionUtils.kt b/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/ExceptionUtils.kt deleted file mode 100644 index edb859d3bd..0000000000 --- a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/ExceptionUtils.kt +++ /dev/null @@ -1,39 +0,0 @@ -@file:JvmName("ExceptionUtils") -package net.corda.serialization.djvm.serializers - -import net.corda.serialization.internal.amqp.AMQPNotSerializableException - -/** - * Utility function which helps tracking the path in the object graph when exceptions are thrown. - * Since there might be a chain of nested calls it is useful to record which part of the graph caused an issue. - * Path information is added to the message of the exception being thrown. - */ -@Suppress("TooGenericExceptionCaught") -internal inline fun ifThrowsAppend(strToAppendFn: () -> String, block: () -> T): T { - try { - return block() - } catch (th: Throwable) { - when (th) { - is AMQPNotSerializableException -> th.classHierarchy.add(strToAppendFn()) - // Do not overwrite the message of these exceptions as it may be used. - is ClassNotFoundException -> {} - is NoClassDefFoundError -> {} - else -> th.resetMessage("${strToAppendFn()} -> ${th.message}") - } - throw th - } -} - -/** - * Not a public property so will have to use reflection - */ -private fun Throwable.resetMessage(newMsg: String) { - val detailMessageField = Throwable::class.java.getDeclaredField("detailMessage") - detailMessageField.isAccessible = true - detailMessageField.set(this, newMsg) -} - -/** - * We currently only support deserialisation, and so we're going to need this. - */ -fun abortReadOnly(): Nothing = throw UnsupportedOperationException("Read Only!") \ No newline at end of file diff --git a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/PrimitiveSerializer.kt b/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/PrimitiveSerializer.kt deleted file mode 100644 index 52e9cd847c..0000000000 --- a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/PrimitiveSerializer.kt +++ /dev/null @@ -1,36 +0,0 @@ -package net.corda.serialization.djvm.serializers - -import net.corda.core.serialization.SerializationContext -import net.corda.serialization.internal.amqp.AMQPSerializer -import net.corda.serialization.internal.amqp.DeserializationInput -import net.corda.serialization.internal.amqp.SerializationOutput -import net.corda.serialization.internal.amqp.SerializationSchemas -import net.corda.serialization.internal.amqp.typeDescriptorFor -import org.apache.qpid.proton.amqp.Binary -import org.apache.qpid.proton.amqp.Symbol -import org.apache.qpid.proton.codec.Data -import java.lang.reflect.Type -import java.util.function.Function - -class PrimitiveSerializer( - override val type: Class<*>, - private val sandboxBasicInput: Function -) : AMQPSerializer { - override val typeDescriptor: Symbol = typeDescriptorFor(type) - - override fun readObject( - obj: Any, schemas: SerializationSchemas, input: DeserializationInput, context: SerializationContext - ): Any { - return (obj as? Binary)?.array ?: sandboxBasicInput.apply(obj)!! - } - - override fun writeClassInfo(output: SerializationOutput) { - abortReadOnly() - } - - override fun writeObject( - obj: Any, data: Data, type: Type, output: SerializationOutput, context: SerializationContext, debugIndent: Int - ) { - abortReadOnly() - } -} diff --git a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxBitSetSerializer.kt b/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxBitSetSerializer.kt deleted file mode 100644 index af11ec7887..0000000000 --- a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxBitSetSerializer.kt +++ /dev/null @@ -1,30 +0,0 @@ -package net.corda.serialization.djvm.serializers - -import net.corda.djvm.rewiring.SandboxClassLoader -import net.corda.serialization.djvm.deserializers.BitSetDeserializer -import net.corda.serialization.djvm.toSandboxAnyClass -import net.corda.serialization.internal.amqp.CustomSerializer -import net.corda.serialization.internal.amqp.SerializerFactory -import net.corda.serialization.internal.amqp.custom.BitSetSerializer.BitSetProxy -import java.util.BitSet -import java.util.function.Function - -class SandboxBitSetSerializer( - classLoader: SandboxClassLoader, - taskFactory: Function>, out Function>, - factory: SerializerFactory -) : CustomSerializer.Proxy( - clazz = classLoader.toSandboxAnyClass(BitSet::class.java), - proxyClass = classLoader.toSandboxAnyClass(BitSetProxy::class.java), - factory = factory -) { - private val task = taskFactory.apply(BitSetDeserializer::class.java) - - override val deserializationAliases = aliasFor(BitSet::class.java) - - override fun toProxy(obj: Any): Any = abortReadOnly() - - override fun fromProxy(proxy: Any): Any { - return task.apply(proxy)!! - } -} diff --git a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxCertPathSerializer.kt b/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxCertPathSerializer.kt deleted file mode 100644 index 25710d654e..0000000000 --- a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxCertPathSerializer.kt +++ /dev/null @@ -1,41 +0,0 @@ -package net.corda.serialization.djvm.serializers - -import net.corda.core.serialization.DESERIALIZATION_CACHE_PROPERTY -import net.corda.core.serialization.SerializationContext -import net.corda.djvm.rewiring.SandboxClassLoader -import net.corda.serialization.djvm.deserializers.CertPathDeserializer -import net.corda.serialization.djvm.toSandboxAnyClass -import net.corda.serialization.internal.amqp.CustomSerializer -import net.corda.serialization.internal.amqp.SerializerFactory -import net.corda.serialization.internal.amqp.custom.CertPathSerializer.CertPathProxy -import java.security.cert.CertPath -import java.util.function.Function - -class SandboxCertPathSerializer( - classLoader: SandboxClassLoader, - taskFactory: Function>, out Function>, - factory: SerializerFactory -) : CustomSerializer.Proxy( - clazz = classLoader.toSandboxAnyClass(CertPath::class.java), - proxyClass = classLoader.toSandboxAnyClass(CertPathProxy::class.java), - factory = factory -) { - private val task = taskFactory.apply(CertPathDeserializer::class.java) - - override val deserializationAliases = aliasFor(CertPath::class.java) - - override fun toProxy(obj: Any): Any = abortReadOnly() - - override fun fromProxy(proxy: Any): Any { - return task.apply(proxy)!! - } - - override fun fromProxy(proxy: Any, context: SerializationContext): Any { - // This requires [CertPathProxy] to have correct - // implementations for [equals] and [hashCode]. - @Suppress("unchecked_cast") - return (context.properties[DESERIALIZATION_CACHE_PROPERTY] as? MutableMap) - ?.computeIfAbsent(proxy, ::fromProxy) - ?: fromProxy(proxy) - } -} diff --git a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxCharacterSerializer.kt b/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxCharacterSerializer.kt deleted file mode 100644 index 926982a82b..0000000000 --- a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxCharacterSerializer.kt +++ /dev/null @@ -1,37 +0,0 @@ -package net.corda.serialization.djvm.serializers - -import net.corda.core.serialization.SerializationContext -import net.corda.djvm.rewiring.SandboxClassLoader -import net.corda.serialization.djvm.toSandboxAnyClass -import net.corda.serialization.internal.amqp.CustomSerializer -import net.corda.serialization.internal.amqp.DeserializationInput -import net.corda.serialization.internal.amqp.Schema -import net.corda.serialization.internal.amqp.SerializationOutput -import net.corda.serialization.internal.amqp.SerializationSchemas -import org.apache.qpid.proton.codec.Data -import java.lang.reflect.Type -import java.util.function.Function - -class SandboxCharacterSerializer( - classLoader: SandboxClassLoader, - private val basicInput: Function -) : CustomSerializer.Is(classLoader.toSandboxAnyClass(Char::class.javaObjectType)) { - - override val schemaForDocumentation: Schema = Schema(emptyList()) - - override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput, context: SerializationContext): Any { - return basicInput.apply(convertToChar(obj))!! - } - - private fun convertToChar(obj: Any): Any { - return when (obj) { - is Short -> obj.toChar() - is Int -> obj.toChar() - else -> obj - } - } - - override fun writeDescribedObject(obj: Any, data: Data, type: Type, output: SerializationOutput, context: SerializationContext) { - abortReadOnly() - } -} diff --git a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxClassSerializer.kt b/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxClassSerializer.kt deleted file mode 100644 index 46d3be88b3..0000000000 --- a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxClassSerializer.kt +++ /dev/null @@ -1,47 +0,0 @@ -package net.corda.serialization.djvm.serializers - -import net.corda.djvm.rewiring.SandboxClassLoader -import net.corda.serialization.djvm.deserializers.ClassDeserializer -import net.corda.serialization.djvm.toSandboxAnyClass -import net.corda.serialization.internal.amqp.AMQPNotSerializableException -import net.corda.serialization.internal.amqp.CustomSerializer -import net.corda.serialization.internal.amqp.SerializerFactory -import net.corda.serialization.internal.amqp.custom.ClassSerializer.ClassProxy -import java.util.function.Function - -@Suppress("unchecked_cast") -class SandboxClassSerializer( - classLoader: SandboxClassLoader, - taskFactory: Function>, out Function>, - factory: SerializerFactory -) : CustomSerializer.Proxy( - clazz = Class::class.java as Class, - proxyClass = classLoader.toSandboxAnyClass(ClassProxy::class.java), - factory = factory -) { - private val task = taskFactory.apply(ClassDeserializer::class.java) - private val nameOf: Function - - init { - val fetch = proxyClass.getMethod("getClassName") - nameOf = Function { proxy -> - fetch(proxy).toString() - } - } - - override fun toProxy(obj: Any): Any = abortReadOnly() - - override fun fromProxy(proxy: Any): Any { - return try { - task.apply(proxy)!! - } catch (e: ClassNotFoundException) { - val className = nameOf.apply(proxy) - throw AMQPNotSerializableException(type, - "Could not instantiate $className - not on the classpath", - "$className was not found by the node, check the Node containing the CorDapp that " + - "implements $className is loaded and on the Classpath", - mutableListOf(className) - ) - } - } -} diff --git a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxCollectionSerializer.kt b/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxCollectionSerializer.kt deleted file mode 100644 index 5c5fa25c19..0000000000 --- a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxCollectionSerializer.kt +++ /dev/null @@ -1,129 +0,0 @@ -package net.corda.serialization.djvm.serializers - -import net.corda.core.serialization.SerializationContext -import net.corda.core.utilities.NonEmptySet -import net.corda.djvm.rewiring.SandboxClassLoader -import net.corda.serialization.djvm.deserializers.CreateCollection -import net.corda.serialization.djvm.toSandboxAnyClass -import net.corda.serialization.internal.amqp.AMQPSerializer -import net.corda.serialization.internal.amqp.CustomSerializer -import net.corda.serialization.internal.amqp.DeserializationInput -import net.corda.serialization.internal.amqp.LocalSerializerFactory -import net.corda.serialization.internal.amqp.Schema -import net.corda.serialization.internal.amqp.SerializationOutput -import net.corda.serialization.internal.amqp.SerializationSchemas -import net.corda.serialization.internal.amqp.redescribe -import net.corda.serialization.internal.model.LocalTypeInformation -import net.corda.serialization.internal.model.TypeIdentifier -import org.apache.qpid.proton.amqp.Symbol -import org.apache.qpid.proton.codec.Data -import java.lang.reflect.ParameterizedType -import java.lang.reflect.Type -import java.util.EnumSet -import java.util.NavigableSet -import java.util.SortedSet -import java.util.function.Function - -class SandboxCollectionSerializer( - classLoader: SandboxClassLoader, - taskFactory: Function>, out Function>, - private val localFactory: LocalSerializerFactory -) : CustomSerializer.Implements(clazz = classLoader.toSandboxAnyClass(Collection::class.java)) { - @Suppress("unchecked_cast") - private val creator: Function, out Any?> - = taskFactory.apply(CreateCollection::class.java) as Function, out Any?> - - private val unsupportedTypes: Set> = listOf( - EnumSet::class.java - ).mapTo(LinkedHashSet()) { - classLoader.toSandboxAnyClass(it) - } - - // The order matters here - the first match should be the most specific one. - // Kotlin preserves the ordering for us by associating into a LinkedHashMap. - private val supportedTypes: Map, Class>> = listOf( - List::class.java, - NonEmptySet::class.java, - NavigableSet::class.java, - SortedSet::class.java, - Set::class.java, - Collection::class.java - ).associateBy { - classLoader.toSandboxAnyClass(it) - } - - private fun getBestMatchFor(type: Class): Map.Entry, Class>> - = supportedTypes.entries.first { it.key.isAssignableFrom(type) } - - override val schemaForDocumentation: Schema = Schema(emptyList()) - - override fun isSerializerFor(clazz: Class<*>): Boolean { - return super.isSerializerFor(clazz) && unsupportedTypes.none { it.isAssignableFrom(clazz) } - } - - override fun specialiseFor(declaredType: Type): AMQPSerializer? { - if (declaredType !is ParameterizedType) { - return null - } - - @Suppress("unchecked_cast") - val rawType = declaredType.rawType as Class - return ConcreteCollectionSerializer(declaredType, getBestMatchFor(rawType), creator, localFactory) - } - - override fun readObject( - obj: Any, schemas: SerializationSchemas, input: DeserializationInput, context: SerializationContext - ): Any { - throw UnsupportedOperationException("Factory only") - } - - override fun writeDescribedObject( - obj: Any, data: Data, type: Type, output: SerializationOutput, context: SerializationContext - ) { - throw UnsupportedOperationException("Factory Only") - } -} - -private class ConcreteCollectionSerializer( - declaredType: ParameterizedType, - private val matchingType: Map.Entry, Class>>, - private val creator: Function, out Any?>, - factory: LocalSerializerFactory -) : AMQPSerializer { - override val type: ParameterizedType = declaredType - - override val typeDescriptor: Symbol by lazy { - factory.createDescriptor( - LocalTypeInformation.ACollection( - observedType = declaredType, - typeIdentifier = TypeIdentifier.forGenericType(declaredType), - elementType = factory.getTypeInformation(declaredType.actualTypeArguments[0]) - ) - ) - } - - override fun readObject( - obj: Any, - schemas: SerializationSchemas, - input: DeserializationInput, - context: SerializationContext - ): Any { - val inboundType = type.actualTypeArguments[0] - return ifThrowsAppend(type::getTypeName) { - val args = (obj as List<*>).map { - input.readObjectOrNull(redescribe(it, inboundType), schemas, inboundType, context) - }.toTypedArray() - creator.apply(arrayOf(matchingType.key, args))!! - } - } - - override fun writeClassInfo(output: SerializationOutput) { - abortReadOnly() - } - - override fun writeObject( - obj: Any, data: Data, type: Type, output: SerializationOutput, context: SerializationContext, debugIndent: Int - ) { - abortReadOnly() - } -} diff --git a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxCorDappCustomSerializer.kt b/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxCorDappCustomSerializer.kt deleted file mode 100644 index 9420dddf9b..0000000000 --- a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxCorDappCustomSerializer.kt +++ /dev/null @@ -1,93 +0,0 @@ -package net.corda.serialization.djvm.serializers - -import com.google.common.reflect.TypeToken -import net.corda.core.internal.objectOrNewInstance -import net.corda.core.serialization.SerializationContext -import net.corda.core.serialization.SerializationCustomSerializer -import net.corda.djvm.rewiring.SandboxClassLoader -import net.corda.serialization.djvm.deserializers.CorDappCustomDeserializer -import net.corda.serialization.internal.amqp.AMQPNotSerializableException -import net.corda.serialization.internal.amqp.AMQPTypeIdentifiers -import net.corda.serialization.internal.amqp.CORDAPP_TYPE -import net.corda.serialization.internal.amqp.CustomSerializer -import net.corda.serialization.internal.amqp.Descriptor -import net.corda.serialization.internal.amqp.DeserializationInput -import net.corda.serialization.internal.amqp.ObjectSerializer -import net.corda.serialization.internal.amqp.PROXY_TYPE -import net.corda.serialization.internal.amqp.Schema -import net.corda.serialization.internal.amqp.SerializationOutput -import net.corda.serialization.internal.amqp.SerializationSchemas -import net.corda.serialization.internal.amqp.SerializerFactory -import net.corda.serialization.internal.amqp.typeDescriptorFor -import net.corda.serialization.internal.model.TypeIdentifier -import org.apache.qpid.proton.amqp.Symbol -import org.apache.qpid.proton.codec.Data -import java.lang.reflect.ParameterizedType -import java.lang.reflect.Type -import java.util.Collections.singleton -import java.util.function.Function - -class SandboxCorDappCustomSerializer( - private val serializerName: String, - classLoader: SandboxClassLoader, - rawTaskFactory: Function>, - factory: SerializerFactory -) : CustomSerializer() { - private val unproxy: Function - private val types: List - - init { - val serializationCustomSerializer = classLoader.toSandboxClass(SerializationCustomSerializer::class.java) - val customSerializerClass = classLoader.toSandboxClass(serializerName) - types = customSerializerClass.genericInterfaces - .mapNotNull { it as? ParameterizedType } - .filter { it.rawType == serializationCustomSerializer } - .flatMap { it.actualTypeArguments.toList() } - if (types.size != 2) { - throw AMQPNotSerializableException( - type = SandboxCorDappCustomSerializer::class.java, - msg = "Unable to determine serializer parent types" - ) - } - - val unproxyTask = classLoader.toSandboxClass(CorDappCustomDeserializer::class.java) - .getConstructor(serializationCustomSerializer) - .newInstance(customSerializerClass.kotlin.objectOrNewInstance()) - unproxy = rawTaskFactory.apply(unproxyTask) - } - - override val schemaForDocumentation: Schema = Schema(emptyList()) - - override val type: Type = types[CORDAPP_TYPE] - private val proxySerializer: ObjectSerializer by lazy { - ObjectSerializer.make(factory.getTypeInformation(types[PROXY_TYPE]), factory) - } - private val deserializationAlias: TypeIdentifier get() = - TypeIdentifier.Erased(AMQPTypeIdentifiers.nameForType(type).replace("sandbox.", ""), 0) - - override val typeDescriptor: Symbol = typeDescriptorFor(type) - override val descriptor: Descriptor = Descriptor(typeDescriptor) - override val deserializationAliases: Set = singleton(deserializationAlias) - - /** - * For 3rd party plugin serializers we are going to exist on exact type matching. i.e. we will - * not support base class serializers for derived types. - */ - override fun isSerializerFor(clazz: Class<*>): Boolean { - return TypeToken.of(type) == TypeToken.of(clazz) - } - - override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput, context: SerializationContext): Any { - return unproxy.apply(proxySerializer.readObject(obj, schemas, input, context))!! - } - - override fun writeClassInfo(output: SerializationOutput) { - abortReadOnly() - } - - override fun writeDescribedObject(obj: Any, data: Data, type: Type, output: SerializationOutput, context: SerializationContext) { - abortReadOnly() - } - - override fun toString(): String = "${this::class.java}($serializerName)" -} \ No newline at end of file diff --git a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxCurrencySerializer.kt b/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxCurrencySerializer.kt deleted file mode 100644 index 758943ee96..0000000000 --- a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxCurrencySerializer.kt +++ /dev/null @@ -1,40 +0,0 @@ -package net.corda.serialization.djvm.serializers - -import net.corda.core.serialization.SerializationContext -import net.corda.djvm.rewiring.SandboxClassLoader -import net.corda.serialization.djvm.deserializers.CreateCurrency -import net.corda.serialization.djvm.toSandboxAnyClass -import net.corda.serialization.internal.amqp.CustomSerializer -import net.corda.serialization.internal.amqp.DeserializationInput -import net.corda.serialization.internal.amqp.Schema -import net.corda.serialization.internal.amqp.SerializationOutput -import net.corda.serialization.internal.amqp.SerializationSchemas -import org.apache.qpid.proton.codec.Data -import java.lang.reflect.Type -import java.util.Currency -import java.util.function.Function - -class SandboxCurrencySerializer( - classLoader: SandboxClassLoader, - taskFactory: Function>, out Function>, - basicInput: Function -) : CustomSerializer.Is(classLoader.toSandboxAnyClass(Currency::class.java)) { - private val creator: Function - - init { - val createTask = taskFactory.apply(CreateCurrency::class.java) - creator = basicInput.andThen(createTask) - } - - override val deserializationAliases = aliasFor(Currency::class.java) - - override val schemaForDocumentation: Schema = Schema(emptyList()) - - override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput, context: SerializationContext): Any { - return creator.apply(obj)!! - } - - override fun writeDescribedObject(obj: Any, data: Data, type: Type, output: SerializationOutput, context: SerializationContext) { - abortReadOnly() - } -} diff --git a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxDecimal128Serializer.kt b/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxDecimal128Serializer.kt deleted file mode 100644 index 234a966988..0000000000 --- a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxDecimal128Serializer.kt +++ /dev/null @@ -1,35 +0,0 @@ -package net.corda.serialization.djvm.serializers - -import net.corda.core.serialization.SerializationContext -import net.corda.djvm.rewiring.SandboxClassLoader -import net.corda.serialization.djvm.deserializers.Decimal128Deserializer -import net.corda.serialization.djvm.toSandboxAnyClass -import net.corda.serialization.internal.amqp.CustomSerializer -import net.corda.serialization.internal.amqp.DeserializationInput -import net.corda.serialization.internal.amqp.Schema -import net.corda.serialization.internal.amqp.SerializationOutput -import net.corda.serialization.internal.amqp.SerializationSchemas -import org.apache.qpid.proton.amqp.Decimal128 -import org.apache.qpid.proton.codec.Data -import java.lang.reflect.Type -import java.util.function.Function - -class SandboxDecimal128Serializer( - classLoader: SandboxClassLoader, - taskFactory: Function>, out Function> -) : CustomSerializer.Is(classLoader.toSandboxAnyClass(Decimal128::class.java)) { - @Suppress("unchecked_cast") - private val transformer: Function - = taskFactory.apply(Decimal128Deserializer::class.java) as Function - - override val schemaForDocumentation: Schema = Schema(emptyList()) - - override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput, context: SerializationContext): Any { - val decimal128 = obj as Decimal128 - return transformer.apply(longArrayOf(decimal128.mostSignificantBits, decimal128.leastSignificantBits))!! - } - - override fun writeDescribedObject(obj: Any, data: Data, type: Type, output: SerializationOutput, context: SerializationContext) { - abortReadOnly() - } -} diff --git a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxDecimal32Serializer.kt b/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxDecimal32Serializer.kt deleted file mode 100644 index 9375431625..0000000000 --- a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxDecimal32Serializer.kt +++ /dev/null @@ -1,34 +0,0 @@ -package net.corda.serialization.djvm.serializers - -import net.corda.core.serialization.SerializationContext -import net.corda.djvm.rewiring.SandboxClassLoader -import net.corda.serialization.djvm.deserializers.Decimal32Deserializer -import net.corda.serialization.djvm.toSandboxAnyClass -import net.corda.serialization.internal.amqp.CustomSerializer -import net.corda.serialization.internal.amqp.DeserializationInput -import net.corda.serialization.internal.amqp.Schema -import net.corda.serialization.internal.amqp.SerializationOutput -import net.corda.serialization.internal.amqp.SerializationSchemas -import org.apache.qpid.proton.amqp.Decimal32 -import org.apache.qpid.proton.codec.Data -import java.lang.reflect.Type -import java.util.function.Function - -class SandboxDecimal32Serializer( - classLoader: SandboxClassLoader, - taskFactory: Function>, out Function> -) : CustomSerializer.Is(classLoader.toSandboxAnyClass(Decimal32::class.java)) { - @Suppress("unchecked_cast") - private val transformer: Function - = taskFactory.apply(Decimal32Deserializer::class.java) as Function - - override val schemaForDocumentation: Schema = Schema(emptyList()) - - override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput, context: SerializationContext): Any { - return transformer.apply(intArrayOf((obj as Decimal32).bits))!! - } - - override fun writeDescribedObject(obj: Any, data: Data, type: Type, output: SerializationOutput, context: SerializationContext) { - abortReadOnly() - } -} diff --git a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxDecimal64Serializer.kt b/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxDecimal64Serializer.kt deleted file mode 100644 index 3461cf7d60..0000000000 --- a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxDecimal64Serializer.kt +++ /dev/null @@ -1,34 +0,0 @@ -package net.corda.serialization.djvm.serializers - -import net.corda.core.serialization.SerializationContext -import net.corda.djvm.rewiring.SandboxClassLoader -import net.corda.serialization.djvm.deserializers.Decimal64Deserializer -import net.corda.serialization.djvm.toSandboxAnyClass -import net.corda.serialization.internal.amqp.CustomSerializer -import net.corda.serialization.internal.amqp.DeserializationInput -import net.corda.serialization.internal.amqp.Schema -import net.corda.serialization.internal.amqp.SerializationOutput -import net.corda.serialization.internal.amqp.SerializationSchemas -import org.apache.qpid.proton.amqp.Decimal64 -import org.apache.qpid.proton.codec.Data -import java.lang.reflect.Type -import java.util.function.Function - -class SandboxDecimal64Serializer( - classLoader: SandboxClassLoader, - taskFactory: Function>, out Function> -) : CustomSerializer.Is(classLoader.toSandboxAnyClass(Decimal64::class.java)) { - @Suppress("unchecked_cast") - private val transformer: Function - = taskFactory.apply(Decimal64Deserializer::class.java) as Function - - override val schemaForDocumentation: Schema = Schema(emptyList()) - - override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput, context: SerializationContext): Any { - return transformer.apply(longArrayOf((obj as Decimal64).bits))!! - } - - override fun writeDescribedObject(obj: Any, data: Data, type: Type, output: SerializationOutput, context: SerializationContext) { - abortReadOnly() - } -} diff --git a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxDurationSerializer.kt b/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxDurationSerializer.kt deleted file mode 100644 index 8b2f8613e2..0000000000 --- a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxDurationSerializer.kt +++ /dev/null @@ -1,30 +0,0 @@ -package net.corda.serialization.djvm.serializers - -import net.corda.djvm.rewiring.SandboxClassLoader -import net.corda.serialization.djvm.deserializers.DurationDeserializer -import net.corda.serialization.djvm.toSandboxAnyClass -import net.corda.serialization.internal.amqp.CustomSerializer -import net.corda.serialization.internal.amqp.SerializerFactory -import net.corda.serialization.internal.amqp.custom.DurationSerializer.DurationProxy -import java.time.Duration -import java.util.function.Function - -class SandboxDurationSerializer( - classLoader: SandboxClassLoader, - taskFactory: Function>, out Function>, - factory: SerializerFactory -) : CustomSerializer.Proxy( - clazz = classLoader.toSandboxAnyClass(Duration::class.java), - proxyClass = classLoader.toSandboxAnyClass(DurationProxy::class.java), - factory = factory -) { - private val task = taskFactory.apply(DurationDeserializer::class.java) - - override val deserializationAliases = aliasFor(Duration::class.java) - - override fun toProxy(obj: Any): Any = abortReadOnly() - - override fun fromProxy(proxy: Any): Any { - return task.apply(proxy)!! - } -} diff --git a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxEnumSerializer.kt b/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxEnumSerializer.kt deleted file mode 100644 index 361b467c08..0000000000 --- a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxEnumSerializer.kt +++ /dev/null @@ -1,121 +0,0 @@ -package net.corda.serialization.djvm.serializers - -import net.corda.core.serialization.SerializationContext -import net.corda.djvm.rewiring.SandboxClassLoader -import net.corda.serialization.djvm.deserializers.CheckEnum -import net.corda.serialization.djvm.deserializers.DescribeEnum -import net.corda.serialization.djvm.deserializers.GetEnumNames -import net.corda.serialization.djvm.toSandboxAnyClass -import net.corda.serialization.internal.amqp.AMQPNotSerializableException -import net.corda.serialization.internal.amqp.AMQPSerializer -import net.corda.serialization.internal.amqp.CustomSerializer -import net.corda.serialization.internal.amqp.DeserializationInput -import net.corda.serialization.internal.amqp.LocalSerializerFactory -import net.corda.serialization.internal.amqp.Schema -import net.corda.serialization.internal.amqp.SerializationOutput -import net.corda.serialization.internal.amqp.SerializationSchemas -import net.corda.serialization.internal.model.EnumTransforms -import net.corda.serialization.internal.model.LocalTypeInformation -import net.corda.serialization.internal.model.TypeIdentifier -import org.apache.qpid.proton.amqp.Symbol -import org.apache.qpid.proton.codec.Data -import java.lang.reflect.Type -import java.util.function.Function -import java.util.function.Predicate - -class SandboxEnumSerializer( - classLoader: SandboxClassLoader, - taskFactory: Function>, out Function>, - predicateFactory: Function>, out Predicate>, - private val localFactory: LocalSerializerFactory -) : CustomSerializer.Implements(clazz = classLoader.toSandboxAnyClass(Enum::class.java)) { - @Suppress("unchecked_cast") - private val describeEnum: Function, Array> - = taskFactory.apply(DescribeEnum::class.java) as Function, Array> - @Suppress("unchecked_cast") - private val getEnumNames: Function, List> - = (taskFactory.apply(GetEnumNames::class.java) as Function, Array>) - .andThen { it.map(Any::toString) } - @Suppress("unchecked_cast") - private val isEnum: Predicate> - = predicateFactory.apply(CheckEnum::class.java) as Predicate> - - override val schemaForDocumentation: Schema = Schema(emptyList()) - - override fun isSerializerFor(clazz: Class<*>): Boolean { - return super.isSerializerFor(clazz) && isEnum.test(clazz) - } - - override fun specialiseFor(declaredType: Type): AMQPSerializer? { - if (declaredType !is Class<*>) { - return null - } - val members = describeEnum.apply(declaredType) - val memberNames = getEnumNames.apply(members) - return ConcreteEnumSerializer(declaredType, members, memberNames, localFactory) - } - - override fun readObject( - obj: Any, schemas: SerializationSchemas, input: DeserializationInput, context: SerializationContext - ): Any { - throw UnsupportedOperationException("Factory only") - } - - override fun writeDescribedObject( - obj: Any, data: Data, type: Type, output: SerializationOutput, context: SerializationContext - ) { - throw UnsupportedOperationException("Factory Only") - } -} - -private class ConcreteEnumSerializer( - declaredType: Class<*>, - private val members: Array, - private val memberNames: List, - factory: LocalSerializerFactory -) : AMQPSerializer { - override val type: Class<*> = declaredType - - override val typeDescriptor: Symbol by lazy { - factory.createDescriptor( - /* - * Partially populated, providing just the information - * required by the fingerprinter. - */ - LocalTypeInformation.AnEnum( - declaredType, - TypeIdentifier.forGenericType(declaredType), - memberNames, - emptyMap(), - emptyList(), - EnumTransforms.empty - ) - ) - } - - override fun readObject( - obj: Any, schemas: SerializationSchemas, input: DeserializationInput, context: SerializationContext - ): Any { - val enumName = (obj as List<*>)[0] as String - val enumOrd = obj[1] as Int - val fromOrd = members[enumOrd] - - if (enumName != memberNames[enumOrd]) { - throw AMQPNotSerializableException( - type, - "Deserializing obj as enum $type with value $enumName.$enumOrd but ordinality has changed" - ) - } - return fromOrd - } - - override fun writeClassInfo(output: SerializationOutput) { - abortReadOnly() - } - - override fun writeObject( - obj: Any, data: Data, type: Type, output: SerializationOutput, context: SerializationContext, debugIndent: Int - ) { - abortReadOnly() - } -} \ No newline at end of file diff --git a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxEnumSetSerializer.kt b/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxEnumSetSerializer.kt deleted file mode 100644 index 4339d472f7..0000000000 --- a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxEnumSetSerializer.kt +++ /dev/null @@ -1,35 +0,0 @@ -package net.corda.serialization.djvm.serializers - -import net.corda.djvm.rewiring.SandboxClassLoader -import net.corda.serialization.djvm.deserializers.EnumSetDeserializer -import net.corda.serialization.djvm.toSandboxAnyClass -import net.corda.serialization.internal.amqp.CustomSerializer -import net.corda.serialization.internal.amqp.SerializerFactory -import net.corda.serialization.internal.amqp.custom.EnumSetSerializer.EnumSetProxy -import java.util.Collections.singleton -import java.util.EnumSet -import java.util.function.Function - -class SandboxEnumSetSerializer( - classLoader: SandboxClassLoader, - taskFactory: Function>, out Function>, - factory: SerializerFactory -) : CustomSerializer.Proxy( - clazz = classLoader.toSandboxAnyClass(EnumSet::class.java), - proxyClass = classLoader.toSandboxAnyClass(EnumSetProxy::class.java), - factory = factory -) { - private val task = taskFactory.apply(EnumSetDeserializer::class.java) - - override val additionalSerializers: Set> = singleton( - SandboxClassSerializer(classLoader, taskFactory, factory) - ) - - override val deserializationAliases = aliasFor(EnumSet::class.java) - - override fun toProxy(obj: Any): Any = abortReadOnly() - - override fun fromProxy(proxy: Any): Any { - return task.apply(proxy)!! - } -} diff --git a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxInputStreamSerializer.kt b/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxInputStreamSerializer.kt deleted file mode 100644 index 03c30cd789..0000000000 --- a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxInputStreamSerializer.kt +++ /dev/null @@ -1,37 +0,0 @@ -package net.corda.serialization.djvm.serializers - -import net.corda.core.serialization.SerializationContext -import net.corda.djvm.rewiring.SandboxClassLoader -import net.corda.serialization.djvm.deserializers.InputStreamDeserializer -import net.corda.serialization.djvm.toSandboxAnyClass -import net.corda.serialization.internal.amqp.CustomSerializer -import net.corda.serialization.internal.amqp.DeserializationInput -import net.corda.serialization.internal.amqp.Schema -import net.corda.serialization.internal.amqp.SerializationOutput -import net.corda.serialization.internal.amqp.SerializationSchemas -import org.apache.qpid.proton.codec.Data -import java.io.InputStream -import java.lang.reflect.Type -import java.util.function.Function - -class SandboxInputStreamSerializer( - classLoader: SandboxClassLoader, - taskFactory: Function>, out Function> -) : CustomSerializer.Implements(classLoader.toSandboxAnyClass(InputStream::class.java)) { - @Suppress("unchecked_cast") - private val decoder: Function - = taskFactory.apply(InputStreamDeserializer::class.java) as Function - - override val schemaForDocumentation: Schema = Schema(emptyList()) - - override val deserializationAliases = aliasFor(InputStream::class.java) - - override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput, context: SerializationContext): Any { - val bits = input.readObject(obj, schemas, ByteArray::class.java, context) as ByteArray - return decoder.apply(bits)!! - } - - override fun writeDescribedObject(obj: Any, data: Data, type: Type, output: SerializationOutput, context: SerializationContext) { - abortReadOnly() - } -} diff --git a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxInstantSerializer.kt b/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxInstantSerializer.kt deleted file mode 100644 index ae6ed2c48f..0000000000 --- a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxInstantSerializer.kt +++ /dev/null @@ -1,30 +0,0 @@ -package net.corda.serialization.djvm.serializers - -import net.corda.djvm.rewiring.SandboxClassLoader -import net.corda.serialization.djvm.deserializers.InstantDeserializer -import net.corda.serialization.djvm.toSandboxAnyClass -import net.corda.serialization.internal.amqp.CustomSerializer -import net.corda.serialization.internal.amqp.SerializerFactory -import net.corda.serialization.internal.amqp.custom.InstantSerializer.InstantProxy -import java.time.Instant -import java.util.function.Function - -class SandboxInstantSerializer( - classLoader: SandboxClassLoader, - taskFactory: Function>, out Function>, - factory: SerializerFactory -) : CustomSerializer.Proxy( - clazz = classLoader.toSandboxAnyClass(Instant::class.java), - proxyClass = classLoader.toSandboxAnyClass(InstantProxy::class.java), - factory = factory -) { - private val task = taskFactory.apply(InstantDeserializer::class.java) - - override val deserializationAliases = aliasFor(Instant::class.java) - - override fun toProxy(obj: Any): Any = abortReadOnly() - - override fun fromProxy(proxy: Any): Any { - return task.apply(proxy)!! - } -} diff --git a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxLocalDateSerializer.kt b/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxLocalDateSerializer.kt deleted file mode 100644 index 9d09131390..0000000000 --- a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxLocalDateSerializer.kt +++ /dev/null @@ -1,30 +0,0 @@ -package net.corda.serialization.djvm.serializers - -import net.corda.djvm.rewiring.SandboxClassLoader -import net.corda.serialization.djvm.deserializers.LocalDateDeserializer -import net.corda.serialization.djvm.toSandboxAnyClass -import net.corda.serialization.internal.amqp.CustomSerializer -import net.corda.serialization.internal.amqp.SerializerFactory -import net.corda.serialization.internal.amqp.custom.LocalDateSerializer.LocalDateProxy -import java.time.LocalDate -import java.util.function.Function - -class SandboxLocalDateSerializer( - classLoader: SandboxClassLoader, - taskFactory: Function>, out Function>, - factory: SerializerFactory -) : CustomSerializer.Proxy( - clazz = classLoader.toSandboxAnyClass(LocalDate::class.java), - proxyClass = classLoader.toSandboxAnyClass(LocalDateProxy::class.java), - factory = factory -) { - private val task = taskFactory.apply(LocalDateDeserializer::class.java) - - override val deserializationAliases = aliasFor(LocalDate::class.java) - - override fun toProxy(obj: Any): Any = abortReadOnly() - - override fun fromProxy(proxy: Any): Any { - return task.apply(proxy)!! - } -} diff --git a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxLocalDateTimeSerializer.kt b/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxLocalDateTimeSerializer.kt deleted file mode 100644 index 8dca5f0617..0000000000 --- a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxLocalDateTimeSerializer.kt +++ /dev/null @@ -1,30 +0,0 @@ -package net.corda.serialization.djvm.serializers - -import net.corda.djvm.rewiring.SandboxClassLoader -import net.corda.serialization.djvm.deserializers.LocalDateTimeDeserializer -import net.corda.serialization.djvm.toSandboxAnyClass -import net.corda.serialization.internal.amqp.CustomSerializer -import net.corda.serialization.internal.amqp.SerializerFactory -import net.corda.serialization.internal.amqp.custom.LocalDateTimeSerializer.LocalDateTimeProxy -import java.time.LocalDateTime -import java.util.function.Function - -class SandboxLocalDateTimeSerializer( - classLoader: SandboxClassLoader, - taskFactory: Function>, out Function>, - factory: SerializerFactory -) : CustomSerializer.Proxy( - clazz = classLoader.toSandboxAnyClass(LocalDateTime::class.java), - proxyClass = classLoader.toSandboxAnyClass(LocalDateTimeProxy::class.java), - factory = factory -) { - private val task = taskFactory.apply(LocalDateTimeDeserializer::class.java) - - override val deserializationAliases = aliasFor(LocalDateTime::class.java) - - override fun toProxy(obj: Any): Any = abortReadOnly() - - override fun fromProxy(proxy: Any): Any { - return task.apply(proxy)!! - } -} diff --git a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxLocalTimeSerializer.kt b/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxLocalTimeSerializer.kt deleted file mode 100644 index 1b545397fa..0000000000 --- a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxLocalTimeSerializer.kt +++ /dev/null @@ -1,30 +0,0 @@ -package net.corda.serialization.djvm.serializers - -import net.corda.djvm.rewiring.SandboxClassLoader -import net.corda.serialization.djvm.deserializers.LocalTimeDeserializer -import net.corda.serialization.djvm.toSandboxAnyClass -import net.corda.serialization.internal.amqp.CustomSerializer -import net.corda.serialization.internal.amqp.SerializerFactory -import net.corda.serialization.internal.amqp.custom.LocalTimeSerializer.LocalTimeProxy -import java.time.LocalTime -import java.util.function.Function - -class SandboxLocalTimeSerializer( - classLoader: SandboxClassLoader, - taskFactory: Function>, out Function>, - factory: SerializerFactory -) : CustomSerializer.Proxy( - clazz = classLoader.toSandboxAnyClass(LocalTime::class.java), - proxyClass = classLoader.toSandboxAnyClass(LocalTimeProxy::class.java), - factory = factory -) { - private val task = taskFactory.apply(LocalTimeDeserializer::class.java) - - override val deserializationAliases = aliasFor(LocalTime::class.java) - - override fun toProxy(obj: Any): Any = abortReadOnly() - - override fun fromProxy(proxy: Any): Any { - return task.apply(proxy)!! - } -} diff --git a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxMapSerializer.kt b/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxMapSerializer.kt deleted file mode 100644 index 6803ff4612..0000000000 --- a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxMapSerializer.kt +++ /dev/null @@ -1,124 +0,0 @@ -package net.corda.serialization.djvm.serializers - -import net.corda.core.serialization.SerializationContext -import net.corda.djvm.rewiring.SandboxClassLoader -import net.corda.serialization.djvm.deserializers.CreateMap -import net.corda.serialization.djvm.toSandboxAnyClass -import net.corda.serialization.internal.amqp.AMQPSerializer -import net.corda.serialization.internal.amqp.CustomSerializer -import net.corda.serialization.internal.amqp.DeserializationInput -import net.corda.serialization.internal.amqp.LocalSerializerFactory -import net.corda.serialization.internal.amqp.Schema -import net.corda.serialization.internal.amqp.SerializationOutput -import net.corda.serialization.internal.amqp.SerializationSchemas -import net.corda.serialization.internal.amqp.redescribe -import net.corda.serialization.internal.model.LocalTypeInformation -import net.corda.serialization.internal.model.TypeIdentifier -import org.apache.qpid.proton.amqp.Symbol -import org.apache.qpid.proton.codec.Data -import java.lang.reflect.ParameterizedType -import java.lang.reflect.Type -import java.util.EnumMap -import java.util.NavigableMap -import java.util.SortedMap -import java.util.TreeMap -import java.util.function.Function - -class SandboxMapSerializer( - classLoader: SandboxClassLoader, - taskFactory: Function>, out Function>, - private val localFactory: LocalSerializerFactory -) : CustomSerializer.Implements(clazz = classLoader.toSandboxAnyClass(Map::class.java)) { - @Suppress("unchecked_cast") - private val creator: Function, out Any?> - = taskFactory.apply(CreateMap::class.java) as Function, out Any?> - - // The order matters here - the first match should be the most specific one. - // Kotlin preserves the ordering for us by associating into a LinkedHashMap. - private val supportedTypes: Map, Class>> = listOf( - TreeMap::class.java, - LinkedHashMap::class.java, - NavigableMap::class.java, - SortedMap::class.java, - EnumMap::class.java, - Map::class.java - ).associateBy { - classLoader.toSandboxAnyClass(it) - } - - private fun getBestMatchFor(type: Class): Map.Entry, Class>> - = supportedTypes.entries.first { it.key.isAssignableFrom(type) } - - override val schemaForDocumentation: Schema = Schema(emptyList()) - - override fun specialiseFor(declaredType: Type): AMQPSerializer? { - if (declaredType !is ParameterizedType) { - return null - } - - @Suppress("unchecked_cast") - val rawType = declaredType.rawType as Class - return ConcreteMapSerializer(declaredType, getBestMatchFor(rawType), creator, localFactory) - } - - override fun readObject( - obj: Any, schemas: SerializationSchemas, input: DeserializationInput, context: SerializationContext - ): Any { - throw UnsupportedOperationException("Factory only") - } - - override fun writeDescribedObject( - obj: Any, data: Data, type: Type, output: SerializationOutput, context: SerializationContext - ) { - throw UnsupportedOperationException("Factory Only") - } -} - -private class ConcreteMapSerializer( - declaredType: ParameterizedType, - private val matchingType: Map.Entry, Class>>, - private val creator: Function, out Any?>, - factory: LocalSerializerFactory -) : AMQPSerializer { - override val type: ParameterizedType = declaredType - - override val typeDescriptor: Symbol by lazy { - factory.createDescriptor( - LocalTypeInformation.AMap( - observedType = declaredType, - typeIdentifier = TypeIdentifier.forGenericType(declaredType), - keyType = factory.getTypeInformation(declaredType.actualTypeArguments[0]), - valueType = factory.getTypeInformation(declaredType.actualTypeArguments[1]) - ) - ) - } - - override fun readObject( - obj: Any, - schemas: SerializationSchemas, - input: DeserializationInput, - context: SerializationContext - ): Any { - val inboundKeyType = type.actualTypeArguments[0] - val inboundValueType = type.actualTypeArguments[1] - return ifThrowsAppend(type::getTypeName) { - val entries = (obj as Map<*, *>).map { - arrayOf( - input.readObjectOrNull(redescribe(it.key, inboundKeyType), schemas, inboundKeyType, context), - input.readObjectOrNull(redescribe(it.value, inboundValueType), schemas, inboundValueType, context) - ) - }.toTypedArray() - creator.apply(arrayOf(matchingType.key, entries))!! - } - } - - override fun writeClassInfo(output: SerializationOutput) { - abortReadOnly() - } - - override fun writeObject( - obj: Any, data: Data, type: Type, output: SerializationOutput, context: SerializationContext, debugIndent: Int - ) { - abortReadOnly() - } -} diff --git a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxMonthDaySerializer.kt b/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxMonthDaySerializer.kt deleted file mode 100644 index 899dc20a7d..0000000000 --- a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxMonthDaySerializer.kt +++ /dev/null @@ -1,30 +0,0 @@ -package net.corda.serialization.djvm.serializers - -import net.corda.djvm.rewiring.SandboxClassLoader -import net.corda.serialization.djvm.deserializers.MonthDayDeserializer -import net.corda.serialization.djvm.toSandboxAnyClass -import net.corda.serialization.internal.amqp.CustomSerializer -import net.corda.serialization.internal.amqp.SerializerFactory -import net.corda.serialization.internal.amqp.custom.MonthDaySerializer.MonthDayProxy -import java.time.MonthDay -import java.util.function.Function - -class SandboxMonthDaySerializer( - classLoader: SandboxClassLoader, - taskFactory: Function>, out Function>, - factory: SerializerFactory -) : CustomSerializer.Proxy( - clazz = classLoader.toSandboxAnyClass(MonthDay::class.java), - proxyClass = classLoader.toSandboxAnyClass(MonthDayProxy::class.java), - factory = factory -) { - private val task = taskFactory.apply(MonthDayDeserializer::class.java) - - override val deserializationAliases = aliasFor(MonthDay::class.java) - - override fun toProxy(obj: Any): Any = abortReadOnly() - - override fun fromProxy(proxy: Any): Any { - return task.apply(proxy)!! - } -} diff --git a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxOffsetDateTimeSerializer.kt b/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxOffsetDateTimeSerializer.kt deleted file mode 100644 index ebff5cdcd3..0000000000 --- a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxOffsetDateTimeSerializer.kt +++ /dev/null @@ -1,30 +0,0 @@ -package net.corda.serialization.djvm.serializers - -import net.corda.djvm.rewiring.SandboxClassLoader -import net.corda.serialization.djvm.deserializers.OffsetDateTimeDeserializer -import net.corda.serialization.djvm.toSandboxAnyClass -import net.corda.serialization.internal.amqp.CustomSerializer -import net.corda.serialization.internal.amqp.SerializerFactory -import net.corda.serialization.internal.amqp.custom.OffsetDateTimeSerializer.OffsetDateTimeProxy -import java.time.OffsetDateTime -import java.util.function.Function - -class SandboxOffsetDateTimeSerializer( - classLoader: SandboxClassLoader, - taskFactory: Function>, out Function>, - factory: SerializerFactory -) : CustomSerializer.Proxy( - clazz = classLoader.toSandboxAnyClass(OffsetDateTime::class.java), - proxyClass = classLoader.toSandboxAnyClass(OffsetDateTimeProxy::class.java), - factory = factory -) { - private val task = taskFactory.apply(OffsetDateTimeDeserializer::class.java) - - override val deserializationAliases = aliasFor(OffsetDateTime::class.java) - - override fun toProxy(obj: Any): Any = abortReadOnly() - - override fun fromProxy(proxy: Any): Any { - return task.apply(proxy)!! - } -} diff --git a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxOffsetTimeSerializer.kt b/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxOffsetTimeSerializer.kt deleted file mode 100644 index 52f5721d13..0000000000 --- a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxOffsetTimeSerializer.kt +++ /dev/null @@ -1,30 +0,0 @@ -package net.corda.serialization.djvm.serializers - -import net.corda.djvm.rewiring.SandboxClassLoader -import net.corda.serialization.djvm.deserializers.OffsetTimeDeserializer -import net.corda.serialization.djvm.toSandboxAnyClass -import net.corda.serialization.internal.amqp.CustomSerializer -import net.corda.serialization.internal.amqp.SerializerFactory -import net.corda.serialization.internal.amqp.custom.OffsetTimeSerializer.OffsetTimeProxy -import java.time.OffsetTime -import java.util.function.Function - -class SandboxOffsetTimeSerializer( - classLoader: SandboxClassLoader, - taskFactory: Function>, out Function>, - factory: SerializerFactory -) : CustomSerializer.Proxy( - clazz = classLoader.toSandboxAnyClass(OffsetTime::class.java), - proxyClass = classLoader.toSandboxAnyClass(OffsetTimeProxy::class.java), - factory = factory -) { - private val task = taskFactory.apply(OffsetTimeDeserializer::class.java) - - override val deserializationAliases = aliasFor(OffsetTime::class.java) - - override fun toProxy(obj: Any): Any = abortReadOnly() - - override fun fromProxy(proxy: Any): Any { - return task.apply(proxy)!! - } -} diff --git a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxOpaqueBytesSubSequenceSerializer.kt b/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxOpaqueBytesSubSequenceSerializer.kt deleted file mode 100644 index 35ac384f5e..0000000000 --- a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxOpaqueBytesSubSequenceSerializer.kt +++ /dev/null @@ -1,30 +0,0 @@ -package net.corda.serialization.djvm.serializers - -import net.corda.core.utilities.OpaqueBytes -import net.corda.core.utilities.OpaqueBytesSubSequence -import net.corda.djvm.rewiring.SandboxClassLoader -import net.corda.serialization.djvm.deserializers.OpaqueBytesSubSequenceDeserializer -import net.corda.serialization.djvm.toSandboxAnyClass -import net.corda.serialization.internal.amqp.CustomSerializer -import net.corda.serialization.internal.amqp.SerializerFactory -import java.util.function.Function - -class SandboxOpaqueBytesSubSequenceSerializer( - classLoader: SandboxClassLoader, - taskFactory: Function>, out Function>, - factory: SerializerFactory -) : CustomSerializer.Proxy( - clazz = classLoader.toSandboxAnyClass(OpaqueBytesSubSequence::class.java), - proxyClass = classLoader.toSandboxAnyClass(OpaqueBytes::class.java), - factory = factory -) { - private val task = taskFactory.apply(OpaqueBytesSubSequenceDeserializer::class.java) - - override val deserializationAliases = aliasFor(OpaqueBytesSubSequence::class.java) - - override fun toProxy(obj: Any): Any = abortReadOnly() - - override fun fromProxy(proxy: Any): Any { - return task.apply(proxy)!! - } -} diff --git a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxOptionalSerializer.kt b/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxOptionalSerializer.kt deleted file mode 100644 index 82a633d603..0000000000 --- a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxOptionalSerializer.kt +++ /dev/null @@ -1,30 +0,0 @@ -package net.corda.serialization.djvm.serializers - -import net.corda.djvm.rewiring.SandboxClassLoader -import net.corda.serialization.djvm.deserializers.OptionalDeserializer -import net.corda.serialization.djvm.toSandboxAnyClass -import net.corda.serialization.internal.amqp.CustomSerializer -import net.corda.serialization.internal.amqp.SerializerFactory -import net.corda.serialization.internal.amqp.custom.OptionalSerializer.OptionalProxy -import java.util.Optional -import java.util.function.Function - -class SandboxOptionalSerializer( - classLoader: SandboxClassLoader, - taskFactory: Function>, out Function>, - factory: SerializerFactory -) : CustomSerializer.Proxy( - clazz = classLoader.toSandboxAnyClass(Optional::class.java), - proxyClass = classLoader.toSandboxAnyClass(OptionalProxy::class.java), - factory = factory -) { - private val task = taskFactory.apply(OptionalDeserializer::class.java) - - override val deserializationAliases = aliasFor(Optional::class.java) - - override fun toProxy(obj: Any): Any = abortReadOnly() - - override fun fromProxy(proxy: Any): Any { - return task.apply(proxy)!! - } -} diff --git a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxPeriodSerializer.kt b/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxPeriodSerializer.kt deleted file mode 100644 index 2f82e02e80..0000000000 --- a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxPeriodSerializer.kt +++ /dev/null @@ -1,30 +0,0 @@ -package net.corda.serialization.djvm.serializers - -import net.corda.djvm.rewiring.SandboxClassLoader -import net.corda.serialization.djvm.deserializers.PeriodDeserializer -import net.corda.serialization.djvm.toSandboxAnyClass -import net.corda.serialization.internal.amqp.CustomSerializer -import net.corda.serialization.internal.amqp.SerializerFactory -import net.corda.serialization.internal.amqp.custom.PeriodSerializer.PeriodProxy -import java.time.Period -import java.util.function.Function - -class SandboxPeriodSerializer( - classLoader: SandboxClassLoader, - taskFactory: Function>, out Function>, - factory: SerializerFactory -) : CustomSerializer.Proxy( - clazz = classLoader.toSandboxAnyClass(Period::class.java), - proxyClass = classLoader.toSandboxAnyClass(PeriodProxy::class.java), - factory = factory -) { - private val task = taskFactory.apply(PeriodDeserializer::class.java) - - override val deserializationAliases = aliasFor(Period::class.java) - - override fun toProxy(obj: Any): Any = abortReadOnly() - - override fun fromProxy(proxy: Any): Any { - return task.apply(proxy)!! - } -} diff --git a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxPrimitiveSerializer.kt b/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxPrimitiveSerializer.kt deleted file mode 100644 index 9af70c9d4a..0000000000 --- a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxPrimitiveSerializer.kt +++ /dev/null @@ -1,30 +0,0 @@ -package net.corda.serialization.djvm.serializers - -import net.corda.core.serialization.SerializationContext -import net.corda.djvm.rewiring.SandboxClassLoader -import net.corda.serialization.djvm.toSandboxAnyClass -import net.corda.serialization.internal.amqp.CustomSerializer -import net.corda.serialization.internal.amqp.DeserializationInput -import net.corda.serialization.internal.amqp.Schema -import net.corda.serialization.internal.amqp.SerializationOutput -import net.corda.serialization.internal.amqp.SerializationSchemas -import org.apache.qpid.proton.codec.Data -import java.lang.reflect.Type -import java.util.function.Function - -class SandboxPrimitiveSerializer( - clazz: Class<*>, - classLoader: SandboxClassLoader, - private val basicInput: Function -) : CustomSerializer.Is(classLoader.toSandboxAnyClass(clazz)) { - - override val schemaForDocumentation: Schema = Schema(emptyList()) - - override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput, context: SerializationContext): Any { - return basicInput.apply(obj)!! - } - - override fun writeDescribedObject(obj: Any, data: Data, type: Type, output: SerializationOutput, context: SerializationContext) { - abortReadOnly() - } -} diff --git a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxPublicKeySerializer.kt b/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxPublicKeySerializer.kt deleted file mode 100644 index f826672647..0000000000 --- a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxPublicKeySerializer.kt +++ /dev/null @@ -1,41 +0,0 @@ -package net.corda.serialization.djvm.serializers - -import net.corda.core.serialization.DESERIALIZATION_CACHE_PROPERTY -import net.corda.core.serialization.SerializationContext -import net.corda.djvm.rewiring.SandboxClassLoader -import net.corda.serialization.djvm.deserializers.PublicKeyDecoder -import net.corda.serialization.djvm.toSandboxAnyClass -import net.corda.serialization.internal.amqp.CustomSerializer -import net.corda.serialization.internal.amqp.DeserializationInput -import net.corda.serialization.internal.amqp.Schema -import net.corda.serialization.internal.amqp.SerializationOutput -import net.corda.serialization.internal.amqp.SerializationSchemas -import org.apache.qpid.proton.codec.Data -import java.lang.reflect.Type -import java.security.PublicKey -import java.util.function.Function - -class SandboxPublicKeySerializer( - classLoader: SandboxClassLoader, - taskFactory: Function>, out Function> -) : CustomSerializer.Implements(classLoader.toSandboxAnyClass(PublicKey::class.java)) { - @Suppress("unchecked_cast") - private val decoder = taskFactory.apply(PublicKeyDecoder::class.java) as Function - - override val schemaForDocumentation: Schema = Schema(emptyList()) - - override val deserializationAliases = aliasFor(PublicKey::class.java) - - override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput, context: SerializationContext): Any { - val bits = input.readObject(obj, schemas, ByteArray::class.java, context) as ByteArray - @Suppress("unchecked_cast") - return (context.properties[DESERIALIZATION_CACHE_PROPERTY] as? MutableMap) - ?.computeIfAbsent(CacheKey(bits)) { key -> - decoder.apply(key.bytes) - } ?: decoder.apply(bits)!! - } - - override fun writeDescribedObject(obj: Any, data: Data, type: Type, output: SerializationOutput, context: SerializationContext) { - abortReadOnly() - } -} diff --git a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxSymbolSerializer.kt b/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxSymbolSerializer.kt deleted file mode 100644 index 2dd0ba116c..0000000000 --- a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxSymbolSerializer.kt +++ /dev/null @@ -1,39 +0,0 @@ -package net.corda.serialization.djvm.serializers - -import net.corda.core.serialization.SerializationContext -import net.corda.djvm.rewiring.SandboxClassLoader -import net.corda.serialization.djvm.deserializers.SymbolDeserializer -import net.corda.serialization.djvm.toSandboxAnyClass -import net.corda.serialization.internal.amqp.CustomSerializer -import net.corda.serialization.internal.amqp.DeserializationInput -import net.corda.serialization.internal.amqp.Schema -import net.corda.serialization.internal.amqp.SerializationOutput -import net.corda.serialization.internal.amqp.SerializationSchemas -import org.apache.qpid.proton.amqp.Symbol -import org.apache.qpid.proton.codec.Data -import java.lang.reflect.Type -import java.util.function.Function - -class SandboxSymbolSerializer( - classLoader: SandboxClassLoader, - taskFactory: Function>, out Function>, - basicInput: Function -) : CustomSerializer.Is(classLoader.toSandboxAnyClass(Symbol::class.java)) { - private val transformer: Function - - init { - val transformTask = taskFactory.apply(SymbolDeserializer::class.java) - @Suppress("unchecked_cast") - transformer = basicInput.andThen(transformTask) as Function - } - - override val schemaForDocumentation: Schema = Schema(emptyList()) - - override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput, context: SerializationContext): Any { - return transformer.apply((obj as Symbol).toString())!! - } - - override fun writeDescribedObject(obj: Any, data: Data, type: Type, output: SerializationOutput, context: SerializationContext) { - abortReadOnly() - } -} diff --git a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxToStringSerializer.kt b/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxToStringSerializer.kt deleted file mode 100644 index f3f17b67ab..0000000000 --- a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxToStringSerializer.kt +++ /dev/null @@ -1,39 +0,0 @@ -package net.corda.serialization.djvm.serializers - -import net.corda.core.serialization.SerializationContext -import net.corda.djvm.rewiring.SandboxClassLoader -import net.corda.serialization.djvm.toSandboxAnyClass -import net.corda.serialization.internal.amqp.CustomSerializer -import net.corda.serialization.internal.amqp.DeserializationInput -import net.corda.serialization.internal.amqp.Schema -import net.corda.serialization.internal.amqp.SerializationOutput -import net.corda.serialization.internal.amqp.SerializationSchemas -import org.apache.qpid.proton.codec.Data -import java.lang.reflect.Type -import java.util.function.Function - -class SandboxToStringSerializer( - unsafeClass: Class<*>, - classLoader: SandboxClassLoader, - basicInput: Function -) : CustomSerializer.Is(classLoader.toSandboxAnyClass(unsafeClass)) { - private val creator: Function - - init { - val stringClass = classLoader.loadClass("sandbox.java.lang.String") - val clazzConstructor = clazz.getConstructor(stringClass) - creator = basicInput.andThen { s -> clazzConstructor.newInstance(s) } - } - - override val deserializationAliases = aliasFor(unsafeClass) - - override val schemaForDocumentation: Schema = Schema(emptyList()) - - override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput, context: SerializationContext): Any { - return creator.apply(obj)!! - } - - override fun writeDescribedObject(obj: Any, data: Data, type: Type, output: SerializationOutput, context: SerializationContext) { - abortReadOnly() - } -} diff --git a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxUnsignedByteSerializer.kt b/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxUnsignedByteSerializer.kt deleted file mode 100644 index f871186997..0000000000 --- a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxUnsignedByteSerializer.kt +++ /dev/null @@ -1,34 +0,0 @@ -package net.corda.serialization.djvm.serializers - -import net.corda.core.serialization.SerializationContext -import net.corda.djvm.rewiring.SandboxClassLoader -import net.corda.serialization.djvm.deserializers.UnsignedByteDeserializer -import net.corda.serialization.djvm.toSandboxAnyClass -import net.corda.serialization.internal.amqp.CustomSerializer -import net.corda.serialization.internal.amqp.DeserializationInput -import net.corda.serialization.internal.amqp.Schema -import net.corda.serialization.internal.amqp.SerializationOutput -import net.corda.serialization.internal.amqp.SerializationSchemas -import org.apache.qpid.proton.amqp.UnsignedByte -import org.apache.qpid.proton.codec.Data -import java.lang.reflect.Type -import java.util.function.Function - -class SandboxUnsignedByteSerializer( - classLoader: SandboxClassLoader, - taskFactory: Function>, out Function> -) : CustomSerializer.Is(classLoader.toSandboxAnyClass(UnsignedByte::class.java)) { - @Suppress("unchecked_cast") - private val transformer: Function - = taskFactory.apply(UnsignedByteDeserializer::class.java) as Function - - override val schemaForDocumentation: Schema = Schema(emptyList()) - - override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput, context: SerializationContext): Any { - return transformer.apply(byteArrayOf((obj as UnsignedByte).toByte()))!! - } - - override fun writeDescribedObject(obj: Any, data: Data, type: Type, output: SerializationOutput, context: SerializationContext) { - abortReadOnly() - } -} diff --git a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxUnsignedIntegerSerializer.kt b/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxUnsignedIntegerSerializer.kt deleted file mode 100644 index 7e612d7a10..0000000000 --- a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxUnsignedIntegerSerializer.kt +++ /dev/null @@ -1,34 +0,0 @@ -package net.corda.serialization.djvm.serializers - -import net.corda.core.serialization.SerializationContext -import net.corda.djvm.rewiring.SandboxClassLoader -import net.corda.serialization.djvm.deserializers.UnsignedIntegerDeserializer -import net.corda.serialization.djvm.toSandboxAnyClass -import net.corda.serialization.internal.amqp.CustomSerializer -import net.corda.serialization.internal.amqp.DeserializationInput -import net.corda.serialization.internal.amqp.Schema -import net.corda.serialization.internal.amqp.SerializationOutput -import net.corda.serialization.internal.amqp.SerializationSchemas -import org.apache.qpid.proton.amqp.UnsignedInteger -import org.apache.qpid.proton.codec.Data -import java.lang.reflect.Type -import java.util.function.Function - -class SandboxUnsignedIntegerSerializer( - classLoader: SandboxClassLoader, - taskFactory: Function>, out Function> -) : CustomSerializer.Is(classLoader.toSandboxAnyClass(UnsignedInteger::class.java)) { - @Suppress("unchecked_cast") - private val transformer: Function - = taskFactory.apply(UnsignedIntegerDeserializer::class.java) as Function - - override val schemaForDocumentation: Schema = Schema(emptyList()) - - override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput, context: SerializationContext): Any { - return transformer.apply(intArrayOf((obj as UnsignedInteger).toInt()))!! - } - - override fun writeDescribedObject(obj: Any, data: Data, type: Type, output: SerializationOutput, context: SerializationContext) { - abortReadOnly() - } -} diff --git a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxUnsignedLongSerializer.kt b/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxUnsignedLongSerializer.kt deleted file mode 100644 index b25404377f..0000000000 --- a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxUnsignedLongSerializer.kt +++ /dev/null @@ -1,34 +0,0 @@ -package net.corda.serialization.djvm.serializers - -import net.corda.core.serialization.SerializationContext -import net.corda.djvm.rewiring.SandboxClassLoader -import net.corda.serialization.djvm.deserializers.UnsignedLongDeserializer -import net.corda.serialization.djvm.toSandboxAnyClass -import net.corda.serialization.internal.amqp.CustomSerializer -import net.corda.serialization.internal.amqp.DeserializationInput -import net.corda.serialization.internal.amqp.Schema -import net.corda.serialization.internal.amqp.SerializationOutput -import net.corda.serialization.internal.amqp.SerializationSchemas -import org.apache.qpid.proton.amqp.UnsignedLong -import org.apache.qpid.proton.codec.Data -import java.lang.reflect.Type -import java.util.function.Function - -class SandboxUnsignedLongSerializer( - classLoader: SandboxClassLoader, - taskFactory: Function>, out Function> -) : CustomSerializer.Is(classLoader.toSandboxAnyClass(UnsignedLong::class.java)) { - @Suppress("unchecked_cast") - private val transformer: Function - = taskFactory.apply(UnsignedLongDeserializer::class.java) as Function - - override val schemaForDocumentation: Schema = Schema(emptyList()) - - override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput, context: SerializationContext): Any { - return transformer.apply(longArrayOf((obj as UnsignedLong).toLong()))!! - } - - override fun writeDescribedObject(obj: Any, data: Data, type: Type, output: SerializationOutput, context: SerializationContext) { - abortReadOnly() - } -} \ No newline at end of file diff --git a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxUnsignedShortSerializer.kt b/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxUnsignedShortSerializer.kt deleted file mode 100644 index a999825eb7..0000000000 --- a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxUnsignedShortSerializer.kt +++ /dev/null @@ -1,34 +0,0 @@ -package net.corda.serialization.djvm.serializers - -import net.corda.core.serialization.SerializationContext -import net.corda.djvm.rewiring.SandboxClassLoader -import net.corda.serialization.djvm.deserializers.UnsignedShortDeserializer -import net.corda.serialization.djvm.toSandboxAnyClass -import net.corda.serialization.internal.amqp.CustomSerializer -import net.corda.serialization.internal.amqp.DeserializationInput -import net.corda.serialization.internal.amqp.Schema -import net.corda.serialization.internal.amqp.SerializationOutput -import net.corda.serialization.internal.amqp.SerializationSchemas -import org.apache.qpid.proton.amqp.UnsignedShort -import org.apache.qpid.proton.codec.Data -import java.lang.reflect.Type -import java.util.function.Function - -class SandboxUnsignedShortSerializer( - classLoader: SandboxClassLoader, - taskFactory: Function>, out Function> -) : CustomSerializer.Is(classLoader.toSandboxAnyClass(UnsignedShort::class.java)) { - @Suppress("unchecked_cast") - private val transformer: Function - = taskFactory.apply(UnsignedShortDeserializer::class.java) as Function - - override val schemaForDocumentation: Schema = Schema(emptyList()) - - override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput, context: SerializationContext): Any { - return transformer.apply(shortArrayOf((obj as UnsignedShort).toShort()))!! - } - - override fun writeDescribedObject(obj: Any, data: Data, type: Type, output: SerializationOutput, context: SerializationContext) { - abortReadOnly() - } -} diff --git a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxX509CRLSerializer.kt b/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxX509CRLSerializer.kt deleted file mode 100644 index 0c19470e25..0000000000 --- a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxX509CRLSerializer.kt +++ /dev/null @@ -1,42 +0,0 @@ -package net.corda.serialization.djvm.serializers - -import net.corda.core.serialization.DESERIALIZATION_CACHE_PROPERTY -import net.corda.core.serialization.SerializationContext -import net.corda.djvm.rewiring.SandboxClassLoader -import net.corda.serialization.djvm.deserializers.X509CRLDeserializer -import net.corda.serialization.djvm.toSandboxAnyClass -import net.corda.serialization.internal.amqp.CustomSerializer -import net.corda.serialization.internal.amqp.DeserializationInput -import net.corda.serialization.internal.amqp.Schema -import net.corda.serialization.internal.amqp.SerializationOutput -import net.corda.serialization.internal.amqp.SerializationSchemas -import org.apache.qpid.proton.codec.Data -import java.lang.reflect.Type -import java.security.cert.X509CRL -import java.util.function.Function - -class SandboxX509CRLSerializer( - classLoader: SandboxClassLoader, - taskFactory: Function>, out Function> -) : CustomSerializer.Implements(classLoader.toSandboxAnyClass(X509CRL::class.java)) { - @Suppress("unchecked_cast") - private val generator: Function - = taskFactory.apply(X509CRLDeserializer::class.java) as Function - - override val schemaForDocumentation: Schema = Schema(emptyList()) - - override val deserializationAliases = aliasFor(X509CRL::class.java) - - override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput, context: SerializationContext): Any { - val bits = input.readObject(obj, schemas, ByteArray::class.java, context) as ByteArray - @Suppress("unchecked_cast") - return (context.properties[DESERIALIZATION_CACHE_PROPERTY] as? MutableMap) - ?.computeIfAbsent(CacheKey(bits)) { key -> - generator.apply(key.bytes) - } ?: generator.apply(bits)!! - } - - override fun writeDescribedObject(obj: Any, data: Data, type: Type, output: SerializationOutput, context: SerializationContext) { - abortReadOnly() - } -} diff --git a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxX509CertificateSerializer.kt b/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxX509CertificateSerializer.kt deleted file mode 100644 index cf6a78da7e..0000000000 --- a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxX509CertificateSerializer.kt +++ /dev/null @@ -1,42 +0,0 @@ -package net.corda.serialization.djvm.serializers - -import net.corda.core.serialization.DESERIALIZATION_CACHE_PROPERTY -import net.corda.core.serialization.SerializationContext -import net.corda.djvm.rewiring.SandboxClassLoader -import net.corda.serialization.djvm.deserializers.X509CertificateDeserializer -import net.corda.serialization.djvm.toSandboxAnyClass -import net.corda.serialization.internal.amqp.CustomSerializer -import net.corda.serialization.internal.amqp.DeserializationInput -import net.corda.serialization.internal.amqp.Schema -import net.corda.serialization.internal.amqp.SerializationOutput -import net.corda.serialization.internal.amqp.SerializationSchemas -import org.apache.qpid.proton.codec.Data -import java.lang.reflect.Type -import java.security.cert.X509Certificate -import java.util.function.Function - -class SandboxX509CertificateSerializer( - classLoader: SandboxClassLoader, - taskFactory: Function>, out Function> -) : CustomSerializer.Implements(classLoader.toSandboxAnyClass(X509Certificate::class.java)) { - @Suppress("unchecked_cast") - private val generator: Function - = taskFactory.apply(X509CertificateDeserializer::class.java) as Function - - override val schemaForDocumentation: Schema = Schema(emptyList()) - - override val deserializationAliases = aliasFor(X509Certificate::class.java) - - override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput, context: SerializationContext): Any { - val bits = input.readObject(obj, schemas, ByteArray::class.java, context) as ByteArray - @Suppress("unchecked_cast") - return (context.properties[DESERIALIZATION_CACHE_PROPERTY] as? MutableMap) - ?.computeIfAbsent(CacheKey(bits)) { key -> - generator.apply(key.bytes) - } ?: generator.apply(bits)!! - } - - override fun writeDescribedObject(obj: Any, data: Data, type: Type, output: SerializationOutput, context: SerializationContext) { - abortReadOnly() - } -} diff --git a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxYearMonthSerializer.kt b/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxYearMonthSerializer.kt deleted file mode 100644 index 051b812368..0000000000 --- a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxYearMonthSerializer.kt +++ /dev/null @@ -1,30 +0,0 @@ -package net.corda.serialization.djvm.serializers - -import net.corda.djvm.rewiring.SandboxClassLoader -import net.corda.serialization.djvm.deserializers.YearMonthDeserializer -import net.corda.serialization.djvm.toSandboxAnyClass -import net.corda.serialization.internal.amqp.CustomSerializer -import net.corda.serialization.internal.amqp.SerializerFactory -import net.corda.serialization.internal.amqp.custom.YearMonthSerializer.YearMonthProxy -import java.time.YearMonth -import java.util.function.Function - -class SandboxYearMonthSerializer( - classLoader: SandboxClassLoader, - taskFactory: Function>, out Function>, - factory: SerializerFactory -) : CustomSerializer.Proxy( - clazz = classLoader.toSandboxAnyClass(YearMonth::class.java), - proxyClass = classLoader.toSandboxAnyClass(YearMonthProxy::class.java), - factory = factory -) { - private val task = taskFactory.apply(YearMonthDeserializer::class.java) - - override val deserializationAliases = aliasFor(YearMonth::class.java) - - override fun toProxy(obj: Any): Any = abortReadOnly() - - override fun fromProxy(proxy: Any): Any { - return task.apply(proxy)!! - } -} diff --git a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxYearSerializer.kt b/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxYearSerializer.kt deleted file mode 100644 index 776a1ada15..0000000000 --- a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxYearSerializer.kt +++ /dev/null @@ -1,30 +0,0 @@ -package net.corda.serialization.djvm.serializers - -import net.corda.djvm.rewiring.SandboxClassLoader -import net.corda.serialization.djvm.deserializers.YearDeserializer -import net.corda.serialization.djvm.toSandboxAnyClass -import net.corda.serialization.internal.amqp.CustomSerializer -import net.corda.serialization.internal.amqp.SerializerFactory -import net.corda.serialization.internal.amqp.custom.YearSerializer.YearProxy -import java.time.Year -import java.util.function.Function - -class SandboxYearSerializer( - classLoader: SandboxClassLoader, - taskFactory: Function>, out Function>, - factory: SerializerFactory -) : CustomSerializer.Proxy( - clazz = classLoader.toSandboxAnyClass(Year::class.java), - proxyClass = classLoader.toSandboxAnyClass(YearProxy::class.java), - factory = factory -) { - private val task = taskFactory.apply(YearDeserializer::class.java) - - override val deserializationAliases = aliasFor(Year::class.java) - - override fun toProxy(obj: Any): Any = abortReadOnly() - - override fun fromProxy(proxy: Any): Any { - return task.apply(proxy)!! - } -} diff --git a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxZoneIdSerializer.kt b/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxZoneIdSerializer.kt deleted file mode 100644 index 722c0ad255..0000000000 --- a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxZoneIdSerializer.kt +++ /dev/null @@ -1,32 +0,0 @@ -package net.corda.serialization.djvm.serializers - -import net.corda.djvm.rewiring.SandboxClassLoader -import net.corda.serialization.djvm.deserializers.ZoneIdDeserializer -import net.corda.serialization.djvm.toSandboxAnyClass -import net.corda.serialization.internal.amqp.CustomSerializer -import net.corda.serialization.internal.amqp.SerializerFactory -import net.corda.serialization.internal.amqp.custom.ZoneIdSerializer.ZoneIdProxy -import java.time.ZoneId -import java.util.function.Function - -class SandboxZoneIdSerializer( - classLoader: SandboxClassLoader, - taskFactory: Function>, out Function>, - factory: SerializerFactory -) : CustomSerializer.Proxy( - clazz = classLoader.toSandboxAnyClass(ZoneId::class.java), - proxyClass = classLoader.toSandboxAnyClass(ZoneIdProxy::class.java), - factory = factory -) { - private val task = taskFactory.apply(ZoneIdDeserializer::class.java) - - override val revealSubclassesInSchema: Boolean = true - - override val deserializationAliases = aliasFor(ZoneId::class.java) - - override fun toProxy(obj: Any): Any = abortReadOnly() - - override fun fromProxy(proxy: Any): Any { - return task.apply(proxy)!! - } -} diff --git a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxZonedDateTimeSerializer.kt b/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxZonedDateTimeSerializer.kt deleted file mode 100644 index ea9126c772..0000000000 --- a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/SandboxZonedDateTimeSerializer.kt +++ /dev/null @@ -1,47 +0,0 @@ -package net.corda.serialization.djvm.serializers - -import net.corda.djvm.rewiring.SandboxClassLoader -import net.corda.serialization.djvm.deserializers.ZonedDateTimeDeserializer -import net.corda.serialization.djvm.toSandboxAnyClass -import net.corda.serialization.internal.amqp.CustomSerializer -import net.corda.serialization.internal.amqp.SerializerFactory -import net.corda.serialization.internal.amqp.custom.ZonedDateTimeSerializer.ZonedDateTimeProxy -import java.time.LocalDateTime -import java.time.ZoneId -import java.time.ZoneOffset -import java.time.ZonedDateTime -import java.util.function.Function - -class SandboxZonedDateTimeSerializer( - classLoader: SandboxClassLoader, - taskFactory: Function>, out Function>, - factory: SerializerFactory -) : CustomSerializer.Proxy( - clazz = classLoader.toSandboxAnyClass(ZonedDateTime::class.java), - proxyClass = classLoader.toSandboxAnyClass(ZonedDateTimeProxy::class.java), - factory = factory -) { - private val task = taskFactory.apply(ZonedDateTimeDeserializer::class.java) - private val creator: Function - - init { - val createTask = clazz.getMethod( - "createDJVM", - classLoader.toSandboxClass(LocalDateTime::class.java), - classLoader.toSandboxClass(ZoneOffset::class.java), - classLoader.toSandboxClass(ZoneId::class.java) - ) - creator = task.andThen { input -> - @Suppress("unchecked_cast", "SpreadOperator") - createTask(null, *(input as Array))!! - } - } - - override val deserializationAliases = aliasFor(ZonedDateTime::class.java) - - override fun toProxy(obj: Any): Any = abortReadOnly() - - override fun fromProxy(proxy: Any): Any { - return creator.apply(proxy)!! - } -} diff --git a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/Serializers.kt b/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/Serializers.kt deleted file mode 100644 index cd67433f37..0000000000 --- a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/serializers/Serializers.kt +++ /dev/null @@ -1,8 +0,0 @@ -@file:JvmName("Serializers") -package net.corda.serialization.djvm.serializers - -import net.corda.serialization.internal.model.TypeIdentifier -import java.lang.reflect.Type -import java.util.Collections.singleton - -fun aliasFor(type: Type): Set = singleton(TypeIdentifier.forGenericType(type)) diff --git a/serialization-djvm/src/test/java/greymalkin/ExternalData.java b/serialization-djvm/src/test/java/greymalkin/ExternalData.java deleted file mode 100644 index 8774478bdb..0000000000 --- a/serialization-djvm/src/test/java/greymalkin/ExternalData.java +++ /dev/null @@ -1,13 +0,0 @@ -package greymalkin; - -public class ExternalData { - private final String data; - - public ExternalData(String data) { - this.data = data; - } - - public String getData() { - return data; - } -} diff --git a/serialization-djvm/src/test/java/greymalkin/ExternalEnum.java b/serialization-djvm/src/test/java/greymalkin/ExternalEnum.java deleted file mode 100644 index 2180693367..0000000000 --- a/serialization-djvm/src/test/java/greymalkin/ExternalEnum.java +++ /dev/null @@ -1,11 +0,0 @@ -package greymalkin; - -import net.corda.core.serialization.CordaSerializable; - -@SuppressWarnings("unused") -@CordaSerializable -public enum ExternalEnum { - DOH, - RAY, - ME -} diff --git a/serialization-djvm/src/test/java/net/corda/serialization/djvm/InnocentData.java b/serialization-djvm/src/test/java/net/corda/serialization/djvm/InnocentData.java deleted file mode 100644 index 1f350f940d..0000000000 --- a/serialization-djvm/src/test/java/net/corda/serialization/djvm/InnocentData.java +++ /dev/null @@ -1,22 +0,0 @@ -package net.corda.serialization.djvm; - -import net.corda.core.serialization.CordaSerializable; - -@CordaSerializable -public class InnocentData { - private final String message; - private final Short number; - - public InnocentData(String message, Short number) { - this.message = message; - this.number = number; - } - - public String getMessage() { - return message; - } - - public Short getNumber() { - return number; - } -} diff --git a/serialization-djvm/src/test/java/net/corda/serialization/djvm/MultiConstructorData.java b/serialization-djvm/src/test/java/net/corda/serialization/djvm/MultiConstructorData.java deleted file mode 100644 index 9a6e66d86e..0000000000 --- a/serialization-djvm/src/test/java/net/corda/serialization/djvm/MultiConstructorData.java +++ /dev/null @@ -1,53 +0,0 @@ -package net.corda.serialization.djvm; - -import net.corda.core.serialization.ConstructorForDeserialization; -import net.corda.core.serialization.CordaSerializable; - -@SuppressWarnings({"unused", "WeakerAccess"}) -@CordaSerializable -public class MultiConstructorData { - private final String message; - private final long bigNumber; - private final Character tag; - - @ConstructorForDeserialization - public MultiConstructorData(String message, long bigNumber, Character tag) { - this.message = message; - this.bigNumber = bigNumber; - this.tag = tag; - } - - public MultiConstructorData(String message, long bigNumber) { - this(message, bigNumber, null); - } - - public MultiConstructorData(String message, char tag) { - this(message, 0, tag); - } - - public MultiConstructorData(String message) { - this(message, 0); - } - - public String getMessage() { - return message; - } - - public long getBigNumber() { - return bigNumber; - } - - public Character getTag() { - return tag; - } - - @SuppressWarnings("StringBufferReplaceableByString") - @Override - public String toString() { - return new StringBuilder("MultiConstructor[message='").append(message) - .append("', bigNumber=").append(bigNumber) - .append(", tag=").append(tag) - .append(']') - .toString(); - } -} diff --git a/serialization-djvm/src/test/java/net/corda/serialization/djvm/VeryEvilData.java b/serialization-djvm/src/test/java/net/corda/serialization/djvm/VeryEvilData.java deleted file mode 100644 index d1ea8f0ad0..0000000000 --- a/serialization-djvm/src/test/java/net/corda/serialization/djvm/VeryEvilData.java +++ /dev/null @@ -1,15 +0,0 @@ -package net.corda.serialization.djvm; - -@SuppressWarnings("unused") -public class VeryEvilData extends InnocentData { - static { - if (!VeryEvilData.class.getName().startsWith("sandbox.")) { - // Execute our evil payload OUTSIDE the sandbox! - throw new IllegalStateException("Victory is mine!"); - } - } - - public VeryEvilData(String message, Short number) { - super(message, number); - } -} diff --git a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/Assertions.kt b/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/Assertions.kt deleted file mode 100644 index 6b7e58e43c..0000000000 --- a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/Assertions.kt +++ /dev/null @@ -1,14 +0,0 @@ -@file:JvmName("Assertions") -package net.corda.serialization.djvm - -import net.corda.core.serialization.CordaSerializable -import org.junit.jupiter.api.Assertions.assertNull - -inline fun assertNotCordaSerializable() { - assertNotCordaSerializable(T::class.java) -} - -fun assertNotCordaSerializable(clazz: Class<*>) { - assertNull(clazz.getAnnotation(CordaSerializable::class.java), - "$clazz must NOT be annotated as @CordaSerializable!") -} diff --git a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeBigDecimalTest.kt b/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeBigDecimalTest.kt deleted file mode 100644 index 4d91d996b9..0000000000 --- a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeBigDecimalTest.kt +++ /dev/null @@ -1,49 +0,0 @@ -package net.corda.serialization.djvm - -import net.corda.core.serialization.CordaSerializable -import net.corda.core.serialization.internal._contextSerializationEnv -import net.corda.core.serialization.serialize -import net.corda.serialization.djvm.SandboxType.KOTLIN -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.fail -import java.math.BigDecimal -import java.util.function.Function - -@ExtendWith(LocalSerialization::class) -class DeserializeBigDecimalTest : TestBase(KOTLIN) { - companion object { - const val VERY_BIG_DECIMAL = 994349993939.32737232 - } - - @Test - fun `test deserializing big decimal`() { - val bigDecimal = BigDecimalData(BigDecimal.valueOf(VERY_BIG_DECIMAL)) - val data = bigDecimal.serialize() - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - - val sandboxBigInteger = data.deserializeFor(classLoader) - - val taskFactory = classLoader.createRawTaskFactory() - val showBigDecimal = taskFactory.compose(classLoader.createSandboxFunction()).apply(ShowBigDecimal::class.java) - val result = showBigDecimal.apply(sandboxBigInteger) ?: fail("Result cannot be null") - - assertEquals(ShowBigDecimal().apply(bigDecimal), result.toString()) - assertEquals(SANDBOX_STRING, result::class.java.name) - } - } - - class ShowBigDecimal : Function { - override fun apply(data: BigDecimalData): String { - return with(data) { - "BigDecimal: $number" - } - } - } -} - -@CordaSerializable -data class BigDecimalData(val number: BigDecimal) \ No newline at end of file diff --git a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeBigIntegerTest.kt b/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeBigIntegerTest.kt deleted file mode 100644 index cec80daf48..0000000000 --- a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeBigIntegerTest.kt +++ /dev/null @@ -1,49 +0,0 @@ -package net.corda.serialization.djvm - -import net.corda.core.serialization.CordaSerializable -import net.corda.core.serialization.internal._contextSerializationEnv -import net.corda.core.serialization.serialize -import net.corda.serialization.djvm.SandboxType.KOTLIN -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.fail -import java.math.BigInteger -import java.util.function.Function - -@ExtendWith(LocalSerialization::class) -class DeserializeBigIntegerTest : TestBase(KOTLIN) { - companion object { - const val VERY_BIG_NUMBER = 1234567890123456789 - } - - @Test - fun `test deserializing big integer`() { - val bigInteger = BigIntegerData(BigInteger.valueOf(VERY_BIG_NUMBER)) - val data = bigInteger.serialize() - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - - val sandboxBigInteger = data.deserializeFor(classLoader) - - val taskFactory = classLoader.createRawTaskFactory() - val showBigInteger = taskFactory.compose(classLoader.createSandboxFunction()).apply(ShowBigInteger::class.java) - val result = showBigInteger.apply(sandboxBigInteger) ?: fail("Result cannot be null") - - assertEquals(ShowBigInteger().apply(bigInteger), result.toString()) - assertEquals(SANDBOX_STRING, result::class.java.name) - } - } - - class ShowBigInteger : Function { - override fun apply(data: BigIntegerData): String { - return with(data) { - "BigInteger: $number" - } - } - } -} - -@CordaSerializable -data class BigIntegerData(val number: BigInteger) diff --git a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeBitSetTest.kt b/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeBitSetTest.kt deleted file mode 100644 index 3a1cb6e15c..0000000000 --- a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeBitSetTest.kt +++ /dev/null @@ -1,39 +0,0 @@ -package net.corda.serialization.djvm - -import net.corda.core.serialization.internal._contextSerializationEnv -import net.corda.core.serialization.serialize -import net.corda.serialization.djvm.SandboxType.KOTLIN -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.fail -import java.util.BitSet -import java.util.function.Function - -@ExtendWith(LocalSerialization::class) -class DeserializeBitSetTest : TestBase(KOTLIN) { - @Test - fun `test deserializing bitset`() { - val bitSet = BitSet.valueOf(byteArrayOf(0x00, 0x70, 0x55, 0x3A, 0x48, 0x12)) - val data = bitSet.serialize() - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - - val sandboxBitSet = data.deserializeFor(classLoader) - - val taskFactory = classLoader.createRawTaskFactory() - val showBitSet = taskFactory.compose(classLoader.createSandboxFunction()).apply(ShowBitSet::class.java) - val result = showBitSet.apply(sandboxBitSet) ?: fail("Result cannot be null") - - assertEquals(bitSet.toString(), result.toString()) - assertEquals(SANDBOX_STRING, result::class.java.name) - } - } - - class ShowBitSet : Function { - override fun apply(bitSet: BitSet): String { - return bitSet.toString() - } - } -} diff --git a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeCertificatesTest.kt b/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeCertificatesTest.kt deleted file mode 100644 index 96de396080..0000000000 --- a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeCertificatesTest.kt +++ /dev/null @@ -1,134 +0,0 @@ -package net.corda.serialization.djvm - -import net.corda.core.serialization.internal._contextSerializationEnv -import net.corda.core.serialization.serialize -import net.corda.serialization.djvm.SandboxType.KOTLIN -import org.assertj.core.api.Assertions.assertThat -import org.bouncycastle.asn1.x509.CRLReason -import org.bouncycastle.asn1.x500.X500Name -import org.bouncycastle.cert.X509v2CRLBuilder -import org.bouncycastle.cert.jcajce.JcaX509CRLConverter -import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.BeforeAll -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.fail -import java.security.KeyPairGenerator -import java.security.cert.CertPath -import java.security.cert.CertificateFactory -import java.security.cert.X509CRL -import java.security.cert.X509Certificate -import java.util.Date -import java.util.function.Function - -@ExtendWith(LocalSerialization::class) -class DeserializeCertificatesTest : TestBase(KOTLIN) { - companion object { - // The sandbox's localisation may not match that of the host. - // E.g. line separator characters. - fun String.toUNIX(): String { - return replace(System.lineSeparator(), "\n") - } - - // Remove the lines which have been added since Java 8. - fun String.toJava8Format(): String { - return replace(" params: null\n", "") - } - - val factory: CertificateFactory = CertificateFactory.getInstance("X.509") - lateinit var certificate: X509Certificate - - @Suppress("unused") - @BeforeAll - @JvmStatic - fun loadCertificate() { - certificate = this::class.java.classLoader.getResourceAsStream("testing.cert")?.use { input -> - factory.generateCertificate(input) as X509Certificate - } ?: fail("Certificate not found") - } - } - - @Test - fun `test deserialize certificate path`() { - val certPath = factory.generateCertPath(listOf(certificate)) - val data = certPath.serialize() - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - - val sandboxCertPath = data.deserializeFor(classLoader) - - val taskFactory = classLoader.createRawTaskFactory() - val showCertPath = taskFactory.compose(classLoader.createSandboxFunction()).apply(ShowCertPath::class.java) - val result = showCertPath.apply(sandboxCertPath) ?: fail("Result cannot be null") - - assertEquals(ShowCertPath().apply(certPath).toUNIX().toJava8Format(), result.toString()) - assertThat(result::class.java.name).startsWith("sandbox.") - } - } - - class ShowCertPath : Function { - override fun apply(certPath: CertPath): String { - return "CertPath -> $certPath" - } - } - - @Test - fun `test deserialize X509 certificate`() { - val data = certificate.serialize() - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - - val sandboxCertificate = data.deserializeFor(classLoader) - - val taskFactory = classLoader.createRawTaskFactory() - val showCertificate = taskFactory.compose(classLoader.createSandboxFunction()).apply(ShowCertificate::class.java) - val result = showCertificate.apply(sandboxCertificate) ?: fail("Result cannot be null") - - assertEquals(ShowCertificate().apply(certificate).toUNIX().toJava8Format(), result.toString()) - assertThat(result::class.java.name).startsWith("sandbox.") - } - } - - class ShowCertificate : Function { - override fun apply(certificate: X509Certificate): String { - return "X.509 Certificate -> $certificate" - } - } - - @Test - fun `test X509 CRL`() { - val caKeyPair = KeyPairGenerator.getInstance("RSA") - .generateKeyPair() - val signer = JcaContentSignerBuilder("SHA256WithRSAEncryption") - .build(caKeyPair.private) - - val now = Date() - val crl = with(X509v2CRLBuilder(X500Name("CN=Test CA"), now)) { - addCRLEntry(certificate.serialNumber, now, CRLReason.privilegeWithdrawn) - JcaX509CRLConverter().getCRL(build(signer)) - } - val data = crl.serialize() - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - - val sandboxCRL = data.deserializeFor(classLoader) - - val taskFactory = classLoader.createRawTaskFactory() - val showCRL = taskFactory.compose(classLoader.createSandboxFunction()).apply(ShowCRL::class.java) - val result = showCRL.apply(sandboxCRL) ?: fail("Result cannot be null") - - assertEquals(ShowCRL().apply(crl).toUNIX(), result.toString()) - assertThat(result::class.java.name).startsWith("sandbox.") - } - } - - class ShowCRL : Function { - override fun apply(crl: X509CRL): String { - return "X.509 CRL -> $crl" - } - } -} diff --git a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeClassTest.kt b/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeClassTest.kt deleted file mode 100644 index 657a71f1f6..0000000000 --- a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeClassTest.kt +++ /dev/null @@ -1,59 +0,0 @@ -package net.corda.serialization.djvm - -import greymalkin.ExternalData -import net.corda.core.serialization.internal._contextSerializationEnv -import net.corda.core.serialization.serialize -import net.corda.djvm.messages.Severity -import net.corda.serialization.djvm.SandboxType.KOTLIN -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.assertThrows -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.fail -import java.io.NotSerializableException -import java.util.function.Function - -@ExtendWith(LocalSerialization::class) -class DeserializeClassTest : TestBase(KOTLIN) { - @Test - fun `test deserializing existing class`() { - val myClass = ExternalData::class.java - val data = myClass.serialize() - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - - val sandboxInstant = data.deserializeFor(classLoader) - - val taskFactory = classLoader.createRawTaskFactory() - val showClass = taskFactory.compose(classLoader.createSandboxFunction()).apply(ShowClass::class.java) - val result = showClass.apply(sandboxInstant) ?: fail("Result cannot be null") - - assertEquals("sandbox.${myClass.name}", result.toString()) - assertEquals(SANDBOX_STRING, result::class.java.name) - } - } - - @Test - fun `test deserializing missing class`() { - // The DJVM will refuse to find this class because it belongs to net.corda.djvm.**. - val myClass = Severity::class.java - val data = myClass.serialize() - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - - val ex = assertThrows{ data.deserializeFor(classLoader) } - assertThat(ex) - .isExactlyInstanceOf(NotSerializableException::class.java) - .hasMessageContaining("Severity was not found by the node,") - } - } -} - -class ShowClass : Function, String> { - override fun apply(type: Class<*>): String { - return type.name - } -} diff --git a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeCollectionsTest.kt b/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeCollectionsTest.kt deleted file mode 100644 index efca075039..0000000000 --- a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeCollectionsTest.kt +++ /dev/null @@ -1,215 +0,0 @@ -package net.corda.serialization.djvm - -import greymalkin.ExternalEnum -import net.corda.core.serialization.CordaSerializable -import net.corda.core.serialization.internal._contextSerializationEnv -import net.corda.core.serialization.serialize -import net.corda.core.utilities.NonEmptySet -import net.corda.serialization.djvm.SandboxType.KOTLIN -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.fail -import java.util.EnumSet -import java.util.NavigableSet -import java.util.SortedSet -import java.util.function.Function - -@ExtendWith(LocalSerialization::class) -class DeserializeCollectionsTest : TestBase(KOTLIN) { - @Test - fun `test deserializing string list`() { - val stringList = StringList(listOf("Hello", "World", "!")) - val data = stringList.serialize() - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - - val sandboxList = data.deserializeFor(classLoader) - - val taskFactory = classLoader.createRawTaskFactory() - val showStringList = taskFactory.compose(classLoader.createSandboxFunction()).apply(ShowStringList::class.java) - val result = showStringList.apply(sandboxList) ?: fail("Result cannot be null") - - assertEquals(stringList.lines.joinToString(), result.toString()) - assertEquals("Hello, World, !", result.toString()) - } - } - - class ShowStringList : Function { - override fun apply(data: StringList): String { - return data.lines.joinToString() - } - } - - @Test - fun `test deserializing integer set`() { - val integerSet = IntegerSet(linkedSetOf(10, 3, 15, 2, 10)) - val data = integerSet.serialize() - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - - val sandboxSet = data.deserializeFor(classLoader) - - val taskFactory = classLoader.createRawTaskFactory() - val showIntegerSet = taskFactory.compose(classLoader.createSandboxFunction()).apply(ShowIntegerSet::class.java) - val result = showIntegerSet.apply(sandboxSet) ?: fail("Result cannot be null") - - assertEquals(integerSet.numbers.joinToString(), result.toString()) - assertEquals("10, 3, 15, 2", result.toString()) - } - } - - class ShowIntegerSet : Function { - override fun apply(data: IntegerSet): String { - return data.numbers.joinToString() - } - } - - @Test - fun `test deserializing integer sorted set`() { - val integerSortedSet = IntegerSortedSet(sortedSetOf(10, 15, 1000, 3, 2, 10)) - val data = integerSortedSet.serialize() - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - - val sandboxSet = data.deserializeFor(classLoader) - - val taskFactory = classLoader.createRawTaskFactory() - val showIntegerSortedSet = taskFactory.compose(classLoader.createSandboxFunction()).apply(ShowIntegerSortedSet::class.java) - val result = showIntegerSortedSet.apply(sandboxSet) ?: fail("Result cannot be null") - - assertEquals(integerSortedSet.numbers.joinToString(), result.toString()) - assertEquals("2, 3, 10, 15, 1000", result.toString()) - } - } - - class ShowIntegerSortedSet : Function { - override fun apply(data: IntegerSortedSet): String { - return data.numbers.joinToString() - } - } - - @Test - fun `test deserializing long navigable set`() { - val longNavigableSet = LongNavigableSet(sortedSetOf(99955L, 10, 15, 1000, 3, 2, 10)) - val data = longNavigableSet.serialize() - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - - val sandboxSet = data.deserializeFor(classLoader) - - val taskFactory = classLoader.createRawTaskFactory() - val showLongNavigableSet = taskFactory.compose(classLoader.createSandboxFunction()).apply(ShowLongNavigableSet::class.java) - val result = showLongNavigableSet.apply(sandboxSet) ?: fail("Result cannot be null") - - assertEquals(longNavigableSet.numbers.joinToString(), result.toString()) - assertEquals("2, 3, 10, 15, 1000, 99955", result.toString()) - } - } - - class ShowLongNavigableSet : Function { - override fun apply(data: LongNavigableSet): String { - return data.numbers.joinToString() - } - } - - @Test - fun `test deserializing short collection`() { - val shortCollection = ShortCollection(listOf(10, 200, 3000)) - val data = shortCollection.serialize() - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - - val sandboxCollection = data.deserializeFor(classLoader) - - val taskFactory = classLoader.createRawTaskFactory() - val showShortCollection = taskFactory.compose(classLoader.createSandboxFunction()).apply(ShowShortCollection::class.java) - val result = showShortCollection.apply(sandboxCollection) ?: fail("Result cannot be null") - - assertEquals(shortCollection.numbers.joinToString(), result.toString()) - assertEquals("10, 200, 3000", result.toString()) - } - } - - class ShowShortCollection : Function { - override fun apply(data: ShortCollection): String { - return data.numbers.joinToString() - } - } - - @Test - fun `test deserializing non-empty string set`() { - val nonEmptyStrings = NonEmptyStringSet(NonEmptySet.of("Hello", "World", "!")) - val data = nonEmptyStrings.serialize() - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - - val sandboxSet = data.deserializeFor(classLoader) - - val taskFactory = classLoader.createRawTaskFactory() - val showNonEmptyStringSet = taskFactory.compose(classLoader.createSandboxFunction()).apply(ShowNonEmptyStringSet::class.java) - val result = showNonEmptyStringSet.apply(sandboxSet) ?: fail("Result cannot be null") - - assertEquals(nonEmptyStrings.lines.joinToString(), result.toString()) - assertEquals("Hello, World, !", result.toString()) - } - } - - class ShowNonEmptyStringSet : Function { - override fun apply(data: NonEmptyStringSet): String { - return data.lines.joinToString() - } - } - - @Test - fun `test deserializing enum set`() { - val enumSet = HasEnumSet(EnumSet.of(ExternalEnum.DOH)) - val data = enumSet.serialize() - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - - val sandboxSet = data.deserializeFor(classLoader) - - val taskFactory = classLoader.createRawTaskFactory() - val showHasEnumSet = taskFactory.compose(classLoader.createSandboxFunction()).apply(ShowHasEnumSet::class.java) - val result = showHasEnumSet.apply(sandboxSet) ?: fail("Result cannot be null") - - assertEquals(enumSet.values.toString(), result.toString()) - assertEquals("[DOH]", result.toString()) - } - } - - class ShowHasEnumSet : Function { - override fun apply(data: HasEnumSet): String { - return data.values.toString() - } - } -} - -@CordaSerializable -class StringList(val lines: List) - -@CordaSerializable -class IntegerSet(val numbers: Set) - -@CordaSerializable -class IntegerSortedSet(val numbers: SortedSet) - -@CordaSerializable -class LongNavigableSet(val numbers: NavigableSet) - -@CordaSerializable -class ShortCollection(val numbers: Collection) - -@CordaSerializable -class NonEmptyStringSet(val lines: NonEmptySet) - -@CordaSerializable -class HasEnumSet(val values: EnumSet) diff --git a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeComposedCustomDataTest.kt b/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeComposedCustomDataTest.kt deleted file mode 100644 index 076a5d00d2..0000000000 --- a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeComposedCustomDataTest.kt +++ /dev/null @@ -1,119 +0,0 @@ -package net.corda.serialization.djvm - -import net.corda.core.serialization.CordaSerializable -import net.corda.core.serialization.SerializationCustomSerializer -import net.corda.core.serialization.internal._contextSerializationEnv -import net.corda.core.serialization.serialize -import net.corda.serialization.djvm.SandboxType.KOTLIN -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.BeforeAll -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.assertThrows -import org.junit.jupiter.api.extension.RegisterExtension -import org.junit.jupiter.api.fail -import java.io.NotSerializableException -import java.util.function.Function - -class DeserializeComposedCustomDataTest: TestBase(KOTLIN) { - companion object { - const val MESSAGE = "Hello Sandbox!" - const val BIG_NUMBER = 23823L - - @Suppress("unused") - @BeforeAll - @JvmStatic - fun checkData() { - assertNotCordaSerializable() - assertNotCordaSerializable() - } - } - - @RegisterExtension - @JvmField - val serialization = LocalSerialization(setOf(StringAtomSerializer(), LongAtomSerializer()), emptySet()) - - @Test - fun `test deserializing composed object`() { - val composed = ComposedData(StringAtom(MESSAGE), LongAtom(BIG_NUMBER)) - val data = composed.serialize() - - sandbox { - val customSerializers = setOf( - StringAtomSerializer::class.java.name, - LongAtomSerializer::class.java.name - ) - _contextSerializationEnv.set(createSandboxSerializationEnv( - classLoader = classLoader, - customSerializerClassNames = customSerializers, - serializationWhitelistNames = emptySet() - )) - - val sandboxComplex = data.deserializeFor(classLoader) - - val taskFactory = classLoader.createRawTaskFactory() - val showComplex = taskFactory.compose(classLoader.createSandboxFunction()).apply(ShowComposedData::class.java) - val result = showComplex.apply(sandboxComplex) ?: fail("Result cannot be null") - assertEquals(SANDBOX_STRING, result::class.java.name) - assertEquals(ShowComposedData().apply(composed), result.toString()) - } - } - - @Test - fun `test deserialization needs custom serializer`() { - val composed = ComposedData(StringAtom(MESSAGE), LongAtom(BIG_NUMBER)) - val data = composed.serialize() - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - assertThrows { data.deserializeFor(classLoader) } - } - } - - class ShowComposedData : Function { - private fun show(atom: Atom<*>): String = atom.toString() - - override fun apply(composed: ComposedData): String { - return "Composed: message=${show(composed.message)} and value=${show(composed.value)}" - } - } - - class StringAtomSerializer : SerializationCustomSerializer { - data class Proxy(val value: String) - - override fun fromProxy(proxy: Proxy): StringAtom = StringAtom(proxy.value) - override fun toProxy(obj: StringAtom): Proxy = Proxy(obj.atom) - } - - class LongAtomSerializer : SerializationCustomSerializer { - data class Proxy(val value: Long?) - - override fun fromProxy(proxy: Proxy): LongAtom = LongAtom(proxy.value) - override fun toProxy(obj: LongAtom): Proxy = Proxy(obj.atom) - } -} - -@CordaSerializable -class ComposedData( - val message: StringAtom, - val value: LongAtom -) - -interface Atom { - val atom: T -} - -abstract class AbstractAtom(initialValue: T) : Atom { - override val atom: T = initialValue - - override fun toString(): String { - return "[$atom]" - } -} - -/** - * These classes REQUIRE custom serializers because their - * constructor parameters cannot be mapped to properties - * automatically. THIS IS DELIBERATE! - */ -class StringAtom(initialValue: String) : AbstractAtom(initialValue) -class LongAtom(initialValue: Long?) : AbstractAtom(initialValue) diff --git a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeCurrencyTest.kt b/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeCurrencyTest.kt deleted file mode 100644 index fc747aebce..0000000000 --- a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeCurrencyTest.kt +++ /dev/null @@ -1,45 +0,0 @@ -package net.corda.serialization.djvm - -import net.corda.core.serialization.CordaSerializable -import net.corda.core.serialization.internal._contextSerializationEnv -import net.corda.core.serialization.serialize -import net.corda.serialization.djvm.SandboxType.KOTLIN -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.fail -import java.util.Currency -import java.util.function.Function - -@ExtendWith(LocalSerialization::class) -class DeserializeCurrencyTest : TestBase(KOTLIN) { - @Test - fun `test deserializing currency`() { - val currency = CurrencyData(Currency.getInstance("GBP")) - val data = currency.serialize() - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - - val sandboxCurrency = data.deserializeFor(classLoader) - - val taskFactory = classLoader.createRawTaskFactory() - val showCurrency = taskFactory.compose(classLoader.createSandboxFunction()).apply(ShowCurrency::class.java) - val result = showCurrency.apply(sandboxCurrency) ?: fail("Result cannot be null") - - assertEquals(ShowCurrency().apply(currency), result.toString()) - assertEquals(SANDBOX_STRING, result::class.java.name) - } - } - - class ShowCurrency : Function { - override fun apply(data: CurrencyData): String { - return with(data) { - "Currency: $currency" - } - } - } -} - -@CordaSerializable -data class CurrencyData(val currency: Currency) diff --git a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeCustomGenericDataTest.kt b/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeCustomGenericDataTest.kt deleted file mode 100644 index 0d624edb84..0000000000 --- a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeCustomGenericDataTest.kt +++ /dev/null @@ -1,97 +0,0 @@ -package net.corda.serialization.djvm - -import net.corda.core.serialization.SerializationCustomSerializer -import net.corda.core.serialization.internal.MissingSerializerException -import net.corda.core.serialization.internal._contextSerializationEnv -import net.corda.core.serialization.serialize -import net.corda.serialization.djvm.SandboxType.KOTLIN -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.BeforeAll -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.assertThrows -import org.junit.jupiter.api.extension.RegisterExtension -import org.junit.jupiter.api.fail -import java.util.function.Function - -class DeserializeCustomGenericDataTest: TestBase(KOTLIN) { - companion object { - const val MESSAGE = "Hello Sandbox!" - const val BIG_NUMBER = 23823L - - @Suppress("unused") - @BeforeAll - @JvmStatic - fun checkData() { - assertNotCordaSerializable>() - assertNotCordaSerializable() - } - } - - @RegisterExtension - @JvmField - val serialization = LocalSerialization(setOf(CustomSerializer()), emptySet()) - - @Test - fun `test deserializing custom generic object`() { - val complex = ComplexGenericData(MESSAGE, BIG_NUMBER) - val data = complex.serialize() - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv( - classLoader = classLoader, - customSerializerClassNames = setOf(CustomSerializer::class.java.name), - serializationWhitelistNames = emptySet() - )) - - val sandboxComplex = data.deserializeFor(classLoader) - - val taskFactory = classLoader.createRawTaskFactory() - val showComplex = taskFactory.compose(classLoader.createSandboxFunction()).apply(ShowComplexData::class.java) - val result = showComplex.apply(sandboxComplex) ?: fail("Result cannot be null") - assertEquals(SANDBOX_STRING, result::class.java.name) - assertEquals(ShowComplexData().apply(complex), result.toString()) - } - } - - @Test - fun `test deserialization needs custom serializer`() { - val complex = ComplexGenericData(MESSAGE, BIG_NUMBER) - val data = complex.serialize() - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - assertThrows { data.deserializeFor(classLoader) } - } - } - - class ShowComplexData : Function { - private fun show(generic: GenericData<*>): String = generic.toString() - - override fun apply(complex: ComplexGenericData): String { - return "Complex: message=${show(complex.message)} and value=${show(complex.value)}" - } - } - - /** - * This class REQUIRES a custom serializer because its - * constructor parameters cannot be mapped to properties - * automatically. THIS IS DELIBERATE! - */ - class ComplexGenericData(msg: String, initialValue: Long?) { - val message = GenericData(msg) - val value = GenericData(initialValue) - } - - class GenericData(val data: T) { - override fun toString(): String { - return "[$data]" - } - } - - class CustomSerializer : SerializationCustomSerializer { - data class Proxy(val message: String, val value: Long?) - - override fun fromProxy(proxy: Proxy): ComplexGenericData = ComplexGenericData(proxy.message, proxy.value) - override fun toProxy(obj: ComplexGenericData): Proxy = Proxy(obj.message.data, obj.value.data) - } -} diff --git a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeCustomisedEnumTest.kt b/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeCustomisedEnumTest.kt deleted file mode 100644 index 657ab25f34..0000000000 --- a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeCustomisedEnumTest.kt +++ /dev/null @@ -1,60 +0,0 @@ -package net.corda.serialization.djvm - -import net.corda.core.serialization.CordaSerializable -import net.corda.core.serialization.internal._contextSerializationEnv -import net.corda.core.serialization.serialize -import net.corda.serialization.djvm.SandboxType.KOTLIN -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.fail -import org.junit.jupiter.params.ParameterizedTest -import org.junit.jupiter.params.provider.EnumSource -import java.util.function.Function - -@ExtendWith(LocalSerialization::class) -class DeserializeCustomisedEnumTest : TestBase(KOTLIN) { - @ParameterizedTest - @EnumSource(UserRole::class) - fun `test deserialize enum with custom toString`(role: UserRole) { - val userEnumData = UserEnumData(role) - val data = userEnumData.serialize() - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - - val sandboxData = data.deserializeFor(classLoader) - - val taskFactory = classLoader.createRawTaskFactory() - val showUserEnumData = taskFactory.compose(classLoader.createSandboxFunction()).apply(ShowUserEnumData::class.java) - val result = showUserEnumData.apply(sandboxData) ?: fail("Result cannot be null") - - assertEquals(ShowUserEnumData().apply(userEnumData), result.toString()) - assertEquals("UserRole: name='${role.roleName}', ordinal='${role.ordinal}'", result.toString()) - assertEquals(SANDBOX_STRING, result::class.java.name) - } - } - - class ShowUserEnumData : Function { - override fun apply(input: UserEnumData): String { - return with(input) { - "UserRole: name='${role.roleName}', ordinal='${role.ordinal}'" - } - } - } -} - -interface Role { - val roleName: String -} - -@Suppress("unused") -@CordaSerializable -enum class UserRole(override val roleName: String) : Role { - CONTROLLER(roleName = "Controller"), - WORKER(roleName = "Worker"); - - override fun toString() = roleName -} - -@CordaSerializable -data class UserEnumData(val role: UserRole) diff --git a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeDurationTest.kt b/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeDurationTest.kt deleted file mode 100644 index 7a2c6a4db8..0000000000 --- a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeDurationTest.kt +++ /dev/null @@ -1,39 +0,0 @@ -package net.corda.serialization.djvm - -import net.corda.core.serialization.internal._contextSerializationEnv -import net.corda.core.serialization.serialize -import net.corda.serialization.djvm.SandboxType.KOTLIN -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.fail -import java.time.Duration -import java.util.function.Function - -@ExtendWith(LocalSerialization::class) -class DeserializeDurationTest : TestBase(KOTLIN) { - @Test - fun `test deserializing duration`() { - val duration = Duration.ofSeconds(12345, 6789) - val data = duration.serialize() - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - - val sandboxDuration = data.deserializeFor(classLoader) - - val taskFactory = classLoader.createRawTaskFactory() - val showDuration = taskFactory.compose(classLoader.createSandboxFunction()).apply(ShowDuration::class.java) - val result = showDuration.apply(sandboxDuration) ?: fail("Result cannot be null") - - assertEquals(duration.toString(), result.toString()) - assertEquals(SANDBOX_STRING, result::class.java.name) - } - } - - class ShowDuration : Function { - override fun apply(duration: Duration): String { - return duration.toString() - } - } -} diff --git a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeEnumSetTest.kt b/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeEnumSetTest.kt deleted file mode 100644 index 22681c44f5..0000000000 --- a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeEnumSetTest.kt +++ /dev/null @@ -1,43 +0,0 @@ -package net.corda.serialization.djvm - -import greymalkin.ExternalEnum -import net.corda.core.serialization.internal._contextSerializationEnv -import net.corda.core.serialization.serialize -import net.corda.serialization.djvm.SandboxType.KOTLIN -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.fail -import org.junit.jupiter.params.ParameterizedTest -import org.junit.jupiter.params.provider.EnumSource -import java.util.EnumSet -import java.util.function.Function - -@ExtendWith(LocalSerialization::class) -class DeserializeEnumSetTest : TestBase(KOTLIN) { - @ParameterizedTest - @EnumSource(ExternalEnum::class) - fun `test deserialize enum set`(value: ExternalEnum) { - val enumSet = EnumSet.of(value) - val data = enumSet.serialize() - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - - val sandboxEnumSet = data.deserializeFor(classLoader) - - val taskFactory = classLoader.createRawTaskFactory() - val showEnumSet = taskFactory.compose(classLoader.createSandboxFunction()).apply(ShowEnumSet::class.java) - val result = showEnumSet.apply(sandboxEnumSet) ?: fail("Result cannot be null") - - assertEquals(ShowEnumSet().apply(enumSet), result.toString()) - assertEquals("EnumSet: [$value]'", result.toString()) - assertEquals(SANDBOX_STRING, result::class.java.name) - } - } - - class ShowEnumSet : Function, String> { - override fun apply(input: EnumSet<*>): String { - return "EnumSet: $input'" - } - } -} diff --git a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeEnumTest.kt b/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeEnumTest.kt deleted file mode 100644 index 2dbbb2ea16..0000000000 --- a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeEnumTest.kt +++ /dev/null @@ -1,55 +0,0 @@ -package net.corda.serialization.djvm - -import net.corda.core.serialization.CordaSerializable -import net.corda.core.serialization.internal._contextSerializationEnv -import net.corda.core.serialization.serialize -import net.corda.serialization.djvm.SandboxType.KOTLIN -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.fail -import org.junit.jupiter.params.ParameterizedTest -import org.junit.jupiter.params.provider.EnumSource -import java.util.function.Function - -@ExtendWith(LocalSerialization::class) -class DeserializeEnumTest : TestBase(KOTLIN) { - @ParameterizedTest - @EnumSource(ExampleEnum::class) - fun `test deserialize basic enum`(value: ExampleEnum) { - val example = ExampleData(value) - val data = example.serialize() - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - - val sandboxExample = data.deserializeFor(classLoader) - - val taskFactory = classLoader.createRawTaskFactory() - val showExampleData = taskFactory.compose(classLoader.createSandboxFunction()).apply(ShowExampleData::class.java) - val result = showExampleData.apply(sandboxExample) ?: fail("Result cannot be null") - - assertEquals(ShowExampleData().apply(example), result.toString()) - assertEquals("Example: name='${value.name}', ordinal='${value.ordinal}'", result.toString()) - assertEquals(SANDBOX_STRING, result::class.java.name) - } - } - - class ShowExampleData : Function { - override fun apply(input: ExampleData): String { - return with(input) { - "Example: name='${value.name}', ordinal='${value.ordinal}'" - } - } - } -} - -@Suppress("unused") -@CordaSerializable -enum class ExampleEnum { - ONE, - TWO, - THREE -} - -@CordaSerializable -data class ExampleData(val value: ExampleEnum) diff --git a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeEnumWithEvolutionTest.kt b/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeEnumWithEvolutionTest.kt deleted file mode 100644 index 32fea60fd0..0000000000 --- a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeEnumWithEvolutionTest.kt +++ /dev/null @@ -1,155 +0,0 @@ -package net.corda.serialization.djvm - -import net.corda.core.serialization.CordaSerializable -import net.corda.core.serialization.CordaSerializationTransformEnumDefault -import net.corda.core.serialization.CordaSerializationTransformEnumDefaults -import net.corda.core.serialization.CordaSerializationTransformRename -import net.corda.core.serialization.CordaSerializationTransformRenames -import net.corda.core.serialization.SerializationContext -import net.corda.core.serialization.SerializedBytes -import net.corda.core.serialization.internal._contextSerializationEnv -import net.corda.core.serialization.serialize -import net.corda.serialization.djvm.EvolvedEnum.ONE -import net.corda.serialization.djvm.EvolvedEnum.TWO -import net.corda.serialization.djvm.EvolvedEnum.THREE -import net.corda.serialization.djvm.EvolvedEnum.FOUR -import net.corda.serialization.djvm.OriginalEnum.One -import net.corda.serialization.djvm.OriginalEnum.Two -import net.corda.serialization.djvm.SandboxType.KOTLIN -import net.corda.serialization.internal.amqp.CompositeType -import net.corda.serialization.internal.amqp.DeserializationInput -import net.corda.serialization.internal.amqp.RestrictedType -import net.corda.serialization.internal.amqp.Transform -import net.corda.serialization.internal.amqp.TransformTypes -import net.corda.serialization.internal.amqp.TypeNotation -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.ExtensionContext -import org.junit.jupiter.api.fail -import org.junit.jupiter.params.ParameterizedTest -import org.junit.jupiter.params.provider.Arguments -import org.junit.jupiter.params.provider.ArgumentsProvider -import org.junit.jupiter.params.provider.ArgumentsSource -import java.util.EnumMap -import java.util.function.Function -import java.util.stream.Stream - -@ExtendWith(LocalSerialization::class) -class DeserializeEnumWithEvolutionTest : TestBase(KOTLIN) { - class EvolutionArgumentProvider : ArgumentsProvider { - override fun provideArguments(context: ExtensionContext?): Stream { - return Stream.of( - Arguments.of(ONE, One), - Arguments.of(TWO, Two), - Arguments.of(THREE, One), - Arguments.of(FOUR, Two) - ) - } - } - - private fun String.devolve() = replace("Evolved", "Original") - - private fun devolveType(type: TypeNotation): TypeNotation { - return when (type) { - is CompositeType -> type.copy( - name = type.name.devolve(), - fields = type.fields.map { it.copy(type = it.type.devolve()) } - ) - is RestrictedType -> type.copy(name = type.name.devolve()) - else -> type - } - } - - private fun SerializedBytes<*>.devolve(context: SerializationContext): SerializedBytes { - val envelope = DeserializationInput.getEnvelope(this, context.encodingWhitelist).apply { - val schemaTypes = schema.types.map(::devolveType) - with(schema.types as MutableList) { - clear() - addAll(schemaTypes) - } - - val transforms = transformsSchema.types.asSequence().associateTo(LinkedHashMap()) { - it.key.devolve() to it.value - } - with(transformsSchema.types as MutableMap>>) { - clear() - putAll(transforms) - } - } - return SerializedBytes(envelope.write()) - } - - @ParameterizedTest - @ArgumentsSource(EvolutionArgumentProvider::class) - fun `test deserialising evolved enum`(value: EvolvedEnum, expected: OriginalEnum) { - val context = (_contextSerializationEnv.get() ?: fail("No serialization environment!")).p2pContext - - val evolvedData = value.serialize() - val originalData = evolvedData.devolve(context) - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - val sandboxOriginal = originalData.deserializeFor(classLoader) - assertEquals("sandbox." + OriginalEnum::class.java.name, sandboxOriginal::class.java.name) - assertEquals(expected.toString(), sandboxOriginal.toString()) - } - } - - @ParameterizedTest - @ArgumentsSource(EvolutionArgumentProvider::class) - fun `test deserialising data with evolved enum`(value: EvolvedEnum, expected: OriginalEnum) { - val context = (_contextSerializationEnv.get() ?: fail("No serialization environment!")).p2pContext - - val evolvedData = EvolvedData(value).serialize() - val originalData = evolvedData.devolve(context) - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - val sandboxOriginal = originalData.deserializeFor(classLoader) - - val taskFactory = classLoader.createRawTaskFactory() - val result = taskFactory.compose(classLoader.createSandboxFunction()) - .apply(ShowOriginalData::class.java) - .apply(sandboxOriginal) ?: fail("Result cannot be null") - assertThat(result.toString()) - .isEqualTo(ShowOriginalData().apply(OriginalData(expected))) - } - } - - class ShowOriginalData : Function { - override fun apply(input: OriginalData): String { - return with(input) { - "Name='${value.name}', Ordinal='${value.ordinal}'" - } - } - } -} - -@CordaSerializable -enum class OriginalEnum { - One, - Two -} - -@CordaSerializable -data class OriginalData(val value: OriginalEnum) - -@CordaSerializable -@CordaSerializationTransformRenames( - CordaSerializationTransformRename(from = "One", to = "ONE"), - CordaSerializationTransformRename(from = "Two", to = "TWO") -) -@CordaSerializationTransformEnumDefaults( - CordaSerializationTransformEnumDefault(new = "THREE", old = "One"), - CordaSerializationTransformEnumDefault(new = "FOUR", old = "Two") -) -enum class EvolvedEnum { - ONE, - TWO, - THREE, - FOUR -} - -@CordaSerializable -data class EvolvedData(val value: EvolvedEnum) \ No newline at end of file diff --git a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeGenericsTest.kt b/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeGenericsTest.kt deleted file mode 100644 index cf78125b82..0000000000 --- a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeGenericsTest.kt +++ /dev/null @@ -1,171 +0,0 @@ -package net.corda.serialization.djvm - -import net.corda.core.serialization.CordaSerializable -import net.corda.core.serialization.internal._contextSerializationEnv -import net.corda.core.serialization.serialize -import net.corda.serialization.djvm.SandboxType.KOTLIN -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.fail -import java.util.function.Function - -@ExtendWith(LocalSerialization::class) -class DeserializeGenericsTest : TestBase(KOTLIN) { - @Test - fun `test deserializing generic wrapper with String`() { - val wrappedString = GenericWrapper(data = "Hello World!") - val data = wrappedString.serialize() - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - - val sandboxWrapper = data.deserializeFor(classLoader) - - val taskFactory = classLoader.createRawTaskFactory() - val getGenericData = taskFactory.compose(classLoader.createSandboxFunction()).apply(GetGenericData::class.java) - val result = getGenericData.apply(sandboxWrapper) ?: fail("Result cannot be null") - - assertEquals("Hello World!", result.toString()) - assertEquals(SANDBOX_STRING, result::class.java.name) - } - } - - @Test - fun `test deserializing generic wrapper with Integer`() { - val wrappedInteger = GenericWrapper(data = 1000) - val data = wrappedInteger.serialize() - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - - val sandboxWrapper = data.deserializeFor(classLoader) - - val taskFactory = classLoader.createRawTaskFactory() - val getGenericData = taskFactory.compose(classLoader.createSandboxFunction()).apply(GetGenericData::class.java) - val result = getGenericData.apply(sandboxWrapper) ?: fail("Result cannot be null") - - assertEquals("sandbox.java.lang.Integer", result::class.java.name) - assertEquals(1000, classLoader.createBasicOutput().apply(result)) - } - } - - @Test - fun `test deserializing generic wrapper with array of Integer`() { - val wrappedArrayOfInteger = GenericWrapper(arrayOf(1000, 2000, 3000)) - val data = wrappedArrayOfInteger.serialize() - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - - val sandboxWrapper = data.deserializeFor(classLoader) - - val taskFactory = classLoader.createRawTaskFactory() - val getGenericData = taskFactory.compose(classLoader.createSandboxFunction()).apply(GetGenericData::class.java) - val result = getGenericData.apply(sandboxWrapper) ?: fail("Result cannot be null") - - assertEquals("[Lsandbox.java.lang.Integer;", result::class.java.name) - assertThat(classLoader.createBasicOutput().apply(result)) - .isEqualTo(arrayOf(1000, 2000, 3000)) - } - } - - @Test - fun `test deserializing generic wrapper with primitive int array`() { - val wrappedArrayOfInteger = GenericWrapper(intArrayOf(1000, 2000, 3000)) - val data = wrappedArrayOfInteger.serialize() - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - - val sandboxWrapper = data.deserializeFor(classLoader) - - val taskFactory = classLoader.createRawTaskFactory() - val getGenericData = taskFactory.compose(classLoader.createSandboxFunction()).apply(GetGenericData::class.java) - val result = getGenericData.apply(sandboxWrapper) ?: fail("Result cannot be null") - - assertEquals("[I", result::class.java.name) - assertThat(classLoader.createBasicOutput().apply(result)) - .isEqualTo(intArrayOf(1000, 2000, 3000)) - } - } - - @Test - fun `test deserializing generic list`() { - val wrappedList = GenericWrapper(data = listOf("Hello World!")) - val data = wrappedList.serialize() - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - - val sandboxWrapper = data.deserializeFor(classLoader) - - val taskFactory = classLoader.createRawTaskFactory() - val sandboxFunction = classLoader.createSandboxFunction() - val getGenericData = taskFactory.compose(sandboxFunction).apply(GetGenericData::class.java) - val dataResult = getGenericData.apply(sandboxWrapper) ?: fail("Result cannot be null") - - assertEquals("[Hello World!]", dataResult.toString()) - assertEquals("sandbox.java.util.Collections\$UnmodifiableRandomAccessList", dataResult::class.java.name) - - val getGenericIterableData = taskFactory.compose(sandboxFunction).apply(GetGenericIterableData::class.java) - val dataItemResult = getGenericIterableData.apply(sandboxWrapper) ?: fail("Result cannot be null") - assertEquals(SANDBOX_STRING, dataItemResult::class.java.name) - } - } - - class GetGenericData : Function, Any?> { - override fun apply(input: GenericWrapper): Any? { - return input.data - } - } - - class GetGenericIterableData : Function>, Any?> { - override fun apply(input: GenericWrapper>): Any? { - return input.data.iterator().let { - if (it.hasNext()) { - it.next() - } else { - null - } - } - } - } - - @Test - fun `test deserializing concrete wrapper`() { - val wrapped = ConcreteWrapper( - first = GenericWrapper("Hello World"), - second = GenericWrapper('!') - ) - val data = wrapped.serialize() - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - - val sandboxWrapped = data.deserializeFor(classLoader) - - val taskFactory = classLoader.createRawTaskFactory() - val showConcreteWrapper = taskFactory.compose(classLoader.createSandboxFunction()).apply(ShowConcreteWrapper::class.java) - val result = showConcreteWrapper.apply(sandboxWrapped) ?: fail("Result cannot be null") - - assertEquals("Concrete: first='Hello World', second='!'", result.toString()) - } - } - - class ShowConcreteWrapper : Function { - override fun apply(input: ConcreteWrapper): String { - return "Concrete: first='${input.first.data}', second='${input.second.data}'" - } - } -} - -@CordaSerializable -data class GenericWrapper(val data: T) - -@CordaSerializable -data class ConcreteWrapper( - val first: GenericWrapper, - val second: GenericWrapper -) diff --git a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeInputStreamTest.kt b/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeInputStreamTest.kt deleted file mode 100644 index 37af4815c7..0000000000 --- a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeInputStreamTest.kt +++ /dev/null @@ -1,43 +0,0 @@ -package net.corda.serialization.djvm - -import net.corda.core.internal.readFully -import net.corda.core.serialization.internal._contextSerializationEnv -import net.corda.core.serialization.serialize -import net.corda.serialization.djvm.SandboxType.KOTLIN -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.fail -import java.io.InputStream -import java.util.function.Function - -@ExtendWith(LocalSerialization::class) -class DeserializeInputStreamTest : TestBase(KOTLIN) { - companion object { - const val MESSAGE = "Round and round the rugged rocks..." - } - - @Test - fun `test deserializing input stream`() { - val data = MESSAGE.byteInputStream().serialize() - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - - val sandboxStream = data.deserializeTypeFor(classLoader) - - val taskFactory = classLoader.createRawTaskFactory() - val showInputStream = taskFactory.compose(classLoader.createSandboxFunction()).apply(ShowInputStream::class.java) - val result = showInputStream.apply(sandboxStream) ?: fail("Result cannot be null") - - assertEquals(String(MESSAGE.byteInputStream().readFully()), result.toString()) - assertEquals(SANDBOX_STRING, result::class.java.name) - } - } - - class ShowInputStream : Function { - override fun apply(input: InputStream): String { - return String(input.readFully()) - } - } -} diff --git a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeInstantTest.kt b/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeInstantTest.kt deleted file mode 100644 index 33e7685513..0000000000 --- a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeInstantTest.kt +++ /dev/null @@ -1,39 +0,0 @@ -package net.corda.serialization.djvm - -import net.corda.core.serialization.internal._contextSerializationEnv -import net.corda.core.serialization.serialize -import net.corda.serialization.djvm.SandboxType.KOTLIN -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.fail -import java.time.Instant -import java.util.function.Function - -@ExtendWith(LocalSerialization::class) -class DeserializeInstantTest : TestBase(KOTLIN) { - @Test - fun `test deserializing instant`() { - val instant = Instant.now() - val data = instant.serialize() - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - - val sandboxInstant = data.deserializeFor(classLoader) - - val taskFactory = classLoader.createRawTaskFactory() - val showInstant = taskFactory.compose(classLoader.createSandboxFunction()).apply(ShowInstant::class.java) - val result = showInstant.apply(sandboxInstant) ?: fail("Result cannot be null") - - assertEquals(instant.toString(), result.toString()) - assertEquals(SANDBOX_STRING, result::class.java.name) - } - } - - class ShowInstant : Function { - override fun apply(instant: Instant): String { - return instant.toString() - } - } -} diff --git a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeJavaWithMultipleConstructorsTest.kt b/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeJavaWithMultipleConstructorsTest.kt deleted file mode 100644 index 26381f6a34..0000000000 --- a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeJavaWithMultipleConstructorsTest.kt +++ /dev/null @@ -1,39 +0,0 @@ -package net.corda.serialization.djvm - -import net.corda.core.serialization.internal._contextSerializationEnv -import net.corda.core.serialization.serialize -import net.corda.serialization.djvm.SandboxType.KOTLIN -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.fail - -import java.util.function.Function - -@ExtendWith(LocalSerialization::class) -class DeserializeJavaWithMultipleConstructorsTest : TestBase(KOTLIN) { - @Test - fun `test deserializing existing class`() { - val multiData = MultiConstructorData("Hello World", Long.MAX_VALUE, '!') - val data = multiData.serialize() - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - - val sandboxData = data.deserializeFor(classLoader) - - val taskFactory = classLoader.createRawTaskFactory() - val showMultiData = taskFactory.compose(classLoader.createSandboxFunction()).apply(ShowMultiData::class.java) - val result = showMultiData.apply(sandboxData) ?: fail("Result cannot be null") - - assertThat(result.toString()) - .isEqualTo("MultiConstructor[message='Hello World', bigNumber=9223372036854775807, tag=!]") - } - } - - class ShowMultiData : Function { - override fun apply(data: MultiConstructorData): String { - return data.toString() - } - } -} diff --git a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeKotlinAliasTest.kt b/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeKotlinAliasTest.kt deleted file mode 100644 index bc2c023db2..0000000000 --- a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeKotlinAliasTest.kt +++ /dev/null @@ -1,69 +0,0 @@ -package net.corda.serialization.djvm - -import net.corda.core.crypto.SecureHash -import net.corda.core.node.services.AttachmentId -import net.corda.core.serialization.CordaSerializable -import net.corda.core.serialization.internal._contextSerializationEnv -import net.corda.core.serialization.serialize -import net.corda.serialization.djvm.SandboxType.KOTLIN -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.fail -import java.util.function.Function - -@ExtendWith(LocalSerialization::class) -class DeserializeKotlinAliasTest : TestBase(KOTLIN) { - @Test - fun `test deserializing kotlin alias`() { - val attachmentId = SecureHash.allOnesHash - val data = attachmentId.serialize() - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - - val sandboxAttachmentId = data.deserializeFor(classLoader) - - val taskFactory = classLoader.createRawTaskFactory() - val showAlias = taskFactory.compose(classLoader.createSandboxFunction()).apply(ShowAlias::class.java) - val result = showAlias.apply(sandboxAttachmentId) ?: fail("Result cannot be null") - - assertEquals(ShowAlias().apply(attachmentId), result.toString()) - assertEquals(SANDBOX_STRING, result::class.java.name) - } - } - - class ShowAlias : Function { - override fun apply(id: AttachmentId): String { - return id.toString() - } - } - - @Test - fun `test deserializing data with kotlin alias`() { - val attachment = AttachmentData(SecureHash.allOnesHash) - val data = attachment.serialize() - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - - val sandboxAttachment = data.deserializeFor(classLoader) - - val taskFactory = classLoader.createRawTaskFactory() - val showAliasData = taskFactory.compose(classLoader.createSandboxFunction()).apply(ShowAliasData::class.java) - val result = showAliasData.apply(sandboxAttachment) ?: fail("Result cannot be null") - - assertEquals(ShowAliasData().apply(attachment), result.toString()) - assertEquals(SANDBOX_STRING, result::class.java.name) - } - } - - class ShowAliasData: Function { - override fun apply(data: AttachmentData): String { - return data.toString() - } - } -} - -@CordaSerializable -data class AttachmentData(val id: AttachmentId) diff --git a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeLocalDateTest.kt b/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeLocalDateTest.kt deleted file mode 100644 index efc6c28c6e..0000000000 --- a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeLocalDateTest.kt +++ /dev/null @@ -1,39 +0,0 @@ -package net.corda.serialization.djvm - -import net.corda.core.serialization.internal._contextSerializationEnv -import net.corda.core.serialization.serialize -import net.corda.serialization.djvm.SandboxType.KOTLIN -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.fail -import java.time.LocalDate -import java.util.function.Function - -@ExtendWith(LocalSerialization::class) -class DeserializeLocalDateTest : TestBase(KOTLIN) { - @Test - fun `test deserializing local date`() { - val date = LocalDate.now() - val data = date.serialize() - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - - val sandboxDate = data.deserializeFor(classLoader) - - val taskFactory = classLoader.createRawTaskFactory() - val showLocalDate = taskFactory.compose(classLoader.createSandboxFunction()).apply(ShowLocalDate::class.java) - val result = showLocalDate.apply(sandboxDate) ?: fail("Result cannot be null") - - assertEquals(date.toString(), result.toString()) - assertEquals(SANDBOX_STRING, result::class.java.name) - } - } - - class ShowLocalDate : Function { - override fun apply(date: LocalDate): String { - return date.toString() - } - } -} diff --git a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeLocalDateTimeTest.kt b/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeLocalDateTimeTest.kt deleted file mode 100644 index 04c87136e7..0000000000 --- a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeLocalDateTimeTest.kt +++ /dev/null @@ -1,39 +0,0 @@ -package net.corda.serialization.djvm - -import net.corda.core.serialization.internal._contextSerializationEnv -import net.corda.core.serialization.serialize -import net.corda.serialization.djvm.SandboxType.KOTLIN -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.fail -import java.time.LocalDateTime -import java.util.function.Function - -@ExtendWith(LocalSerialization::class) -class DeserializeLocalDateTimeTest : TestBase(KOTLIN) { - @Test - fun `test deserializing local date-time`() { - val dateTime = LocalDateTime.now() - val data = dateTime.serialize() - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - - val sandboxDateTime = data.deserializeFor(classLoader) - - val taskFactory = classLoader.createRawTaskFactory() - val showLocalDateTime = taskFactory.compose(classLoader.createSandboxFunction()).apply(ShowLocalDateTime::class.java) - val result = showLocalDateTime.apply( sandboxDateTime) ?: fail("Result cannot be null") - - assertEquals(dateTime.toString(), result.toString()) - assertEquals(SANDBOX_STRING, result::class.java.name) - } - } - - class ShowLocalDateTime : Function { - override fun apply(dateTime: LocalDateTime): String { - return dateTime.toString() - } - } -} diff --git a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeLocalTimeTest.kt b/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeLocalTimeTest.kt deleted file mode 100644 index de5cc7b738..0000000000 --- a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeLocalTimeTest.kt +++ /dev/null @@ -1,39 +0,0 @@ -package net.corda.serialization.djvm - -import net.corda.core.serialization.internal._contextSerializationEnv -import net.corda.core.serialization.serialize -import net.corda.serialization.djvm.SandboxType.KOTLIN -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.fail -import java.time.LocalTime -import java.util.function.Function - -@ExtendWith(LocalSerialization::class) -class DeserializeLocalTimeTest : TestBase(KOTLIN) { - @Test - fun `test deserializing local time`() { - val time = LocalTime.now() - val data = time.serialize() - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - - val sandboxTime = data.deserializeFor(classLoader) - - val taskFactory = classLoader.createRawTaskFactory() - val showLocalTime = taskFactory.compose(classLoader.createSandboxFunction()).apply(ShowLocalTime::class.java) - val result = showLocalTime.apply(sandboxTime) ?: fail("Result cannot be null") - - assertEquals(time.toString(), result.toString()) - assertEquals(SANDBOX_STRING, result::class.java.name) - } - } - - class ShowLocalTime : Function { - override fun apply(time: LocalTime): String { - return time.toString() - } - } -} diff --git a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeMapsTest.kt b/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeMapsTest.kt deleted file mode 100644 index 9214a79a5f..0000000000 --- a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeMapsTest.kt +++ /dev/null @@ -1,203 +0,0 @@ -package net.corda.serialization.djvm - -import net.corda.core.serialization.CordaSerializable -import net.corda.core.serialization.internal._contextSerializationEnv -import net.corda.core.serialization.serialize -import net.corda.serialization.djvm.SandboxType.KOTLIN -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.fail -import java.util.EnumMap -import java.util.NavigableMap -import java.util.SortedMap -import java.util.TreeMap -import java.util.function.Function - -@ExtendWith(LocalSerialization::class) -class DeserializeMapsTest : TestBase(KOTLIN) { - @Test - fun `test deserializing map`() { - val stringMap = StringMap(mapOf("Open" to "Hello World", "Close" to "Goodbye, Cruel World")) - val data = stringMap.serialize() - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - - val sandboxMap = data.deserializeFor(classLoader) - - val taskFactory = classLoader.createRawTaskFactory() - val showStringMap = taskFactory.compose(classLoader.createSandboxFunction()).apply(ShowStringMap::class.java) - val result = showStringMap.apply(sandboxMap) ?: fail("Result cannot be null") - - assertEquals(stringMap.values.entries.joinToString(), result.toString()) - assertEquals("Open=Hello World, Close=Goodbye, Cruel World", result.toString()) - } - } - - class ShowStringMap : Function { - override fun apply(data: StringMap): String { - return data.values.entries.joinToString() - } - } - - @Test - fun `test deserializing sorted map`() { - val sortedMap = StringSortedMap(sortedMapOf( - 100 to "Goodbye, Cruel World", - 10 to "Hello World", - 50 to "Having Fun!" - )) - val data = sortedMap.serialize() - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - - val sandboxMap = data.deserializeFor(classLoader) - - val taskFactory = classLoader.createRawTaskFactory() - val showStringSortedMap = taskFactory.compose(classLoader.createSandboxFunction()).apply(ShowStringSortedMap::class.java) - val result = showStringSortedMap.apply(sandboxMap) ?: fail("Result cannot be null") - - assertEquals(sortedMap.values.entries.joinToString(), result.toString()) - assertEquals("10=Hello World, 50=Having Fun!, 100=Goodbye, Cruel World", result.toString()) - } - } - - class ShowStringSortedMap : Function { - override fun apply(data: StringSortedMap): String { - return data.values.entries.joinToString() - } - } - - @Test - fun `test deserializing navigable map`() { - val navigableMap = StringNavigableMap(mapOf( - 10000L to "Goodbye, Cruel World", - 1000L to "Hello World", - 5000L to "Having Fun!" - ).toMap(TreeMap())) - val data = navigableMap.serialize() - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - - val sandboxMap = data.deserializeFor(classLoader) - - val taskFactory = classLoader.createRawTaskFactory() - val showStringNavigableMap = taskFactory.compose(classLoader.createSandboxFunction()).apply(ShowStringNavigableMap::class.java) - val result = showStringNavigableMap.apply(sandboxMap) ?: fail("Result cannot be null") - - assertEquals(navigableMap.values.entries.joinToString(), result.toString()) - assertEquals("1000=Hello World, 5000=Having Fun!, 10000=Goodbye, Cruel World", result.toString()) - } - } - - class ShowStringNavigableMap : Function { - override fun apply(data: StringNavigableMap): String { - return data.values.entries.joinToString() - } - } - - @Test - fun `test deserializing linked hash map`() { - val linkedHashMap = StringLinkedHashMap(linkedMapOf( - "Close" to "Goodbye, Cruel World", - "Open" to "Hello World", - "During" to "Having Fun!" - )) - val data = linkedHashMap.serialize() - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - - val sandboxMap = data.deserializeFor(classLoader) - - val taskFactory = classLoader.createRawTaskFactory() - val sandboxFunction = classLoader.createSandboxFunction() - val showStringLinkedHashMap = taskFactory.compose(sandboxFunction).apply(ShowStringLinkedHashMap::class.java) - val result = showStringLinkedHashMap.apply(sandboxMap) ?: fail("Result cannot be null") - - assertEquals(linkedHashMap.values.entries.joinToString(), result.toString()) - assertEquals("Close=Goodbye, Cruel World, Open=Hello World, During=Having Fun!", result.toString()) - } - } - - class ShowStringLinkedHashMap : Function { - override fun apply(data: StringLinkedHashMap): String { - return data.values.entries.joinToString() - } - } - - @Test - fun `test deserializing tree map`() { - val treeMap = StringTreeMap(mapOf( - 10000 to "Goodbye, Cruel World", - 1000 to "Hello World", - 5000 to "Having Fun!" - ).toMap(TreeMap())) - val data = treeMap.serialize() - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - - val sandboxMap = data.deserializeFor(classLoader) - - val taskFactory = classLoader.createRawTaskFactory() - val showStringTreeMap = taskFactory.compose(classLoader.createSandboxFunction()).apply(ShowStringTreeMap::class.java) - val result = showStringTreeMap.apply(sandboxMap) ?: fail("Result cannot be null") - - assertEquals(treeMap.values.entries.joinToString(), result.toString()) - assertEquals("1000=Hello World, 5000=Having Fun!, 10000=Goodbye, Cruel World", result.toString()) - } - } - - class ShowStringTreeMap : Function { - override fun apply(data: StringTreeMap): String { - return data.values.entries.joinToString() - } - } - - @Test - fun `test deserializing enum map`() { - val enumMap = EnumMap(mapOf( - ExampleEnum.ONE to "One!", - ExampleEnum.TWO to "Two!" - )) - val data = enumMap.serialize() - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - - val sandboxMap = data.deserializeFor(classLoader) - - val taskFactory = classLoader.createRawTaskFactory() - val showEnumMap = taskFactory.compose(classLoader.createSandboxFunction()).apply(ShowEnumMap::class.java) - val result = showEnumMap.apply(sandboxMap) ?: fail("Result cannot be null") - - assertEquals(enumMap.toString(), result.toString()) - assertEquals("{ONE=One!, TWO=Two!}", result.toString()) - } - } - - class ShowEnumMap : Function, String> { - override fun apply(data: EnumMap<*, String>): String { - return data.toString() - } - } -} - -@CordaSerializable -class StringMap(val values: Map) - -@CordaSerializable -class StringSortedMap(val values: SortedMap) - -@CordaSerializable -class StringNavigableMap(val values: NavigableMap) - -@CordaSerializable -class StringLinkedHashMap(val values: LinkedHashMap) - -@CordaSerializable -class StringTreeMap(val values: TreeMap) diff --git a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeMonthDayTest.kt b/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeMonthDayTest.kt deleted file mode 100644 index edc4ca56ab..0000000000 --- a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeMonthDayTest.kt +++ /dev/null @@ -1,39 +0,0 @@ -package net.corda.serialization.djvm - -import net.corda.core.serialization.internal._contextSerializationEnv -import net.corda.core.serialization.serialize -import net.corda.serialization.djvm.SandboxType.KOTLIN -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.fail -import java.time.MonthDay -import java.util.function.Function - -@ExtendWith(LocalSerialization::class) -class DeserializeMonthDayTest : TestBase(KOTLIN) { - @Test - fun `test deserializing month-day`() { - val monthDay = MonthDay.now() - val data = monthDay.serialize() - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - - val sandboxMonthDay = data.deserializeFor(classLoader) - - val taskFactory = classLoader.createRawTaskFactory() - val showMonthDay = taskFactory.compose(classLoader.createSandboxFunction()).apply(ShowMonthDay::class.java) - val result = showMonthDay.apply(sandboxMonthDay) ?: fail("Result cannot be null") - - assertEquals(monthDay.toString(), result.toString()) - assertEquals(SANDBOX_STRING, result::class.java.name) - } - } - - class ShowMonthDay : Function { - override fun apply(monthDay: MonthDay): String { - return monthDay.toString() - } - } -} diff --git a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeObjectArraysTest.kt b/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeObjectArraysTest.kt deleted file mode 100644 index 0090ae2535..0000000000 --- a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeObjectArraysTest.kt +++ /dev/null @@ -1,298 +0,0 @@ -package net.corda.serialization.djvm - -import net.corda.core.serialization.CordaSerializable -import net.corda.core.serialization.internal._contextSerializationEnv -import net.corda.core.serialization.serialize -import net.corda.serialization.djvm.SandboxType.KOTLIN -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.fail -import java.util.UUID -import java.util.function.Function - -@ExtendWith(LocalSerialization::class) -class DeserializeObjectArraysTest : TestBase(KOTLIN) { - @Test - fun `test deserializing string array`() { - val stringArray = HasStringArray(arrayOf("Hello", "World", "!")) - val data = stringArray.serialize() - assertEquals("Hello, World, !", ShowStringArray().apply(stringArray)) - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - - val sandboxArray = data.deserializeFor(classLoader) - - val taskFactory = classLoader.createRawTaskFactory() - val showStringArray = taskFactory.compose(classLoader.createSandboxFunction()).apply(ShowStringArray::class.java) - val result = showStringArray.apply(sandboxArray) ?: fail("Result cannot be null") - - assertEquals(SANDBOX_STRING, result::class.java.name) - assertEquals("Hello, World, !", result.toString()) - } - } - - class ShowStringArray : Function { - override fun apply(data: HasStringArray): String { - return data.lines.joinToString() - } - } - - @Test - fun `test deserializing character array`() { - val charArray = HasCharacterArray(arrayOf('H', 'e', 'l', 'l', 'o', '!')) - val data = charArray.serialize() - assertEquals("Hello!", ShowCharacterArray().apply(charArray)) - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - - val sandboxArray = data.deserializeFor(classLoader) - - val taskFactory = classLoader.createRawTaskFactory() - val showCharacterArray = taskFactory.compose(classLoader.createSandboxFunction()).apply(ShowCharacterArray::class.java) - val result = showCharacterArray.apply(sandboxArray) ?: fail("Result cannot be null") - - assertEquals(SANDBOX_STRING, result::class.java.name) - assertEquals("Hello!", result.toString()) - } - } - - class ShowCharacterArray : Function { - override fun apply(data: HasCharacterArray): String { - return data.letters.joinTo(StringBuilder(), separator = "").toString() - } - } - - @Test - fun `test deserializing long array`() { - val longArray = HasLongArray(arrayOf(1000, 2000, 3000, 4000, 5000)) - val data = longArray.serialize() - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - - val sandboxArray = data.deserializeFor(classLoader) - - val taskFactory = classLoader.createRawTaskFactory() - val showLongArray = taskFactory.compose(classLoader.createSandboxFunction()).apply(ShowLongArray::class.java) - val result = showLongArray.apply(sandboxArray) ?: fail("Result cannot be null") - - assertEquals("sandbox.java.lang.Long", result::class.java.name) - assertEquals("15000", result.toString()) - } - } - - class ShowLongArray : Function { - override fun apply(data: HasLongArray): Long { - return data.longs.sum() - } - } - - @Test - fun `test deserializing integer array`() { - val integerArray = HasIntegerArray(arrayOf(100, 200, 300, 400, 500)) - val data = integerArray.serialize() - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - - val sandboxArray = data.deserializeFor(classLoader) - - val taskFactory = classLoader.createRawTaskFactory() - val showIntegerArray = taskFactory.compose(classLoader.createSandboxFunction()).apply(ShowIntegerArray::class.java) - val result = showIntegerArray.apply(sandboxArray) ?: fail("Result cannot be null") - - assertEquals("sandbox.java.lang.Integer", result::class.java.name) - assertEquals("1500", result.toString()) - } - } - - class ShowIntegerArray : Function { - override fun apply(data: HasIntegerArray): Int { - return data.integers.sum() - } - } - - @Test - fun `test deserializing short array`() { - val shortArray = HasShortArray(arrayOf(100, 200, 300, 400, 500)) - val data = shortArray.serialize() - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - - val sandboxArray = data.deserializeFor(classLoader) - - val taskFactory = classLoader.createRawTaskFactory() - val showShortArray = taskFactory.compose(classLoader.createSandboxFunction()).apply(ShowShortArray::class.java) - val result = showShortArray.apply(sandboxArray) ?: fail("Result cannot be null") - - assertEquals("sandbox.java.lang.Integer", result::class.java.name) - assertEquals("1500", result.toString()) - } - } - - class ShowShortArray : Function { - override fun apply(data: HasShortArray): Int { - return data.shorts.sum() - } - } - - @Test - fun `test deserializing byte array`() { - val byteArray = HasByteArray(arrayOf(10, 20, 30, 40, 50)) - val data = byteArray.serialize() - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - - val sandboxArray = data.deserializeFor(classLoader) - - val taskFactory = classLoader.createRawTaskFactory() - val showByteArray = taskFactory.compose(classLoader.createSandboxFunction()).apply(ShowByteArray::class.java) - val result = showByteArray.apply(sandboxArray) ?: fail("Result cannot be null") - - assertEquals("sandbox.java.lang.Integer", result::class.java.name) - assertEquals("150", result.toString()) - } - } - - class ShowByteArray : Function { - override fun apply(data: HasByteArray): Int { - return data.bytes.sum() - } - } - - @Test - fun `test deserializing double array`() { - val doubleArray = HasDoubleArray(arrayOf(1000.0, 2000.0, 3000.0, 4000.0, 5000.0)) - val data = doubleArray.serialize() - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - - val sandboxArray = data.deserializeFor(classLoader) - - val taskFactory = classLoader.createRawTaskFactory() - val showDoubleArray = taskFactory.compose(classLoader.createSandboxFunction()).apply(ShowDoubleArray::class.java) - val result = showDoubleArray.apply(sandboxArray) ?: fail("Result cannot be null") - - assertEquals("sandbox.java.lang.Double", result::class.java.name) - assertEquals("15000.0", result.toString()) - } - } - - class ShowDoubleArray : Function { - override fun apply(data: HasDoubleArray): Double { - return data.doubles.sum() - } - } - - @Test - fun `test deserializing float array`() { - val floatArray = HasFloatArray(arrayOf(10.0f, 20.0f, 30.0f, 40.0f, 50.0f)) - val data = floatArray.serialize() - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - - val sandboxArray = data.deserializeFor(classLoader) - - val taskFactory = classLoader.createRawTaskFactory() - val showFloatArray = taskFactory.compose(classLoader.createSandboxFunction()).apply(ShowFloatArray::class.java) - val result = showFloatArray.apply(sandboxArray) ?: fail("Result cannot be null") - - assertEquals("sandbox.java.lang.Float", result::class.java.name) - assertEquals("150.0", result.toString()) - } - } - - class ShowFloatArray : Function { - override fun apply(data: HasFloatArray): Float { - return data.floats.sum() - } - } - - @Test - fun `test deserializing boolean array`() { - val booleanArray = HasBooleanArray(arrayOf(true, true, true)) - val data = booleanArray.serialize() - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - - val sandboxArray = data.deserializeFor(classLoader) - - val taskFactory = classLoader.createRawTaskFactory() - val showBooleanArray = taskFactory.compose(classLoader.createSandboxFunction()).apply(ShowBooleanArray::class.java) - val result = showBooleanArray.apply(sandboxArray) ?: fail("Result cannot be null") - - assertEquals("sandbox.java.lang.Boolean", result::class.java.name) - assertEquals("true", result.toString()) - } - } - - class ShowBooleanArray : Function { - override fun apply(data: HasBooleanArray): Boolean { - return data.bools.all { it } - } - } - - @Test - fun `test deserializing uuid array`() { - val uuid = UUID.randomUUID() - val uuidArray = HasUUIDArray(arrayOf(uuid)) - val data = uuidArray.serialize() - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - - val sandboxArray = data.deserializeFor(classLoader) - - val taskFactory = classLoader.createRawTaskFactory() - val showUUIDArray = taskFactory.compose(classLoader.createSandboxFunction()).apply(ShowUUIDArray::class.java) - val result = showUUIDArray.apply(sandboxArray) ?: fail("Result cannot be null") - - assertEquals(SANDBOX_STRING, result::class.java.name) - assertEquals(uuid.toString(), result.toString()) - } - } - - class ShowUUIDArray : Function { - override fun apply(data: HasUUIDArray): String { - return data.uuids.joinTo(StringBuilder()).toString() - } - } -} - -@CordaSerializable -class HasStringArray(val lines: Array) - -@CordaSerializable -class HasCharacterArray(val letters: Array) - -@CordaSerializable -class HasLongArray(val longs: Array) - -@CordaSerializable -class HasIntegerArray(val integers: Array) - -@CordaSerializable -class HasShortArray(val shorts: Array) - -@CordaSerializable -class HasByteArray(val bytes: Array) - -@CordaSerializable -class HasDoubleArray(val doubles: Array) - -@CordaSerializable -class HasFloatArray(val floats: Array) - -@CordaSerializable -class HasBooleanArray(val bools: Array) - -@CordaSerializable -class HasUUIDArray(val uuids: Array) diff --git a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeOffsetDateTimeTest.kt b/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeOffsetDateTimeTest.kt deleted file mode 100644 index d21d135f1c..0000000000 --- a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeOffsetDateTimeTest.kt +++ /dev/null @@ -1,39 +0,0 @@ -package net.corda.serialization.djvm - -import net.corda.core.serialization.internal._contextSerializationEnv -import net.corda.core.serialization.serialize -import net.corda.serialization.djvm.SandboxType.KOTLIN -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.fail -import java.time.OffsetDateTime -import java.util.function.Function - -@ExtendWith(LocalSerialization::class) -class DeserializeOffsetDateTimeTest : TestBase(KOTLIN) { - @Test - fun `test deserializing offset date-time`() { - val dateTime = OffsetDateTime.now() - val data = dateTime.serialize() - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - - val sandboxDateTime = data.deserializeFor(classLoader) - - val taskFactory = classLoader.createRawTaskFactory() - val showOffsetDateTime = taskFactory.compose(classLoader.createSandboxFunction()).apply(ShowOffsetDateTime::class.java) - val result = showOffsetDateTime.apply(sandboxDateTime) ?: fail("Result cannot be null") - - assertEquals(dateTime.toString(), result.toString()) - assertEquals(SANDBOX_STRING, result::class.java.name) - } - } - - class ShowOffsetDateTime : Function { - override fun apply(dateTime: OffsetDateTime): String { - return dateTime.toString() - } - } -} diff --git a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeOffsetTimeTest.kt b/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeOffsetTimeTest.kt deleted file mode 100644 index c23fe08ebe..0000000000 --- a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeOffsetTimeTest.kt +++ /dev/null @@ -1,39 +0,0 @@ -package net.corda.serialization.djvm - -import net.corda.core.serialization.internal._contextSerializationEnv -import net.corda.core.serialization.serialize -import net.corda.serialization.djvm.SandboxType.KOTLIN -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.fail -import java.time.OffsetTime -import java.util.function.Function - -@ExtendWith(LocalSerialization::class) -class DeserializeOffsetTimeTest : TestBase(KOTLIN) { - @Test - fun `test deserializing offset time`() { - val time = OffsetTime.now() - val data = time.serialize() - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - - val sandboxTime = data.deserializeFor(classLoader) - - val taskFactory = classLoader.createRawTaskFactory() - val showOffsetTime = taskFactory.compose(classLoader.createSandboxFunction()).apply(ShowOffsetTime::class.java) - val result = showOffsetTime.apply(sandboxTime) ?: fail("Result cannot be null") - - assertEquals(time.toString(), result.toString()) - assertEquals(SANDBOX_STRING, result::class.java.name) - } - } - - class ShowOffsetTime : Function { - override fun apply(time: OffsetTime): String { - return time.toString() - } - } -} diff --git a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeOpaqueBytesSubSequenceTest.kt b/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeOpaqueBytesSubSequenceTest.kt deleted file mode 100644 index ca1041af21..0000000000 --- a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeOpaqueBytesSubSequenceTest.kt +++ /dev/null @@ -1,49 +0,0 @@ -package net.corda.serialization.djvm - -import net.corda.core.serialization.internal._contextSerializationEnv -import net.corda.core.serialization.serialize -import net.corda.core.utilities.OpaqueBytesSubSequence -import net.corda.serialization.djvm.SandboxType.KOTLIN -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.fail -import java.util.function.Function - -@ExtendWith(LocalSerialization::class) -class DeserializeOpaqueBytesSubSequenceTest : TestBase(KOTLIN) { - companion object { - const val MESSAGE = "The rain in spain falls mainly on the plain." - const val OFFSET = MESSAGE.length / 2 - } - - @Test - fun `test deserializing opaquebytes subsequence`() { - val subSequence = OpaqueBytesSubSequence( - bytes = MESSAGE.toByteArray(), - offset = OFFSET, - size = MESSAGE.length - OFFSET - ) - val data = subSequence.serialize() - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - - val sandboxBytes = data.deserializeFor(classLoader) - - val taskFactory = classLoader.createRawTaskFactory() - val sandboxFunction = classLoader.createSandboxFunction() - val showOpaqueBytesSubSequence = taskFactory.compose(sandboxFunction).apply(ShowOpaqueBytesSubSequence::class.java) - val result = showOpaqueBytesSubSequence.apply(sandboxBytes) ?: fail("Result cannot be null") - - assertEquals(MESSAGE.substring(OFFSET), String(result as ByteArray)) - assertEquals(String(subSequence.copyBytes()), String(result)) - } - } - - class ShowOpaqueBytesSubSequence : Function { - override fun apply(sequence: OpaqueBytesSubSequence): ByteArray { - return sequence.copyBytes() - } - } -} diff --git a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeOptionalTest.kt b/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeOptionalTest.kt deleted file mode 100644 index f908ff439e..0000000000 --- a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeOptionalTest.kt +++ /dev/null @@ -1,58 +0,0 @@ -package net.corda.serialization.djvm - -import net.corda.core.serialization.internal._contextSerializationEnv -import net.corda.core.serialization.serialize -import net.corda.serialization.djvm.SandboxType.KOTLIN -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.fail -import java.util.Optional -import java.util.function.Function - -@ExtendWith(LocalSerialization::class) -class DeserializeOptionalTest : TestBase(KOTLIN) { - @Test - fun `test deserializing optional with object`() { - val optional = Optional.of("Hello World!") - val data = optional.serialize() - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - - val sandboxOptional = data.deserializeFor(classLoader) - - val taskFactory = classLoader.createRawTaskFactory() - val showOptional = taskFactory.compose(classLoader.createSandboxFunction()).apply(ShowOptional::class.java) - val result = showOptional.apply(sandboxOptional) ?: fail("Result cannot be null") - - assertEquals("Optional -> Optional[Hello World!]", result.toString()) - assertEquals(SANDBOX_STRING, result::class.java.name) - } - } - - @Test - fun `test deserializing optional without object`() { - val optional = Optional.empty() - val data = optional.serialize() - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - - val sandboxOptional = data.deserializeFor(classLoader) - - val taskFactory = classLoader.createRawTaskFactory() - val showOptional = taskFactory.compose(classLoader.createSandboxFunction()).apply(ShowOptional::class.java) - val result = showOptional.apply(sandboxOptional) ?: fail("Result cannot be null") - - assertEquals("Optional -> Optional.empty", result.toString()) - assertEquals(SANDBOX_STRING, result::class.java.name) - } - } -} - -class ShowOptional : Function, String> { - override fun apply(optional: Optional<*>): String { - return "Optional -> $optional" - } -} diff --git a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializePeriodTest.kt b/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializePeriodTest.kt deleted file mode 100644 index 781d74a634..0000000000 --- a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializePeriodTest.kt +++ /dev/null @@ -1,39 +0,0 @@ -package net.corda.serialization.djvm - -import net.corda.core.serialization.internal._contextSerializationEnv -import net.corda.core.serialization.serialize -import net.corda.serialization.djvm.SandboxType.KOTLIN -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.fail -import java.time.Period -import java.util.function.Function - -@ExtendWith(LocalSerialization::class) -class DeserializePeriodTest : TestBase(KOTLIN) { - @Test - fun `test deserializing period`() { - val period = Period.of(1, 2, 3) - val data = period.serialize() - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - - val sandboxPeriod = data.deserializeFor(classLoader) - - val taskFactory = classLoader.createRawTaskFactory() - val showPeriod = taskFactory.compose(classLoader.createSandboxFunction()).apply(ShowPeriod::class.java) - val result = showPeriod.apply(sandboxPeriod) ?: fail("Result cannot be null") - - assertEquals(period.toString(), result.toString()) - assertEquals(SANDBOX_STRING, result::class.java.name) - } - } - - class ShowPeriod : Function { - override fun apply(period: Period): String { - return period.toString() - } - } -} diff --git a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializePrimitiveArraysTest.kt b/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializePrimitiveArraysTest.kt deleted file mode 100644 index 74c7af3e16..0000000000 --- a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializePrimitiveArraysTest.kt +++ /dev/null @@ -1,239 +0,0 @@ -package net.corda.serialization.djvm - -import net.corda.core.serialization.CordaSerializable -import net.corda.core.serialization.internal._contextSerializationEnv -import net.corda.core.serialization.serialize -import net.corda.serialization.djvm.SandboxType.KOTLIN -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.fail -import java.util.function.Function - -@ExtendWith(LocalSerialization::class) -class DeserializePrimitiveArraysTest : TestBase(KOTLIN) { - @Test - fun `test deserializing character array`() { - val charArray = PrimitiveCharArray(charArrayOf('H', 'e', 'l', 'l', 'o', '!')) - val data = charArray.serialize() - assertEquals("Hello!", ShowCharArray().apply(charArray)) - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - - val sandboxArray = data.deserializeFor(classLoader) - - val taskFactory = classLoader.createRawTaskFactory() - val showCharArray = taskFactory.compose(classLoader.createSandboxFunction()).apply(ShowCharArray::class.java) - val result = showCharArray.apply(sandboxArray) ?: fail("Result cannot be null") - - assertEquals(SANDBOX_STRING, result::class.java.name) - assertEquals("Hello!", result.toString()) - } - } - - class ShowCharArray : Function { - override fun apply(data: PrimitiveCharArray): String { - return data.letters.joinTo(StringBuilder(), separator = "").toString() - } - } - - @Test - fun `test deserializing integer array`() { - val intArray = PrimitiveIntegerArray(intArrayOf(100, 200, 300, 400, 500)) - val data = intArray.serialize() - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - - val sandboxArray = data.deserializeFor(classLoader) - - val taskFactory = classLoader.createRawTaskFactory() - val showIntegerArray = taskFactory.compose(classLoader.createSandboxFunction()).apply(ShowIntegerArray::class.java) - val result = showIntegerArray.apply(sandboxArray) ?: fail("Result cannot be null") - - assertEquals("sandbox.java.lang.Integer", result::class.java.name) - assertEquals("1500", result.toString()) - } - } - - class ShowIntegerArray : Function { - override fun apply(data: PrimitiveIntegerArray): Int { - return data.integers.sum() - } - } - - @Test - fun `test deserializing long array`() { - val longArray = PrimitiveLongArray(longArrayOf(1000, 2000, 3000, 4000, 5000)) - val data = longArray.serialize() - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - - val sandboxArray = data.deserializeFor(classLoader) - - val taskFactory = classLoader.createRawTaskFactory() - val showLongArray = taskFactory.compose(classLoader.createSandboxFunction()).apply(ShowLongArray::class.java) - val result = showLongArray.apply(sandboxArray) ?: fail("Result cannot be null") - - assertEquals("sandbox.java.lang.Long", result::class.java.name) - assertEquals("15000", result.toString()) - } - } - - class ShowLongArray : Function { - override fun apply(data: PrimitiveLongArray): Long { - return data.longs.sum() - } - } - - @Test - fun `test deserializing short array`() { - val shortArray = PrimitiveShortArray(shortArrayOf(100, 200, 300, 400, 500)) - val data = shortArray.serialize() - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - - val sandboxArray = data.deserializeFor(classLoader) - - val taskFactory = classLoader.createRawTaskFactory() - val showShortArray = taskFactory.compose(classLoader.createSandboxFunction()).apply(ShowShortArray::class.java) - val result = showShortArray.apply(sandboxArray) ?: fail("Result cannot be null") - - assertEquals("sandbox.java.lang.Integer", result::class.java.name) - assertEquals("1500", result.toString()) - } - } - - class ShowShortArray : Function { - override fun apply(data: PrimitiveShortArray): Int { - return data.shorts.sum() - } - } - - @Test - fun `test deserializing byte array`() { - val byteArray = PrimitiveByteArray(byteArrayOf(10, 20, 30, 40, 50)) - val data = byteArray.serialize() - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - - val sandboxArray = data.deserializeFor(classLoader) - - val taskFactory = classLoader.createRawTaskFactory() - val showByteArray = taskFactory.compose(classLoader.createSandboxFunction()).apply(ShowByteArray::class.java) - val result = showByteArray.apply(sandboxArray) ?: fail("Result cannot be null") - - assertEquals("sandbox.java.lang.Integer", result::class.java.name) - assertEquals("150", result.toString()) - } - } - - class ShowByteArray : Function { - override fun apply(data: PrimitiveByteArray): Int { - return data.bytes.sum() - } - } - - @Test - fun `test deserializing boolean array`() { - val booleanArray = PrimitiveBooleanArray(booleanArrayOf(true, true)) - val data = booleanArray.serialize() - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - - val sandboxArray = data.deserializeFor(classLoader) - - val taskFactory = classLoader.createRawTaskFactory() - val showBooleanArray = taskFactory.compose(classLoader.createSandboxFunction()).apply(ShowBooleanArray::class.java) - val result = showBooleanArray.apply(sandboxArray) ?: fail("Result cannot be null") - - assertEquals("sandbox.java.lang.Boolean", result::class.java.name) - assertEquals("true", result.toString()) - } - } - - class ShowBooleanArray : Function { - override fun apply(data: PrimitiveBooleanArray): Boolean { - return data.bools.all { it } - } - } - - @Test - fun `test deserializing double array`() { - val doubleArray = PrimitiveDoubleArray(doubleArrayOf(1000.0, 2000.0, 3000.0, 4000.0, 5000.0)) - val data = doubleArray.serialize() - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - - val sandboxArray = data.deserializeFor(classLoader) - - val taskFactory = classLoader.createRawTaskFactory() - val showDoubleArray = taskFactory.compose(classLoader.createSandboxFunction()).apply(ShowDoubleArray::class.java) - val result = showDoubleArray.apply(sandboxArray) ?: fail("Result cannot be null") - - assertEquals("sandbox.java.lang.Double", result::class.java.name) - assertEquals("15000.0", result.toString()) - } - } - - class ShowDoubleArray : Function { - override fun apply(data: PrimitiveDoubleArray): Double { - return data.doubles.sum() - } - } - - @Test - fun `test deserializing float array`() { - val floatArray = PrimitiveFloatArray(floatArrayOf(100.0f, 200.0f, 300.0f, 400.0f, 500.0f)) - val data = floatArray.serialize() - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - - val sandboxArray = data.deserializeFor(classLoader) - - val taskFactory = classLoader.createRawTaskFactory() - val showFloatArray = taskFactory.compose(classLoader.createSandboxFunction()).apply(ShowFloatArray::class.java) - val result = showFloatArray.apply(sandboxArray) ?: fail("Result cannot be null") - - assertEquals("sandbox.java.lang.Float", result::class.java.name) - assertEquals("1500.0", result.toString()) - } - } - - class ShowFloatArray : Function { - override fun apply(data: PrimitiveFloatArray): Float { - return data.floats.sum() - } - } -} - -@CordaSerializable -class PrimitiveCharArray(val letters: CharArray) - -@CordaSerializable -class PrimitiveShortArray(val shorts: ShortArray) - -@CordaSerializable -class PrimitiveIntegerArray(val integers: IntArray) - -@CordaSerializable -class PrimitiveLongArray(val longs: LongArray) - -@CordaSerializable -class PrimitiveByteArray(val bytes: ByteArray) - -@CordaSerializable -class PrimitiveBooleanArray(val bools: BooleanArray) - -@CordaSerializable -class PrimitiveDoubleArray(val doubles: DoubleArray) - -@CordaSerializable -class PrimitiveFloatArray(val floats: FloatArray) diff --git a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializePrimitivesTest.kt b/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializePrimitivesTest.kt deleted file mode 100644 index 00c5fce429..0000000000 --- a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializePrimitivesTest.kt +++ /dev/null @@ -1,76 +0,0 @@ -package net.corda.serialization.djvm - -import net.corda.core.serialization.CordaSerializable -import net.corda.core.serialization.internal._contextSerializationEnv -import net.corda.core.serialization.serialize -import net.corda.serialization.djvm.SandboxType.KOTLIN -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import java.util.Date -import java.util.UUID - -@ExtendWith(LocalSerialization::class) -class DeserializePrimitivesTest : TestBase(KOTLIN) { - @Test - fun `test naked uuid`() { - val uuid = UUID.randomUUID() - val data = uuid.serialize() - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - - val sandboxUUID = data.deserializeFor(classLoader) - assertEquals(uuid.toString(), sandboxUUID.toString()) - assertEquals("sandbox.${uuid::class.java.name}", sandboxUUID::class.java.name) - } - } - - @Test - fun `test wrapped uuid`() { - val uuid = WrappedUUID(UUID.randomUUID()) - val data = uuid.serialize() - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - - val sandboxUUID = data.deserializeFor(classLoader) - assertEquals(uuid.toString(), sandboxUUID.toString()) - assertEquals("sandbox.${uuid::class.java.name}", sandboxUUID::class.java.name) - } - } - - @Test - fun `test naked date`() { - val now = Date() - val data = now.serialize() - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - - val sandboxNow = data.deserializeFor(classLoader) - assertEquals(now.toString(), sandboxNow.toString()) - assertEquals("sandbox.${now::class.java.name}", sandboxNow::class.java.name) - } - } - - @Test - fun `test wrapped date`() { - val now = WrappedDate(Date()) - val data = now.serialize() - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - - val sandboxNow = data.deserializeFor(classLoader) - assertEquals(now.toString(), sandboxNow.toString()) - assertEquals("sandbox.${now::class.java.name}", sandboxNow::class.java.name) - } - } -} - -@CordaSerializable -data class WrappedUUID(val uuid: UUID) - -@CordaSerializable -data class WrappedDate(val date: Date) diff --git a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeProtonJTest.kt b/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeProtonJTest.kt deleted file mode 100644 index 579d8b7a16..0000000000 --- a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeProtonJTest.kt +++ /dev/null @@ -1,245 +0,0 @@ -package net.corda.serialization.djvm - -import net.corda.core.serialization.CordaSerializable -import net.corda.core.serialization.internal._contextSerializationEnv -import net.corda.core.serialization.serialize -import net.corda.serialization.djvm.SandboxType.KOTLIN -import org.apache.qpid.proton.amqp.Decimal128 -import org.apache.qpid.proton.amqp.Decimal32 -import org.apache.qpid.proton.amqp.Decimal64 -import org.apache.qpid.proton.amqp.Symbol -import org.apache.qpid.proton.amqp.UnsignedByte -import org.apache.qpid.proton.amqp.UnsignedInteger -import org.apache.qpid.proton.amqp.UnsignedLong -import org.apache.qpid.proton.amqp.UnsignedShort -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.fail -import java.util.function.Function - -@ExtendWith(LocalSerialization::class) -class DeserializeProtonJTest : TestBase(KOTLIN) { - @Test - fun `test deserializing unsigned long`() { - val protonJ = HasUnsignedLong(UnsignedLong.valueOf(12345678)) - val data = protonJ.serialize() - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - - val sandboxProtonJ = data.deserializeFor(classLoader) - - val taskFactory = classLoader.createRawTaskFactory() - val showUnsignedLong = taskFactory.compose(classLoader.createSandboxFunction()).apply(ShowUnsignedLong::class.java) - val result = showUnsignedLong.apply(sandboxProtonJ) ?: fail("Result cannot be null") - - assertEquals(protonJ.number.toString(), result.toString()) - assertEquals(SANDBOX_STRING, result::class.java.name) - } - } - - class ShowUnsignedLong : Function { - override fun apply(data: HasUnsignedLong): String { - return data.number.toString() - } - } - - @Test - fun `test deserializing unsigned integer`() { - val protonJ = HasUnsignedInteger(UnsignedInteger.valueOf(123456)) - val data = protonJ.serialize() - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - - val sandboxProtonJ = data.deserializeFor(classLoader) - - val taskFactory = classLoader.createRawTaskFactory() - val showUnsignedInteger = taskFactory.compose(classLoader.createSandboxFunction()).apply(ShowUnsignedInteger::class.java) - val result = showUnsignedInteger.apply(sandboxProtonJ) ?: fail("Result cannot be null") - - assertEquals(protonJ.number.toString(), result.toString()) - assertEquals(SANDBOX_STRING, result::class.java.name) - } - } - - class ShowUnsignedInteger : Function { - override fun apply(data: HasUnsignedInteger): String { - return data.number.toString() - } - } - - @Test - fun `test deserializing unsigned short`() { - val protonJ = HasUnsignedShort(UnsignedShort.valueOf(12345)) - val data = protonJ.serialize() - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - - val sandboxProtonJ = data.deserializeFor(classLoader) - - val taskFactory = classLoader.createRawTaskFactory() - val showUnsignedShort = taskFactory.compose(classLoader.createSandboxFunction()).apply(ShowUnsignedShort::class.java) - val result = showUnsignedShort.apply(sandboxProtonJ) ?: fail("Result cannot be null") - - assertEquals(protonJ.number.toString(), result.toString()) - assertEquals(SANDBOX_STRING, result::class.java.name) - } - } - - class ShowUnsignedShort : Function { - override fun apply(data: HasUnsignedShort): String { - return data.number.toString() - } - } - - @Test - fun `test deserializing unsigned byte`() { - val protonJ = HasUnsignedByte(UnsignedByte.valueOf(0x8f.toByte())) - val data = protonJ.serialize() - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - - val sandboxProtonJ = data.deserializeFor(classLoader) - - val taskFactory = classLoader.createRawTaskFactory() - val showUnsignedByte = taskFactory.compose(classLoader.createSandboxFunction()).apply(ShowUnsignedByte::class.java) - val result = showUnsignedByte.apply(sandboxProtonJ) ?: fail("Result cannot be null") - - assertEquals(protonJ.number.toString(), result.toString()) - assertEquals(SANDBOX_STRING, result::class.java.name) - } - } - - class ShowUnsignedByte : Function { - override fun apply(data: HasUnsignedByte): String { - return data.number.toString() - } - } - - @Test - fun `test deserializing 128 bit decimal`() { - val protonJ = HasDecimal128(Decimal128(12345678, 98765432)) - val data = protonJ.serialize() - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - - val sandboxProtonJ = data.deserializeFor(classLoader) - - val taskFactory = classLoader.createRawTaskFactory() - val showDecimal128 = taskFactory.compose(classLoader.createSandboxFunction()).apply(ShowDecimal128::class.java) - val result = showDecimal128.apply(sandboxProtonJ) ?: fail("Result cannot be null") - - assertThat(result) - .isEqualTo(protonJ.number.let { longArrayOf(it.mostSignificantBits, it.leastSignificantBits) }) - } - } - - class ShowDecimal128 : Function { - override fun apply(data: HasDecimal128): LongArray { - return data.number.let { longArrayOf(it.mostSignificantBits, it.leastSignificantBits) } - } - } - - @Test - fun `test deserializing 64 bit decimal`() { - val protonJ = HasDecimal64(Decimal64(98765432)) - val data = protonJ.serialize() - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - - val sandboxProtonJ = data.deserializeFor(classLoader) - - val taskFactory = classLoader.createRawTaskFactory() - val showDecimal64 = taskFactory.compose(classLoader.createSandboxFunction()).apply(ShowDecimal64::class.java) - val result = showDecimal64.apply(sandboxProtonJ) ?: fail("Result cannot be null") - - assertEquals(protonJ.number.bits.toString(), result.toString()) - } - } - - class ShowDecimal64 : Function { - override fun apply(data: HasDecimal64): Long { - return data.number.bits - } - } - - @Test - fun `test deserializing 32 bit decimal`() { - val protonJ = HasDecimal32(Decimal32(123456)) - val data = protonJ.serialize() - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - - val sandboxProtonJ = data.deserializeFor(classLoader) - - val taskFactory = classLoader.createRawTaskFactory() - val showDecimal32 = taskFactory.compose(classLoader.createSandboxFunction()).apply(ShowDecimal32::class.java) - val result = showDecimal32.apply(sandboxProtonJ) ?: fail("Result cannot be null") - - assertEquals(protonJ.number.bits.toString(), result.toString()) - } - } - - class ShowDecimal32 : Function { - override fun apply(data: HasDecimal32): Int { - return data.number.bits - } - } - - @Test - fun `test deserializing symbol`() { - val protonJ = HasSymbol(Symbol.valueOf("-my-symbol-value-")) - val data = protonJ.serialize() - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - - val sandboxProtonJ = data.deserializeFor(classLoader) - - val taskFactory = classLoader.createRawTaskFactory() - val showSymbol = taskFactory.compose(classLoader.createSandboxFunction()).apply(ShowSymbol::class.java) - val result = showSymbol.apply(sandboxProtonJ) ?: fail("Result cannot be null") - - assertEquals(protonJ.symbol.toString(), result.toString()) - assertEquals(SANDBOX_STRING, result::class.java.name) - } - } - - class ShowSymbol : Function { - override fun apply(data: HasSymbol): String { - return data.symbol.toString() - } - } -} - -@CordaSerializable -class HasUnsignedLong(val number: UnsignedLong) - -@CordaSerializable -class HasUnsignedInteger(val number: UnsignedInteger) - -@CordaSerializable -class HasUnsignedShort(val number: UnsignedShort) - -@CordaSerializable -class HasUnsignedByte(val number: UnsignedByte) - -@CordaSerializable -class HasDecimal32(val number: Decimal32) - -@CordaSerializable -class HasDecimal64(val number: Decimal64) - -@CordaSerializable -class HasDecimal128(val number: Decimal128) - -@CordaSerializable -class HasSymbol(val symbol: Symbol) diff --git a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializePublicKeyTest.kt b/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializePublicKeyTest.kt deleted file mode 100644 index dc982a8569..0000000000 --- a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializePublicKeyTest.kt +++ /dev/null @@ -1,104 +0,0 @@ -package net.corda.serialization.djvm - -import net.corda.core.crypto.CompositeKey -import net.corda.core.crypto.Crypto -import net.corda.core.crypto.SignatureScheme -import net.corda.core.internal.hash -import net.corda.core.serialization.CordaSerializable -import net.corda.core.serialization.internal._contextSerializationEnv -import net.corda.core.serialization.serialize -import net.corda.serialization.djvm.SandboxType.KOTLIN -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.ExtensionContext -import org.junit.jupiter.api.fail -import org.junit.jupiter.params.ParameterizedTest -import org.junit.jupiter.params.provider.Arguments -import org.junit.jupiter.params.provider.ArgumentsProvider -import org.junit.jupiter.params.provider.ArgumentsSource -import java.security.PublicKey -import java.util.function.Function -import java.util.stream.Stream - -@ExtendWith(LocalSerialization::class) -class DeserializePublicKeyTest : TestBase(KOTLIN) { - class SignatureSchemeProvider : ArgumentsProvider { - override fun provideArguments(context: ExtensionContext?): Stream { - return Crypto.supportedSignatureSchemes().stream() - .filter { it != Crypto.COMPOSITE_KEY } - .map { Arguments.of(it) } - } - } - - @ArgumentsSource(SignatureSchemeProvider::class) - @ParameterizedTest(name = "{index} => {0}") - fun `test deserializing public key`(signatureScheme: SignatureScheme) { - val keyPair = Crypto.generateKeyPair(signatureScheme) - val publicKey = PublicKeyData(keyPair.public) - val data = publicKey.serialize() - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - - val sandboxKey = data.deserializeFor(classLoader) - - val taskFactory = classLoader.createRawTaskFactory() - val showPublicKey = taskFactory.compose(classLoader.createSandboxFunction()).apply(ShowPublicKey::class.java) - val result = showPublicKey.apply(sandboxKey) ?: fail("Result cannot be null") - - assertEquals(ShowPublicKey().apply(publicKey), result.toString()) - assertEquals(SANDBOX_STRING, result::class.java.name) - } - } - - @Test - fun `test composite public key`() { - val key1 = Crypto.generateKeyPair(Crypto.ECDSA_SECP256K1_SHA256).public - val key2 = Crypto.generateKeyPair(Crypto.ECDSA_SECP256R1_SHA256).public - val key3 = Crypto.generateKeyPair(Crypto.EDDSA_ED25519_SHA512).public - - val compositeKey = CompositeKey.Builder() - .addKey(key1, weight = 1) - .addKey(key2, weight = 1) - .addKey(key3, weight = 1) - .build(2) - val compositeData = PublicKeyData(compositeKey) - val data = compositeData.serialize() - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - - val sandboxData = data.deserializeFor(classLoader) - - val taskFactory = classLoader.createRawTaskFactory().compose(classLoader.createSandboxFunction()) - val showPublicKey = taskFactory.apply(ShowPublicKey::class.java) - val result = showPublicKey.apply(sandboxData) ?: fail("Result cannot be null") - - assertEquals(ShowPublicKey().apply(compositeData), result.toString()) - - val sandboxKey = taskFactory.apply(GetPublicKey::class.java) - .apply(sandboxData) ?: fail("PublicKey cannot be null") - assertThat(sandboxKey::class.java.name) - .isEqualTo("sandbox." + CompositeKey::class.java.name) - } - } - - class ShowPublicKey : Function { - override fun apply(data: PublicKeyData): String { - return with(data) { - "PublicKey: algorithm='${key.algorithm}', format='${key.format}', hash=${key.hash}" - } - } - } - - class GetPublicKey : Function { - override fun apply(data: PublicKeyData): PublicKey { - return data.key - } - } -} - -@CordaSerializable -data class PublicKeyData(val key: PublicKey) diff --git a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeRemoteCustomisedEnumTest.kt b/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeRemoteCustomisedEnumTest.kt deleted file mode 100644 index 7c2f5ffee7..0000000000 --- a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeRemoteCustomisedEnumTest.kt +++ /dev/null @@ -1,144 +0,0 @@ -package net.corda.serialization.djvm - -import net.corda.core.serialization.CordaSerializable -import net.corda.core.serialization.SerializedBytes -import net.corda.core.serialization.internal._contextSerializationEnv -import net.corda.core.serialization.serialize -import net.corda.serialization.djvm.SandboxType.KOTLIN -import net.corda.serialization.internal.amqp.CompositeType -import net.corda.serialization.internal.amqp.DeserializationInput -import net.corda.serialization.internal.amqp.RestrictedType -import net.corda.serialization.internal.amqp.TypeNotation -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.fail -import org.junit.jupiter.params.ParameterizedTest -import org.junit.jupiter.params.provider.EnumSource -import java.util.function.Function - -/** - * Corda 4.4 briefly serialised [Enum] values using [Enum.toString] rather - * than [Enum.name]. We need to be able to deserialise these values now - * that the bug has been fixed. - */ -@ExtendWith(LocalSerialization::class) -class DeserializeRemoteCustomisedEnumTest : TestBase(KOTLIN) { - @ParameterizedTest - @EnumSource(Broken::class) - fun `test deserialize broken enum with custom toString`(broken: Broken) { - val workingData = broken.serialize().rewriteEnumAsWorking() - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - - val sandboxWorkingClass = classLoader.toSandboxClass(Working::class.java) - val sandboxWorkingValue = workingData.deserializeFor(classLoader) - assertThat(sandboxWorkingValue::class.java).isSameAs(sandboxWorkingClass) - assertThat(sandboxWorkingValue.toString()).isEqualTo(broken.label) - } - } - - /** - * This function rewrites the [SerializedBytes] for a naked [Broken] object - * into the [SerializedBytes] that Corda 4.4 would generate for an equivalent - * [Working] object. - */ - @Suppress("unchecked_cast") - private fun SerializedBytes.rewriteEnumAsWorking(): SerializedBytes { - val envelope = DeserializationInput.getEnvelope(this).apply { - val restrictedType = schema.types[0] as RestrictedType - (schema.types as MutableList)[0] = restrictedType.copy( - name = toWorking(restrictedType.name) - ) - } - return SerializedBytes(envelope.write()) - } - - @ParameterizedTest - @EnumSource(Broken::class) - fun `test deserialize composed broken enum with custom toString`(broken: Broken) { - val brokenContainer = BrokenContainer(broken) - val workingData = brokenContainer.serialize().rewriteContainerAsWorking() - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - - val sandboxContainer = workingData.deserializeFor(classLoader) - - val taskFactory = classLoader.createRawTaskFactory() - val showWorkingData = taskFactory.compose(classLoader.createSandboxFunction()).apply(ShowWorkingData::class.java) - val result = showWorkingData.apply(sandboxContainer) ?: fail("Result cannot be null") - - assertEquals("Working: label='${broken.label}', ordinal='${broken.ordinal}'", result.toString()) - assertEquals(SANDBOX_STRING, result::class.java.name) - } - } - - class ShowWorkingData : Function { - override fun apply(input: WorkingContainer): String { - return with(input) { - "Working: label='${value.label}', ordinal='${value.ordinal}'" - } - } - } - - /** - * This function rewrites the [SerializedBytes] for a [Broken] - * property that has been composed inside a [BrokenContainer]. - * It will generate the [SerializedBytes] that Corda 4.4 would - * generate for an equivalent [WorkingContainer]. - */ - @Suppress("unchecked_cast") - private fun SerializedBytes.rewriteContainerAsWorking(): SerializedBytes { - val envelope = DeserializationInput.getEnvelope(this).apply { - val compositeType = schema.types[0] as CompositeType - (schema.types as MutableList)[0] = compositeType.copy( - name = toWorking(compositeType.name), - fields = compositeType.fields.map { it.copy(type = toWorking(it.type)) } - ) - val restrictedType = schema.types[1] as RestrictedType - (schema.types as MutableList)[1] = restrictedType.copy( - name = toWorking(restrictedType.name) - ) - } - return SerializedBytes(envelope.write()) - } - - private fun toWorking(oldName: String): String = oldName.replace("Broken", "Working") - - /** - * This is the enumerated type, as it actually exist. - */ - @Suppress("unused") - enum class Working(val label: String) { - ZERO("None"), - ONE("Once"), - TWO("Twice"); - - @Override - override fun toString(): String = label - } - - @CordaSerializable - data class WorkingContainer(val value: Working) - - /** - * This represents a broken serializer's view of the [Working] - * enumerated type, which would serialize using [Enum.toString] - * rather than [Enum.name]. - */ - @Suppress("unused") - @CordaSerializable - enum class Broken(val label: String) { - None("None"), - Once("Once"), - Twice("Twice"); - - @Override - override fun toString(): String = label - } - - @CordaSerializable - data class BrokenContainer(val value: Broken) -} diff --git a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeStringBufferTest.kt b/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeStringBufferTest.kt deleted file mode 100644 index 35a5f9cfb0..0000000000 --- a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeStringBufferTest.kt +++ /dev/null @@ -1,38 +0,0 @@ -package net.corda.serialization.djvm - -import net.corda.core.serialization.internal._contextSerializationEnv -import net.corda.core.serialization.serialize -import net.corda.serialization.djvm.SandboxType.KOTLIN -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.fail -import java.util.function.Function - -@ExtendWith(LocalSerialization::class) -class DeserializeStringBufferTest : TestBase(KOTLIN) { - @Test - fun `test deserializing string buffer`() { - val buffer = StringBuffer("Hello World!") - val data = buffer.serialize() - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - - val sandboxBuffer = data.deserializeFor(classLoader) - - val taskFactory = classLoader.createRawTaskFactory() - val showStringBuffer = taskFactory.compose(classLoader.createSandboxFunction()).apply( ShowStringBuffer::class.java) - val result = showStringBuffer.apply(sandboxBuffer) ?: fail("Result cannot be null") - - assertEquals(ShowStringBuffer().apply(buffer), result.toString()) - assertEquals(SANDBOX_STRING, result::class.java.name) - } - } - - class ShowStringBuffer : Function { - override fun apply(buffer: StringBuffer): String { - return buffer.toString() - } - } -} diff --git a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeStringTest.kt b/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeStringTest.kt deleted file mode 100644 index 914ca2f387..0000000000 --- a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeStringTest.kt +++ /dev/null @@ -1,71 +0,0 @@ -package net.corda.serialization.djvm - -import net.corda.core.serialization.CordaSerializable -import net.corda.core.serialization.internal._contextSerializationEnv -import net.corda.core.serialization.serialize -import net.corda.serialization.djvm.SandboxType.KOTLIN -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.fail -import java.util.function.Function - -@ExtendWith(LocalSerialization::class) -class DeserializeStringTest : TestBase(KOTLIN) { - @Test - fun `test deserializing string`() { - val stringMessage = StringMessage("Hello World!") - val data = stringMessage.serialize() - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - - val sandboxString = data.deserializeFor(classLoader) - - val taskFactory = classLoader.createRawTaskFactory() - val showStringMessage = taskFactory.compose(classLoader.createSandboxFunction()).apply(ShowStringMessage::class.java) - val result = showStringMessage.apply(sandboxString) ?: fail("Result cannot be null") - - assertEquals(stringMessage.message, result.toString()) - assertEquals(SANDBOX_STRING, result::class.java.name) - } - } - - class ShowStringMessage : Function { - override fun apply(data: StringMessage): String { - return data.message - } - } - - @Test - fun `test deserializing string list of arrays`() { - val stringListArray = StringListOfArray(listOf( - arrayOf("Hello"), arrayOf("World"), arrayOf("!")) - ) - val data = stringListArray.serialize() - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - - val sandboxListArray = data.deserializeFor(classLoader) - - val taskFactory = classLoader.createRawTaskFactory() - val showStringListOfArray = taskFactory.compose(classLoader.createSandboxFunction()).apply(ShowStringListOfArray::class.java) - val result = showStringListOfArray.apply(sandboxListArray) ?: fail("Result cannot be null") - - assertEquals(stringListArray.data.flatMap(Array::toList).joinToString(), result.toString()) - } - } - - class ShowStringListOfArray : Function { - override fun apply(obj: StringListOfArray): String { - return obj.data.flatMap(Array::toList).joinToString() - } - } -} - -@CordaSerializable -data class StringMessage(val message: String) - -@CordaSerializable -class StringListOfArray(val data: List>) diff --git a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeWithCustomSerializerTest.kt b/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeWithCustomSerializerTest.kt deleted file mode 100644 index 36a0a65178..0000000000 --- a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeWithCustomSerializerTest.kt +++ /dev/null @@ -1,88 +0,0 @@ -package net.corda.serialization.djvm - -import net.corda.core.serialization.SerializationCustomSerializer -import net.corda.core.serialization.internal.MissingSerializerException -import net.corda.core.serialization.internal._contextSerializationEnv -import net.corda.core.serialization.serialize -import net.corda.serialization.djvm.SandboxType.KOTLIN -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.BeforeAll -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.assertThrows -import org.junit.jupiter.api.extension.RegisterExtension -import org.junit.jupiter.api.fail -import java.util.function.Function - -class DeserializeWithCustomSerializerTest: TestBase(KOTLIN) { - companion object { - const val MESSAGE = "Hello Sandbox!" - - @Suppress("unused") - @BeforeAll - @JvmStatic - fun checkData() { - assertNotCordaSerializable() - } - } - - @RegisterExtension - @JvmField - val serialization = LocalSerialization(setOf(CustomSerializer()), emptySet()) - - @Test - fun `test deserializing custom object`() { - val custom = CustomData(MESSAGE) - val data = custom.serialize() - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv( - classLoader = classLoader, - customSerializerClassNames = setOf(CustomSerializer::class.java.name), - serializationWhitelistNames = emptySet() - )) - - val sandboxCustom = data.deserializeFor(classLoader) - - val taskFactory = classLoader.createRawTaskFactory() - val showCustom = taskFactory.compose(classLoader.createSandboxFunction()).apply(ShowCustomData::class.java) - val result = showCustom.apply(sandboxCustom) ?: fail("Result cannot be null") - - assertEquals(custom.value, result.toString()) - assertEquals(SANDBOX_STRING, result::class.java.name) - } - } - - @Test - fun `test deserialization needs custom serializer`() { - val custom = CustomData(MESSAGE) - val data = custom.serialize() - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - assertThrows { data.deserializeFor(classLoader) } - } - } - - class ShowCustomData : Function { - override fun apply(custom: CustomData): String { - return custom.value - } - } - - /** - * This class REQUIRES a custom serializer because its - * constructor parameter cannot be mapped to a property - * automatically. THIS IS DELIBERATE! - */ - class CustomData(initialValue: String) { - // DO NOT MOVE THIS PROPERTY INTO THE CONSTRUCTOR! - val value: String = initialValue - } - - class CustomSerializer : SerializationCustomSerializer { - data class Proxy(val value: String) - - override fun fromProxy(proxy: Proxy): CustomData = CustomData(proxy.value) - override fun toProxy(obj: CustomData): Proxy = Proxy(obj.value) - } -} diff --git a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeWithObjectCustomSerializerTest.kt b/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeWithObjectCustomSerializerTest.kt deleted file mode 100644 index 2b86b220db..0000000000 --- a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeWithObjectCustomSerializerTest.kt +++ /dev/null @@ -1,75 +0,0 @@ -package net.corda.serialization.djvm - -import net.corda.core.serialization.SerializationCustomSerializer -import net.corda.core.serialization.internal._contextSerializationEnv -import net.corda.core.serialization.serialize -import net.corda.serialization.djvm.SandboxType.KOTLIN -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.BeforeAll -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.RegisterExtension -import org.junit.jupiter.api.fail -import java.util.function.Function - -class DeserializeWithObjectCustomSerializerTest: TestBase(KOTLIN) { - companion object { - const val MESSAGE = "Hello Sandbox!" - - @Suppress("unused") - @BeforeAll - @JvmStatic - fun checkData() { - assertNotCordaSerializable() - } - } - - @RegisterExtension - @JvmField - val serialization = LocalSerialization(setOf(ObjectCustomSerializer), emptySet()) - - @Test - fun `test deserializing custom object with object serializer`() { - val custom = CustomData(MESSAGE) - val data = custom.serialize() - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv( - classLoader = classLoader, - customSerializerClassNames = setOf(ObjectCustomSerializer::class.java.name), - serializationWhitelistNames = emptySet() - )) - - val sandboxCustom = data.deserializeFor(classLoader) - - val taskFactory = classLoader.createRawTaskFactory() - val showCustom = taskFactory.compose(classLoader.createSandboxFunction()).apply(ShowCustomData::class.java) - val result = showCustom.apply(sandboxCustom) ?: fail("Result cannot be null") - - assertEquals(custom.value, result.toString()) - assertEquals(SANDBOX_STRING, result::class.java.name) - } - } - - class ShowCustomData : Function { - override fun apply(custom: CustomData): String { - return custom.value - } - } - - /** - * This class REQUIRES a custom serializer because its - * constructor parameter cannot be mapped to a property - * automatically. THIS IS DELIBERATE! - */ - class CustomData(initialValue: String) { - // DO NOT MOVE THIS PROPERTY INTO THE CONSTRUCTOR! - val value: String = initialValue - } - - object ObjectCustomSerializer : SerializationCustomSerializer { - data class Proxy(val value: String) - - override fun fromProxy(proxy: Proxy): CustomData = CustomData(proxy.value) - override fun toProxy(obj: CustomData): Proxy = Proxy(obj.value) - } -} diff --git a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeWithSerializationWhitelistTest.kt b/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeWithSerializationWhitelistTest.kt deleted file mode 100644 index 8b190fe1b1..0000000000 --- a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeWithSerializationWhitelistTest.kt +++ /dev/null @@ -1,81 +0,0 @@ -package net.corda.serialization.djvm - -import net.corda.core.serialization.SerializationWhitelist -import net.corda.core.serialization.internal._contextSerializationEnv -import net.corda.core.serialization.serialize -import net.corda.serialization.djvm.SandboxType.KOTLIN -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.BeforeAll -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.assertThrows -import org.junit.jupiter.api.extension.RegisterExtension -import org.junit.jupiter.api.fail -import java.io.NotSerializableException -import java.util.function.Function - -class DeserializeWithSerializationWhitelistTest: TestBase(KOTLIN) { - companion object { - const val MESSAGE = "Hello Sandbox!" - - @Suppress("unused") - @BeforeAll - @JvmStatic - fun checkData() { - assertNotCordaSerializable() - } - } - - @RegisterExtension - @JvmField - val serialization = LocalSerialization(emptySet(), setOf(CustomWhitelist)) - - @Test - fun `test deserializing custom object`() { - val custom = CustomData(MESSAGE) - val data = custom.serialize() - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv( - classLoader = classLoader, - customSerializerClassNames = emptySet(), - serializationWhitelistNames = setOf(CustomWhitelist::class.java.name) - )) - - val sandboxCustom = data.deserializeFor(classLoader) - - val taskFactory = classLoader.createRawTaskFactory() - val showCustom = taskFactory.compose(classLoader.createSandboxFunction()).apply(ShowCustomData::class.java) - val result = showCustom.apply(sandboxCustom) ?: fail("Result cannot be null") - - assertEquals(custom.value, result.toString()) - assertEquals(SANDBOX_STRING, result::class.java.name) - } - } - - @Test - fun `test deserialization needs whitelisting`() { - val custom = CustomData(MESSAGE) - val data = custom.serialize() - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - val ex = assertThrows { data.deserializeFor(classLoader) } - assertThat(ex).hasMessageContaining( - "Class \"class sandbox.${CustomData::class.java.name}\" is not on the whitelist or annotated with @CordaSerializable." - ) - } - } - - class ShowCustomData : Function { - override fun apply(custom: CustomData): String { - return custom.value - } - } - - data class CustomData(val value: String) - - object CustomWhitelist : SerializationWhitelist { - override val whitelist: List> = listOf(CustomData::class.java) - } -} diff --git a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeYearMonthTest.kt b/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeYearMonthTest.kt deleted file mode 100644 index ab03c397d5..0000000000 --- a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeYearMonthTest.kt +++ /dev/null @@ -1,39 +0,0 @@ -package net.corda.serialization.djvm - -import net.corda.core.serialization.internal._contextSerializationEnv -import net.corda.core.serialization.serialize -import net.corda.serialization.djvm.SandboxType.KOTLIN -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.fail -import java.time.YearMonth -import java.util.function.Function - -@ExtendWith(LocalSerialization::class) -class DeserializeYearMonthTest : TestBase(KOTLIN) { - @Test - fun `test deserializing year-month`() { - val yearMonth = YearMonth.now() - val data = yearMonth.serialize() - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - - val sandboxYearMonth = data.deserializeFor(classLoader) - - val taskFactory = classLoader.createRawTaskFactory() - val showYearMonth = taskFactory.compose(classLoader.createSandboxFunction()).apply( ShowYearMonth::class.java) - val result = showYearMonth.apply(sandboxYearMonth) ?: fail("Result cannot be null") - - assertEquals(yearMonth.toString(), result.toString()) - assertEquals(SANDBOX_STRING, result::class.java.name) - } - } - - class ShowYearMonth : Function { - override fun apply(yearMonth: YearMonth): String { - return yearMonth.toString() - } - } -} diff --git a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeYearTest.kt b/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeYearTest.kt deleted file mode 100644 index 6d998ce243..0000000000 --- a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeYearTest.kt +++ /dev/null @@ -1,39 +0,0 @@ -package net.corda.serialization.djvm - -import net.corda.core.serialization.internal._contextSerializationEnv -import net.corda.core.serialization.serialize -import net.corda.serialization.djvm.SandboxType.KOTLIN -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.fail -import java.time.Year -import java.util.function.Function - -@ExtendWith(LocalSerialization::class) -class DeserializeYearTest : TestBase(KOTLIN) { - @Test - fun `test deserializing year`() { - val year = Year.now() - val data = year.serialize() - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - - val sandboxYear = data.deserializeFor(classLoader) - - val taskFactory = classLoader.createRawTaskFactory() - val showYear = taskFactory.compose(classLoader.createSandboxFunction()).apply(ShowYear::class.java) - val result = showYear.apply(sandboxYear) ?: fail("Result cannot be null") - - assertEquals(year.toString(), result.toString()) - assertEquals(SANDBOX_STRING, result::class.java.name) - } - } - - class ShowYear : Function { - override fun apply(year: Year): String { - return year.toString() - } - } -} diff --git a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeZoneIdTest.kt b/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeZoneIdTest.kt deleted file mode 100644 index fbbd937328..0000000000 --- a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeZoneIdTest.kt +++ /dev/null @@ -1,51 +0,0 @@ -package net.corda.serialization.djvm - -import net.corda.core.serialization.internal._contextSerializationEnv -import net.corda.core.serialization.serialize -import net.corda.serialization.djvm.SandboxType.KOTLIN -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.ExtensionContext -import org.junit.jupiter.api.fail -import org.junit.jupiter.params.ParameterizedTest -import org.junit.jupiter.params.provider.Arguments -import org.junit.jupiter.params.provider.ArgumentsProvider -import org.junit.jupiter.params.provider.ArgumentsSource -import java.time.ZoneId -import java.util.function.Function -import java.util.stream.Stream - -@ExtendWith(LocalSerialization::class) -class DeserializeZoneIdTest : TestBase(KOTLIN) { - class ZoneIdProvider : ArgumentsProvider { - override fun provideArguments(context: ExtensionContext?): Stream { - return ZoneId.getAvailableZoneIds().stream() - .sorted().limit(10).map { Arguments.of(ZoneId.of(it)) } - } - } - - @ArgumentsSource(ZoneIdProvider::class) - @ParameterizedTest(name = "{index} => {0}") - fun `test deserializing zone id`(zoneId: ZoneId) { - val data = zoneId.serialize() - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - - val sandboxZoneId = data.deserializeFor(classLoader) - - val taskFactory = classLoader.createRawTaskFactory() - val showZoneId = taskFactory.compose(classLoader.createSandboxFunction()).apply(ShowZoneId::class.java) - val result = showZoneId.apply(sandboxZoneId) ?: fail("Result cannot be null") - - assertEquals(zoneId.toString(), result.toString()) - assertEquals(SANDBOX_STRING, result::class.java.name) - } - } - - class ShowZoneId : Function { - override fun apply(zoneId: ZoneId): String { - return zoneId.toString() - } - } -} diff --git a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeZonedDateTimeTest.kt b/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeZonedDateTimeTest.kt deleted file mode 100644 index 4ee3ffda95..0000000000 --- a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/DeserializeZonedDateTimeTest.kt +++ /dev/null @@ -1,39 +0,0 @@ -package net.corda.serialization.djvm - -import net.corda.core.serialization.internal._contextSerializationEnv -import net.corda.core.serialization.serialize -import net.corda.serialization.djvm.SandboxType.KOTLIN -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.fail -import java.time.ZonedDateTime -import java.util.function.Function - -@ExtendWith(LocalSerialization::class) -class DeserializeZonedDateTimeTest : TestBase(KOTLIN) { - @Test - fun `test deserializing zoned date-time`() { - val dateTime = ZonedDateTime.now() - val data = dateTime.serialize() - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - - val sandboxDateTime = data.deserializeFor(classLoader) - - val taskFactory = classLoader.createRawTaskFactory() - val showZonedDateTime = taskFactory.compose(classLoader.createSandboxFunction()).apply(ShowZonedDateTime::class.java) - val result = showZonedDateTime.apply(sandboxDateTime) ?: fail("Result cannot be null") - - assertEquals(dateTime.toString(), result.toString()) - assertEquals(SANDBOX_STRING, result::class.java.name) - } - } - - class ShowZonedDateTime : Function { - override fun apply(dateTime: ZonedDateTime): String { - return dateTime.toString() - } - } -} diff --git a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/LocalSerialization.kt b/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/LocalSerialization.kt deleted file mode 100644 index 36042d6920..0000000000 --- a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/LocalSerialization.kt +++ /dev/null @@ -1,73 +0,0 @@ -package net.corda.serialization.djvm - -import net.corda.core.serialization.SerializationContext -import net.corda.core.serialization.SerializationContext.UseCase -import net.corda.core.serialization.SerializationCustomSerializer -import net.corda.core.serialization.SerializationWhitelist -import net.corda.core.serialization.internal.SerializationEnvironment -import net.corda.core.serialization.internal._contextSerializationEnv -import net.corda.serialization.internal.BuiltInExceptionsWhitelist -import net.corda.serialization.internal.CordaSerializationMagic -import net.corda.serialization.internal.GlobalTransientClassWhiteList -import net.corda.serialization.internal.SerializationContextImpl -import net.corda.serialization.internal.SerializationFactoryImpl -import net.corda.serialization.internal.amqp.AbstractAMQPSerializationScheme -import net.corda.serialization.internal.amqp.AccessOrderLinkedHashMap -import net.corda.serialization.internal.amqp.SerializationFactoryCacheKey -import net.corda.serialization.internal.amqp.SerializerFactory -import net.corda.serialization.internal.amqp.amqpMagic -import org.junit.jupiter.api.extension.AfterEachCallback -import org.junit.jupiter.api.extension.BeforeEachCallback -import org.junit.jupiter.api.extension.ExtensionContext - -class LocalSerialization( - private val customSerializers: Set>, - private val serializationWhitelists: Set -) : BeforeEachCallback, AfterEachCallback { - private companion object { - private val AMQP_P2P_CONTEXT = SerializationContextImpl( - amqpMagic, - LocalSerialization::class.java.classLoader, - GlobalTransientClassWhiteList(BuiltInExceptionsWhitelist()), - emptyMap(), - true, - UseCase.P2P, - null - ) - } - - constructor() : this(emptySet(), emptySet()) - - override fun beforeEach(context: ExtensionContext) { - _contextSerializationEnv.set(createTestSerializationEnv()) - } - - override fun afterEach(context: ExtensionContext) { - _contextSerializationEnv.set(null) - } - - private fun createTestSerializationEnv(): SerializationEnvironment { - val factory = SerializationFactoryImpl(mutableMapOf()).apply { - registerScheme(AMQPSerializationScheme(customSerializers, serializationWhitelists, AccessOrderLinkedHashMap(128))) - } - return SerializationEnvironment.with(factory, AMQP_P2P_CONTEXT) - } - - private class AMQPSerializationScheme( - customSerializers: Set>, - serializationWhitelists: Set, - serializerFactoriesForContexts: AccessOrderLinkedHashMap - ) : AbstractAMQPSerializationScheme(customSerializers, serializationWhitelists, serializerFactoriesForContexts) { - override fun rpcServerSerializerFactory(context: SerializationContext): SerializerFactory { - throw UnsupportedOperationException() - } - - override fun rpcClientSerializerFactory(context: SerializationContext): SerializerFactory { - throw UnsupportedOperationException() - } - - override fun canDeserializeVersion(magic: CordaSerializationMagic, target: UseCase): Boolean { - return canDeserializeVersion(magic) && target == UseCase.P2P - } - } -} diff --git a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/LocalTypeModelTest.kt b/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/LocalTypeModelTest.kt deleted file mode 100644 index 90b5d0813f..0000000000 --- a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/LocalTypeModelTest.kt +++ /dev/null @@ -1,210 +0,0 @@ -package net.corda.serialization.djvm - -import net.corda.core.serialization.SerializationFactory -import net.corda.core.serialization.internal._contextSerializationEnv -import net.corda.djvm.rewiring.SandboxClassLoader -import net.corda.serialization.djvm.SandboxType.KOTLIN -import net.corda.serialization.internal.SerializationFactoryImpl -import net.corda.serialization.internal.amqp.SerializerFactory -import net.corda.serialization.internal.model.LocalTypeInformation -import net.corda.serialization.internal.model.LocalTypeInformation.ACollection -import net.corda.serialization.internal.model.LocalTypeInformation.AnEnum -import net.corda.serialization.internal.model.LocalTypeInformation.AMap -import net.corda.serialization.internal.model.LocalTypeInformation.Abstract -import net.corda.serialization.internal.model.LocalTypeInformation.Atomic -import net.corda.serialization.internal.model.LocalTypeInformation.Opaque -import org.apache.qpid.proton.amqp.Decimal128 -import org.apache.qpid.proton.amqp.Decimal32 -import org.apache.qpid.proton.amqp.Decimal64 -import org.apache.qpid.proton.amqp.Symbol -import org.apache.qpid.proton.amqp.UnsignedByte -import org.apache.qpid.proton.amqp.UnsignedInteger -import org.apache.qpid.proton.amqp.UnsignedLong -import org.apache.qpid.proton.amqp.UnsignedShort -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.Test -import java.util.Date -import java.util.EnumSet -import java.util.UUID - -class LocalTypeModelTest : TestBase(KOTLIN) { - private val serializerFactory: SerializerFactory get() { - val factory = SerializationFactory.defaultFactory as SerializationFactoryImpl - val scheme = factory.getRegisteredSchemes().single() as AMQPSerializationScheme - return scheme.serializerFactory - } - - private inline fun sandbox(classLoader: SandboxClassLoader): Class<*> { - return classLoader.toSandboxClass(T::class.java) - } - - private inline fun assertLocalType(type: Class<*>): LOCAL { - return assertLocalType(LOCAL::class.java, type) as LOCAL - } - - private fun assertLocalType(localType: Class, type: Class<*>): LocalTypeInformation { - val typeData = serializerFactory.getTypeInformation(type) - assertThat(typeData).isInstanceOf(localType) - return typeData - } - - @Test - fun testString() = sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - assertLocalType(sandbox(classLoader)) - } - - @Test - fun testLong() = sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - assertLocalType(sandbox(classLoader)) - assertLocalType(Long::class.javaPrimitiveType!!) - } - - @Test - fun testInteger() = sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - assertLocalType(sandbox(classLoader)) - assertLocalType(Int::class.javaPrimitiveType!!) - } - - @Test - fun testShort() = sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - assertLocalType(sandbox(classLoader)) - assertLocalType(Short::class.javaPrimitiveType!!) - } - - @Test - fun testByte() = sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - assertLocalType(sandbox(classLoader)) - assertLocalType(Byte::class.javaPrimitiveType!!) - } - - @Test - fun testDouble() = sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - assertLocalType(sandbox(classLoader)) - assertLocalType(Double::class.javaPrimitiveType!!) - } - - @Test - fun testFloat() = sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - assertLocalType(sandbox(classLoader)) - assertLocalType(Float::class.javaPrimitiveType!!) - } - - @Test - fun testChar() = sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - assertLocalType(sandbox(classLoader)) - assertLocalType(Char::class.javaPrimitiveType!!) - } - - @Test - fun testUnsignedLong() = sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - assertLocalType(sandbox(classLoader)) - } - - @Test - fun testUnsignedInteger() = sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - assertLocalType(sandbox(classLoader)) - } - - @Test - fun testUnsignedShort() = sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - assertLocalType(sandbox(classLoader)) - } - - @Test - fun testUnsignedByte() = sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - assertLocalType(sandbox(classLoader)) - } - - @Test - fun testDecimal32() = sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - assertLocalType(sandbox(classLoader)) - } - - @Test - fun testDecimal64() = sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - assertLocalType(sandbox(classLoader)) - } - - @Test - fun testDecimal128() = sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - assertLocalType(sandbox(classLoader)) - } - - @Test - fun testSymbol() = sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - assertLocalType(sandbox(classLoader)) - } - - @Test - fun testUUID() = sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - assertLocalType(sandbox(classLoader)) - } - - @Test - fun testDate() = sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - assertLocalType(sandbox(classLoader)) - } - - @Test - fun testCollection() = sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - assertLocalType(sandbox>(classLoader)) - } - - @Test - fun testEnum() = sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - assertLocalType(sandbox(classLoader)) - } - - @Test - fun testCustomEnum() = sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - val anEnum = assertLocalType(sandbox(classLoader)) - assertThat(anEnum.members) - .containsExactlyElementsOf(CustomEnum::class.java.enumConstants.map(CustomEnum::name)) - } - - @Test - fun testEnumSet() = sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - assertLocalType(sandbox>(classLoader)) - - val exampleEnumSet = EnumSet.noneOf(ExampleEnum::class.java) - assertLocalType(classLoader.toSandboxClass(exampleEnumSet::class.java)) - } - - @Test - fun testMap() = sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - assertLocalType(sandbox>(classLoader)) - } - - @Suppress("unused") - enum class CustomEnum { - ONE, - TWO; - - override fun toString(): String { - return "[${name.toLowerCase()}]" - } - } -} \ No newline at end of file diff --git a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/SafeDeserialisationTest.kt b/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/SafeDeserialisationTest.kt deleted file mode 100644 index 1552279f45..0000000000 --- a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/SafeDeserialisationTest.kt +++ /dev/null @@ -1,65 +0,0 @@ -package net.corda.serialization.djvm - -import net.corda.core.serialization.SerializedBytes -import net.corda.core.serialization.internal._contextSerializationEnv -import net.corda.core.serialization.serialize -import net.corda.serialization.djvm.SandboxType.KOTLIN -import net.corda.serialization.internal.amqp.CompositeType -import net.corda.serialization.internal.amqp.DeserializationInput -import net.corda.serialization.internal.amqp.TypeNotation -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.assertThrows -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.fail -import java.util.function.Function - -@ExtendWith(LocalSerialization::class) -class SafeDeserialisationTest : TestBase(KOTLIN) { - companion object { - const val MESSAGE = "Nothing to see here..." - const val NUMBER = 123.toShort() - } - - @Test - fun `test deserialising an evil class`() { - val context = (_contextSerializationEnv.get() ?: fail("No serialization environment!")).p2pContext - - val innocent = InnocentData(MESSAGE, NUMBER) - val innocentData = innocent.serialize() - val envelope = DeserializationInput.getEnvelope(innocentData, context.encodingWhitelist).apply { - val innocentType = schema.types[0] as CompositeType - (schema.types as MutableList)[0] = innocentType.copy( - name = innocentType.name.replace("Innocent", "VeryEvil") - ) - } - val evilData = SerializedBytes(envelope.write()) - - sandbox { - _contextSerializationEnv.set(createSandboxSerializationEnv(classLoader)) - - val sandboxData = evilData.deserializeFor(classLoader) - - val taskFactory = classLoader.createRawTaskFactory() - val showInnocentData = taskFactory.compose(classLoader.createSandboxFunction()).apply(ShowInnocentData::class.java) - val result = showInnocentData.apply(sandboxData) ?: fail("Result cannot be null") - - // Check that we have deserialised the data without instantiating the Evil class. - assertThat(result.toString()) - .isEqualTo("sandbox.net.corda.serialization.djvm.VeryEvilData: $MESSAGE, $NUMBER") - - // Check that instantiating the Evil class does indeed cause an error. - val ex = assertThrows{ VeryEvilData("Naughty!", 0) } - assertThat(ex.cause) - .isExactlyInstanceOf(IllegalStateException::class.java) - .hasMessageContaining("Victory is mine!") - } - } - - class ShowInnocentData : Function { - override fun apply(data: InnocentData): String { - return "${data::class.java.name}: ${data.message}, ${data.number}" - } - } -} - diff --git a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/SandboxType.kt b/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/SandboxType.kt deleted file mode 100644 index 6b2d5d827f..0000000000 --- a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/SandboxType.kt +++ /dev/null @@ -1,6 +0,0 @@ -package net.corda.serialization.djvm - -enum class SandboxType { - JAVA, - KOTLIN -} diff --git a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/TestBase.kt b/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/TestBase.kt deleted file mode 100644 index 75735552f0..0000000000 --- a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/TestBase.kt +++ /dev/null @@ -1,124 +0,0 @@ -package net.corda.serialization.djvm - -import net.corda.core.serialization.ConstructorForDeserialization -import net.corda.core.serialization.CordaSerializable -import net.corda.core.serialization.CordaSerializationTransformEnumDefault -import net.corda.core.serialization.CordaSerializationTransformEnumDefaults -import net.corda.core.serialization.CordaSerializationTransformRename -import net.corda.core.serialization.CordaSerializationTransformRenames -import net.corda.core.serialization.DeprecatedConstructorForDeserialization -import net.corda.djvm.SandboxConfiguration -import net.corda.djvm.SandboxRuntimeContext -import net.corda.djvm.analysis.AnalysisConfiguration -import net.corda.djvm.messages.Severity -import net.corda.djvm.messages.Severity.WARNING -import net.corda.djvm.source.BootstrapClassLoader -import net.corda.djvm.source.UserPathSource -import org.junit.jupiter.api.AfterAll -import org.junit.jupiter.api.BeforeAll -import org.junit.jupiter.api.Timeout -import org.junit.jupiter.api.fail -import java.io.File -import java.nio.file.Files.exists -import java.nio.file.Files.isDirectory -import java.nio.file.Path -import java.nio.file.Paths -import java.util.concurrent.TimeUnit.MINUTES -import java.util.function.Consumer -import kotlin.concurrent.thread - -@Suppress("unused", "MemberVisibilityCanBePrivate") -@Timeout(5, unit = MINUTES) -abstract class TestBase(type: SandboxType) { - companion object { - const val SANDBOX_STRING = "sandbox.java.lang.String" - - @JvmField - val DETERMINISTIC_RT: Path = Paths.get( - System.getProperty("deterministic-rt.path") ?: fail("deterministic-rt.path property not set")) - - @JvmField - val TESTING_LIBRARIES: List = (System.getProperty("sandbox-libraries.path") - ?: fail("sandbox-libraries.path property not set")) - .split(File.pathSeparator).map { Paths.get(it) }.filter { exists(it) } - - private lateinit var bootstrapClassLoader: BootstrapClassLoader - private lateinit var parentConfiguration: SandboxConfiguration - - @BeforeAll - @JvmStatic - fun setupClassLoader() { - bootstrapClassLoader = BootstrapClassLoader(DETERMINISTIC_RT) - val rootConfiguration = AnalysisConfiguration.createRoot( - userSource = UserPathSource(emptyList()), - visibleAnnotations = setOf( - CordaSerializable::class.java, - CordaSerializationTransformEnumDefault::class.java, - CordaSerializationTransformEnumDefaults::class.java, - CordaSerializationTransformRename::class.java, - CordaSerializationTransformRenames::class.java, - ConstructorForDeserialization::class.java, - DeprecatedConstructorForDeserialization::class.java - ), - bootstrapSource = bootstrapClassLoader - ) - parentConfiguration = SandboxConfiguration.createFor( - analysisConfiguration = rootConfiguration, - profile = null - ) - } - - @AfterAll - @JvmStatic - fun destroyRootContext() { - bootstrapClassLoader.close() - } - } - - val classPaths: List = when(type) { - SandboxType.KOTLIN -> TESTING_LIBRARIES - SandboxType.JAVA -> TESTING_LIBRARIES.filter { isDirectory(it) } - } - - inline fun sandbox(crossinline action: SandboxRuntimeContext.() -> Unit) { - sandbox(Consumer { ctx -> action(ctx) }) - } - - fun sandbox(action: Consumer) { - sandbox(WARNING, emptySet(), action) - } - - inline fun sandbox(visibleAnnotations: Set>, crossinline action: SandboxRuntimeContext.() -> Unit) { - sandbox(visibleAnnotations, Consumer { ctx -> action(ctx) }) - } - - fun sandbox( - visibleAnnotations: Set>, - action: Consumer - ) { - sandbox(WARNING, visibleAnnotations, action) - } - - fun sandbox( - minimumSeverityLevel: Severity, - visibleAnnotations: Set>, - action: Consumer - ) { - var thrownException: Throwable? = null - thread(start = false) { - UserPathSource(classPaths).use { userSource -> - SandboxRuntimeContext(parentConfiguration.createChild(userSource, Consumer { - it.setMinimumSeverityLevel(minimumSeverityLevel) - it.setVisibleAnnotations(visibleAnnotations) - })).use(action) - } - }.apply { - uncaughtExceptionHandler = Thread.UncaughtExceptionHandler { _, ex -> - thrownException = ex - } - start() - join() - } - throw thrownException ?: return - } -} diff --git a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/TestHelpers.kt b/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/TestHelpers.kt deleted file mode 100644 index 0d5a46d179..0000000000 --- a/serialization-djvm/src/test/kotlin/net/corda/serialization/djvm/TestHelpers.kt +++ /dev/null @@ -1,28 +0,0 @@ -@file:JvmName("TestHelpers") -package net.corda.serialization.djvm - -import net.corda.serialization.internal.SectionId -import net.corda.serialization.internal.amqp.Envelope -import net.corda.serialization.internal.amqp.alsoAsByteBuffer -import net.corda.serialization.internal.amqp.amqpMagic -import net.corda.serialization.internal.amqp.withDescribed -import net.corda.serialization.internal.amqp.withList -import org.apache.qpid.proton.codec.Data -import java.io.ByteArrayOutputStream - -fun Envelope.write(): ByteArray { - val data = Data.Factory.create() - data.withDescribed(Envelope.DESCRIPTOR_OBJECT) { - withList { - putObject(obj) - putObject(schema) - putObject(transformsSchema) - } - } - return ByteArrayOutputStream().use { - amqpMagic.writeTo(it) - SectionId.DATA_AND_STOP.writeTo(it) - it.alsoAsByteBuffer(data.encodedSize().toInt(), data::encode) - it.toByteArray() - } -} diff --git a/serialization-djvm/src/test/resources/log4j2-test.xml b/serialization-djvm/src/test/resources/log4j2-test.xml deleted file mode 100644 index cf527ff319..0000000000 --- a/serialization-djvm/src/test/resources/log4j2-test.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/serialization-djvm/src/test/resources/testing.cert b/serialization-djvm/src/test/resources/testing.cert deleted file mode 100644 index 03e1558cca71d9fe0f42f4a11d7d3cb1e8819cd8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 801 zcmXqLVwN>%Vq#su%*4pV#G)>I_M8DP8>d#AN85K^Mn-N{1_OITZUas>=1>+kVWv=T zLw=wh-iR;`|A7Okpt!?a9szwH60GhChPCi~uf^2S8=)n@;7YDZZm z{c?HIi*mL;3p&xdb%n7&yWGT-uE3kiHhHnx$C$mkYB5J=xANThDRbY3Y@ONObK;-d zf!W>jfrP9~=8sSP>s9$LH1{%BR7na$#{4b*2X7op zy;0KdEmTsR<5s2EUASxWv51K9@0ynVaP#Y&tEQrsnq5(M+Y{y?2O==$fq}@#Fsu3Rv3X(Y z=N)U>Z?=|gTKi@}*0bsMFTCQCG9FmeRLNbF$^E16@U?rY?v?JfK92lek#pY_RvNkg zo_g|%rTNY8evgc2W^OK3e(w@?t1Pm0=#fz14mkCUrW8!_Qt2W5@^I?CdwYGJV zZg$&}_m-cKK7F%+Vee^&-)<8-ME9_Bzwg*}`2bf(v!j@#USn{`pOtR|*0edEdhSxa zy7X3sd)}K-q$d^;=-W+n_{!*Ds G*+&5lOg@qT diff --git a/serialization-djvm/src/test/scripts/generate-certificate.sh b/serialization-djvm/src/test/scripts/generate-certificate.sh deleted file mode 100755 index 4863542cd5..0000000000 --- a/serialization-djvm/src/test/scripts/generate-certificate.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/sh - -KEYPASS=deterministic -STOREPASS=deterministic - -rm -f keystore testing.cert - -keytool -keystore keystore -storetype pkcs12 -genkey -dname 'CN=localhost, O=R3, L=London, C=UK' -keyalg RSA -validity 3650 -keypass ${KEYPASS} -storepass ${STOREPASS} -keytool -keystore keystore -storetype pkcs12 -export -keyalg RSA -file testing.cert -keypass ${KEYPASS} -storepass ${STOREPASS} - -rm -f keystore diff --git a/serialization/build.gradle b/serialization/build.gradle index 224bd642b4..a65ff2da15 100644 --- a/serialization/build.gradle +++ b/serialization/build.gradle @@ -6,7 +6,6 @@ apply plugin: 'com.jfrog.artifactory' description 'Corda serialization' -// required by DJVM and Avian JVM (for running inside the SGX enclave) which only supports Java 8. targetCompatibility = VERSION_1_8 dependencies { diff --git a/serialization/src/main/java/net/corda/serialization/internal/amqp/custom/CacheKey.java b/serialization/src/main/java/net/corda/serialization/internal/amqp/custom/CacheKey.java index 2a341d5130..cf5e7ffa05 100644 --- a/serialization/src/main/java/net/corda/serialization/internal/amqp/custom/CacheKey.java +++ b/serialization/src/main/java/net/corda/serialization/internal/amqp/custom/CacheKey.java @@ -1,6 +1,5 @@ package net.corda.serialization.internal.amqp.custom; -import net.corda.core.KeepForDJVM; import org.jetbrains.annotations.NotNull; import java.util.Arrays; @@ -9,7 +8,6 @@ import java.util.Arrays; * This class is deliberately written in Java so * that it can be package private. */ -@KeepForDJVM final class CacheKey { private final byte[] bytes; private final int hashValue; diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/AllButBlacklisted.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/AllButBlacklisted.kt index ac13d19e3b..e0579b04ca 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/AllButBlacklisted.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/AllButBlacklisted.kt @@ -1,6 +1,5 @@ package net.corda.serialization.internal -import net.corda.core.DeleteForDJVM import net.corda.core.serialization.ClassWhitelist import java.io.* import java.lang.invoke.* @@ -33,7 +32,6 @@ import kotlin.collections.LinkedHashSet * in the blacklist - it will still be serialized as specified by custom serializer. * For more details, see [net.corda.serialization.internal.CordaClassResolver.getRegistration] */ -@DeleteForDJVM object AllButBlacklisted : ClassWhitelist { private val blacklistedClasses = hashSetOf( diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/ByteBufferStreams.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/ByteBufferStreams.kt index 144a0bb047..8068cdf1a3 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/ByteBufferStreams.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/ByteBufferStreams.kt @@ -1,9 +1,6 @@ @file:JvmName("ByteBufferStreams") -@file:DeleteForDJVM package net.corda.serialization.internal -import net.corda.core.DeleteForDJVM -import net.corda.core.KeepForDJVM import net.corda.core.internal.LazyPool import java.io.ByteArrayOutputStream import java.io.IOException @@ -24,7 +21,6 @@ fun byteArrayOutput(task: (ByteBufferOutputStream) -> T): ByteArray { } } -@KeepForDJVM class ByteBufferInputStream(val byteBuffer: ByteBuffer) : InputStream() { @Throws(IOException::class) override fun read(): Int { @@ -46,7 +42,6 @@ class ByteBufferInputStream(val byteBuffer: ByteBuffer) : InputStream() { } } -@KeepForDJVM class ByteBufferOutputStream(size: Int) : ByteArrayOutputStream(size) { companion object { private val ensureCapacity = ByteArrayOutputStream::class.java.getDeclaredMethod("ensureCapacity", Int::class.java).apply { diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/CheckpointSerializationScheme.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/CheckpointSerializationScheme.kt index f037e2dfbb..73890ce8d4 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/CheckpointSerializationScheme.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/CheckpointSerializationScheme.kt @@ -1,13 +1,11 @@ package net.corda.serialization.internal -import net.corda.core.KeepForDJVM import net.corda.core.serialization.CheckpointCustomSerializer import net.corda.core.serialization.ClassWhitelist import net.corda.core.serialization.EncodingWhitelist import net.corda.core.serialization.SerializationEncoding import net.corda.core.serialization.internal.CheckpointSerializationContext -@KeepForDJVM data class CheckpointSerializationContextImpl @JvmOverloads constructor( override val deserializationClassLoader: ClassLoader, override val whitelist: ClassWhitelist, diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/ClassWhitelists.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/ClassWhitelists.kt index a99eba253e..5d647d4f17 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/ClassWhitelists.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/ClassWhitelists.kt @@ -1,20 +1,16 @@ package net.corda.serialization.internal -import net.corda.core.KeepForDJVM import net.corda.core.serialization.ClassWhitelist import java.util.* -@KeepForDJVM interface MutableClassWhitelist : ClassWhitelist { fun add(entry: Class<*>) } -@KeepForDJVM object AllWhitelist : ClassWhitelist { override fun hasListed(type: Class<*>): Boolean = true } -@KeepForDJVM class BuiltInExceptionsWhitelist : ClassWhitelist { companion object { private val packageName = "^(?:java|kotlin)(?:[.]|$)".toRegex() @@ -44,11 +40,9 @@ sealed class AbstractMutableClassWhitelist(private val whitelist: MutableSet = Collections.synchronizedSet(mutableSetOf()) diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/ClientContexts.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/ClientContexts.kt index 8f8e9d4cb6..17e95d128f 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/ClientContexts.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/ClientContexts.kt @@ -1,8 +1,6 @@ -@file:DeleteForDJVM @file:JvmName("ClientContexts") package net.corda.serialization.internal -import net.corda.core.DeleteForDJVM import net.corda.core.serialization.SerializationContext import net.corda.core.serialization.SerializationDefaults import net.corda.serialization.internal.amqp.amqpMagic diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/GeneratedAttachment.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/GeneratedAttachment.kt index 1c0684b49d..a9a89dc5a7 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/GeneratedAttachment.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/GeneratedAttachment.kt @@ -1,10 +1,8 @@ package net.corda.serialization.internal -import net.corda.core.KeepForDJVM import net.corda.core.crypto.sha256 import net.corda.core.internal.AbstractAttachment -@KeepForDJVM class GeneratedAttachment(val bytes: ByteArray, uploader: String?) : AbstractAttachment({ bytes }, uploader) { override val id = bytes.sha256() } diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/OrdinalIO.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/OrdinalIO.kt index e094d6b98d..8b297b4f4f 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/OrdinalIO.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/OrdinalIO.kt @@ -1,14 +1,11 @@ package net.corda.serialization.internal -import net.corda.core.KeepForDJVM import java.io.EOFException import java.io.InputStream import java.io.OutputStream import java.nio.ByteBuffer -@KeepForDJVM class OrdinalBits(private val ordinal: Int) { - @KeepForDJVM interface OrdinalWriter { val bits: OrdinalBits @JvmDefault val encodedSize: Int get() = 1 @@ -22,7 +19,6 @@ class OrdinalBits(private val ordinal: Int) { } } -@KeepForDJVM class OrdinalReader(private val values: Array) { private val enumName = values[0].javaClass.simpleName private val range = 0 until values.size diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/SerializationFormat.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/SerializationFormat.kt index 7eb236f23d..90f60d099e 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/SerializationFormat.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/SerializationFormat.kt @@ -1,6 +1,5 @@ package net.corda.serialization.internal -import net.corda.core.KeepForDJVM import net.corda.core.serialization.SerializationEncoding import net.corda.core.utilities.ByteSequence import net.corda.core.utilities.OpaqueBytes @@ -14,7 +13,6 @@ import java.nio.ByteBuffer import java.util.zip.DeflaterOutputStream import java.util.zip.InflaterInputStream -@KeepForDJVM class CordaSerializationMagic(bytes: ByteArray) : OpaqueBytes(bytes) { private val bufferView = slice() fun consume(data: ByteSequence): ByteBuffer? { @@ -22,7 +20,6 @@ class CordaSerializationMagic(bytes: ByteArray) : OpaqueBytes(bytes) { } } -@KeepForDJVM enum class SectionId : OrdinalWriter { /** Serialization data follows, and then discard the rest of the stream (if any) as legacy data may have trailing garbage. */ DATA_AND_STOP, @@ -38,7 +35,6 @@ enum class SectionId : OrdinalWriter { override val bits = OrdinalBits(ordinal) } -@KeepForDJVM enum class CordaSerializationEncoding : SerializationEncoding, OrdinalWriter { DEFLATE { override fun wrap(stream: OutputStream) = DeflaterOutputStream(stream) diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/SerializationScheme.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/SerializationScheme.kt index 6b63a46655..2ec5e8d9b8 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/SerializationScheme.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/SerializationScheme.kt @@ -1,7 +1,5 @@ package net.corda.serialization.internal -import net.corda.core.DeleteForDJVM -import net.corda.core.KeepForDJVM import net.corda.core.crypto.SecureHash import net.corda.core.internal.VisibleForTesting import net.corda.core.internal.copyBytes @@ -24,7 +22,6 @@ internal object SnappyEncodingWhitelist: EncodingWhitelist { } } -@KeepForDJVM data class SerializationContextImpl @JvmOverloads constructor(override val preferredSerializationVersion: SerializationMagic, override val deserializationClassLoader: ClassLoader, override val whitelist: ClassWhitelist, @@ -82,12 +79,10 @@ data class SerializationContextImpl @JvmOverloads constructor(override val prefe override fun withEncodingWhitelist(encodingWhitelist: EncodingWhitelist) = copy(encodingWhitelist = encodingWhitelist) } -@KeepForDJVM open class SerializationFactoryImpl( // TODO: This is read-mostly. Probably a faster implementation to be found. private val schemes: MutableMap, SerializationScheme> ) : SerializationFactory() { - @DeleteForDJVM constructor() : this(ConcurrentHashMap()) companion object { @@ -155,7 +150,6 @@ open class SerializationFactoryImpl( override fun hashCode(): Int = registeredSchemes.hashCode() } -@KeepForDJVM interface SerializationScheme { fun canDeserializeVersion(magic: CordaSerializationMagic, target: SerializationContext.UseCase): Boolean @Throws(NotSerializableException::class) diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/SerializeAsTokenContextImpl.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/SerializeAsTokenContextImpl.kt index 025e27a38a..50dc886bd3 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/SerializeAsTokenContextImpl.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/SerializeAsTokenContextImpl.kt @@ -1,7 +1,6 @@ -@file:DeleteForDJVM + package net.corda.serialization.internal -import net.corda.core.DeleteForDJVM import net.corda.core.node.ServiceHub import net.corda.core.serialization.* import net.corda.core.serialization.internal.CheckpointSerializationContext @@ -21,7 +20,6 @@ fun CheckpointSerializationContext.withTokenContext(serializationContext: Serial * Then it is a case of using the companion object methods on [SerializeAsTokenSerializer] to set and clear context as necessary * when serializing to enable/disable tokenization. */ -@DeleteForDJVM class SerializeAsTokenContextImpl(override val serviceHub: ServiceHub, init: SerializeAsTokenContext.() -> Unit) : SerializeAsTokenContext { constructor(toBeTokenized: Any, serializationFactory: SerializationFactory, context: SerializationContext, serviceHub: ServiceHub) : this(serviceHub, { serializationFactory.serialize(toBeTokenized, context.withTokenContext(this)) @@ -68,7 +66,6 @@ class SerializeAsTokenContextImpl(override val serviceHub: ServiceHub, init: Ser * Then it is a case of using the companion object methods on [SerializeAsTokenSerializer] to set and clear context as necessary * when serializing to enable/disable tokenization. */ -@DeleteForDJVM class CheckpointSerializeAsTokenContextImpl(override val serviceHub: ServiceHub, init: SerializeAsTokenContext.() -> Unit) : SerializeAsTokenContext { constructor(toBeTokenized: Any, serializer: CheckpointSerializer, context: CheckpointSerializationContext, serviceHub: ServiceHub) : this(serviceHub, { serializer.serialize(toBeTokenized, context.withTokenContext(this)) diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/ServerContexts.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/ServerContexts.kt index 160c12298b..619324b24e 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/ServerContexts.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/ServerContexts.kt @@ -1,8 +1,6 @@ @file:JvmName("ServerContexts") -@file:DeleteForDJVM package net.corda.serialization.internal -import net.corda.core.DeleteForDJVM import net.corda.core.serialization.SerializationContext import net.corda.core.serialization.SerializationDefaults import net.corda.serialization.internal.amqp.amqpMagic diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/SharedContexts.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/SharedContexts.kt index c2d12e8b55..557e10850e 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/SharedContexts.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/SharedContexts.kt @@ -1,9 +1,6 @@ @file:JvmName("SharedContexts") -@file:DeleteForDJVM package net.corda.serialization.internal -import net.corda.core.DeleteForDJVM -import net.corda.core.KeepForDJVM import net.corda.core.serialization.* import net.corda.serialization.internal.amqp.amqpMagic @@ -17,7 +14,6 @@ val AMQP_P2P_CONTEXT = SerializationContextImpl( null ) -@KeepForDJVM object AlwaysAcceptEncodingWhitelist : EncodingWhitelist { override fun acceptEncoding(encoding: SerializationEncoding) = true } diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/UseCaseAwareness.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/UseCaseAwareness.kt index 2ce03e1e3b..c372f9d293 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/UseCaseAwareness.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/UseCaseAwareness.kt @@ -1,7 +1,5 @@ -@file:KeepForDJVM package net.corda.serialization.internal -import net.corda.core.KeepForDJVM import net.corda.core.serialization.SerializationContext import net.corda.core.serialization.SerializationFactory import java.util.* diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPRemoteTypeModel.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPRemoteTypeModel.kt index ab4edf859f..50022575cb 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPRemoteTypeModel.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPRemoteTypeModel.kt @@ -3,6 +3,7 @@ package net.corda.serialization.internal.amqp import net.corda.serialization.internal.NotSerializableDetailedException import net.corda.serialization.internal.model.* import java.io.NotSerializableException +import java.util.concurrent.ConcurrentHashMap import kotlin.collections.LinkedHashMap /** @@ -10,7 +11,7 @@ import kotlin.collections.LinkedHashMap */ class AMQPRemoteTypeModel { - private val cache: MutableMap = DefaultCacheProvider.createCache() + private val cache: MutableMap = ConcurrentHashMap() /** * Interpret a [Schema] to obtain a [Map] of all of the [RemoteTypeInformation] contained therein, indexed by diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPSerializationScheme.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPSerializationScheme.kt index 1e8cb09080..4b698610c7 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPSerializationScheme.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPSerializationScheme.kt @@ -1,10 +1,6 @@ @file:JvmName("AMQPSerializationScheme") package net.corda.serialization.internal.amqp - -import net.corda.core.DeleteForDJVM -import net.corda.core.KeepForDJVM -import net.corda.core.StubOutForDJVM import net.corda.core.cordapp.Cordapp import net.corda.core.internal.VisibleForTesting import net.corda.core.internal.toSynchronised @@ -16,6 +12,7 @@ import net.corda.serialization.internal.DefaultWhitelist import net.corda.serialization.internal.MutableClassWhitelist import net.corda.serialization.internal.SerializationScheme import java.util.* +import java.util.concurrent.ConcurrentMap val AMQP_ENABLED get() = SerializationDefaults.P2P_CONTEXT.preferredSerializationVersion == amqpMagic @@ -40,14 +37,12 @@ interface SerializerFactoryFactory { fun make(context: SerializationContext): SerializerFactory } -@KeepForDJVM abstract class AbstractAMQPSerializationScheme( private val cordappCustomSerializers: Set>, private val cordappSerializationWhitelists: Set, maybeNotConcurrentSerializerFactoriesForContexts: MutableMap, val sff: SerializerFactoryFactory = createSerializerFactoryFactory() ) : SerializationScheme { - @DeleteForDJVM constructor(cordapps: List) : this( cordapps.customSerializers, cordapps.serializationWhitelists, @@ -57,23 +52,19 @@ abstract class AbstractAMQPSerializationScheme( @VisibleForTesting fun getRegisteredCustomSerializers() = cordappCustomSerializers - // This is a bit gross but a broader check for ConcurrentMap is not allowed inside DJVM. private val serializerFactoriesForContexts: MutableMap = - if (maybeNotConcurrentSerializerFactoriesForContexts is - AccessOrderLinkedHashMap) { - Collections.synchronizedMap(maybeNotConcurrentSerializerFactoriesForContexts) - } else { + if (maybeNotConcurrentSerializerFactoriesForContexts is ConcurrentMap<*, *>) { maybeNotConcurrentSerializerFactoriesForContexts + } else { + Collections.synchronizedMap(maybeNotConcurrentSerializerFactoriesForContexts) } companion object { private val serializationWhitelists: List by lazy { listOf(DefaultWhitelist) } - @DeleteForDJVM val List.customSerializers get() = flatMapTo(LinkedHashSet(), Cordapp::serializationCustomSerializers) - @DeleteForDJVM val List.serializationWhitelists get() = flatMapTo(LinkedHashSet(), Cordapp::serializationWhitelists) } @@ -125,6 +116,7 @@ abstract class AbstractAMQPSerializationScheme( fun getSerializerFactory(context: SerializationContext): SerializerFactory { val key = SerializationFactoryCacheKey(context.whitelist, context.deserializationClassLoader, context.preventDataLoss, context.customSerializers) // ConcurrentHashMap.get() is lock free, but computeIfAbsent is not, even if the key is in the map already. + // This was fixed in Java 9, so remove the extra get() when we upgrade (https://bugs.openjdk.org/browse/JDK-8161372). return serializerFactoriesForContexts[key] ?: serializerFactoriesForContexts.computeIfAbsent(key) { when (context.useCase) { SerializationContext.UseCase.RPCClient -> @@ -194,18 +186,7 @@ fun registerCustomSerializers(factory: SerializerFactory) { register(net.corda.serialization.internal.amqp.custom.BitSetSerializer(this)) register(net.corda.serialization.internal.amqp.custom.EnumSetSerializer(this)) register(net.corda.serialization.internal.amqp.custom.ContractAttachmentSerializer(this)) - } - registerNonDeterministicSerializers(factory) -} - -/* - * Register the serializers which will be excluded from the DJVM. - */ -@StubOutForDJVM -private fun registerNonDeterministicSerializers(factory: SerializerFactory) { - with(factory) { register(net.corda.serialization.internal.amqp.custom.PrivateKeySerializer) register(net.corda.serialization.internal.amqp.custom.SimpleStringSerializer) } } - diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPSerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPSerializer.kt index b4b0b2e58e..283b997a84 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPSerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPSerializer.kt @@ -1,6 +1,5 @@ package net.corda.serialization.internal.amqp -import net.corda.core.KeepForDJVM import net.corda.core.serialization.SerializationContext import org.apache.qpid.proton.amqp.Symbol import org.apache.qpid.proton.codec.Data @@ -9,7 +8,6 @@ import java.lang.reflect.Type /** * Implemented to serialize and deserialize different types of objects to/from AMQP. */ -@KeepForDJVM interface AMQPSerializer { /** * The JVM type this can serialize and deserialize. diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPStreams.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPStreams.kt index c249a98aed..8ec8ddf598 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPStreams.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPStreams.kt @@ -1,8 +1,6 @@ @file:JvmName("AMQPStreams") -@file:DeleteForDJVM package net.corda.serialization.internal.amqp -import net.corda.core.DeleteForDJVM import net.corda.serialization.internal.ByteBufferInputStream import net.corda.serialization.internal.ByteBufferOutputStream import net.corda.serialization.internal.serializeOutputStreamPool diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/AccessOrderLinkedHashMap.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/AccessOrderLinkedHashMap.kt index d7e50afa4d..6ebea86dfc 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/AccessOrderLinkedHashMap.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/AccessOrderLinkedHashMap.kt @@ -1,7 +1,5 @@ package net.corda.serialization.internal.amqp -import net.corda.core.KeepForDJVM -@KeepForDJVM class AccessOrderLinkedHashMap(private val maxSize: Int) : LinkedHashMap(16, 0.75f, true) { constructor(loader: () -> Int) : this(loader.invoke()) diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/ArraySerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/ArraySerializer.kt index b6a1b8294e..be13d023fa 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/ArraySerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/ArraySerializer.kt @@ -1,6 +1,5 @@ package net.corda.serialization.internal.amqp -import net.corda.core.KeepForDJVM import net.corda.core.serialization.SerializationContext import net.corda.core.utilities.contextLogger import net.corda.core.utilities.debug @@ -14,7 +13,6 @@ import java.lang.reflect.Type /** * Serialization / deserialization of arrays. */ -@KeepForDJVM open class ArraySerializer(override val type: Type, factory: LocalSerializerFactory) : AMQPSerializer { companion object { fun make(type: Type, factory: LocalSerializerFactory) : AMQPSerializer { diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/CollectionSerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/CollectionSerializer.kt index 29953e840a..76bad27da4 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/CollectionSerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/CollectionSerializer.kt @@ -1,6 +1,5 @@ package net.corda.serialization.internal.amqp -import net.corda.core.KeepForDJVM import net.corda.core.serialization.SerializationContext import net.corda.core.utilities.NonEmptySet import net.corda.serialization.internal.model.LocalTypeInformation @@ -16,7 +15,6 @@ import kotlin.collections.LinkedHashSet /** * Serialization / deserialization of predefined set of supported [Collection] types covering mostly [List]s and [Set]s. */ -@KeepForDJVM class CollectionSerializer(private val declaredType: ParameterizedType, factory: LocalSerializerFactory) : AMQPSerializer { override val type: Type = declaredType diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/CustomSerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/CustomSerializer.kt index ee28ca00de..8563e3de56 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/CustomSerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/CustomSerializer.kt @@ -1,9 +1,7 @@ package net.corda.serialization.internal.amqp -import net.corda.core.KeepForDJVM import net.corda.core.serialization.SerializationContext import net.corda.serialization.internal.model.FingerprintWriter -import net.corda.serialization.internal.model.TypeIdentifier import org.apache.qpid.proton.amqp.Symbol import org.apache.qpid.proton.codec.Data import java.lang.reflect.Type @@ -29,12 +27,6 @@ abstract class CustomSerializer : AMQPSerializer, SerializerFor { */ open val additionalSerializers: Iterable> = emptyList() - /** - * This custom serializer is also allowed to deserialize these classes. This allows us - * to deserialize objects into completely different types, e.g. `A` -> `sandbox.A`. - */ - open val deserializationAliases: Set = emptySet() - protected abstract val descriptor: Descriptor /** * This exists purely for documentation and cross-platform purposes. It is not used by our serialization / deserialization @@ -73,7 +65,6 @@ abstract class CustomSerializer : AMQPSerializer, SerializerFor { * subclass in the schema, so that we can distinguish between subclasses. */ // TODO: should this be a custom serializer at all, or should it just be a plain AMQPSerializer? - @KeepForDJVM class SubClass(private val clazz: Class<*>, private val superClassSerializer: CustomSerializer) : CustomSerializer() { // TODO: should this be empty or contain the schema of the super? override val schemaForDocumentation = Schema(emptyList()) @@ -133,13 +124,11 @@ abstract class CustomSerializer : AMQPSerializer, SerializerFor { /** * Additional base features for a custom serializer for a particular class, that excludes subclasses. */ - @KeepForDJVM abstract class Is(clazz: Class) : CustomSerializerImp(clazz, false) /** * Additional base features for a custom serializer for all implementations of a particular interface or super class. */ - @KeepForDJVM abstract class Implements(clazz: Class) : CustomSerializerImp(clazz, true) /** @@ -149,7 +138,6 @@ abstract class CustomSerializer : AMQPSerializer, SerializerFor { * The proxy class must use only types which are either native AMQP or other types for which there are pre-registered * custom serializers. */ - @KeepForDJVM abstract class Proxy(clazz: Class, protected val proxyClass: Class

    , protected val factory: LocalSerializerFactory, @@ -212,7 +200,6 @@ abstract class CustomSerializer : AMQPSerializer, SerializerFor { * @param maker A lambda for constructing an instance, that defaults to calling a constructor that expects a string. * @param unmaker A lambda that extracts the string value for an instance, that defaults to the [toString] method. */ - @KeepForDJVM abstract class ToString(clazz: Class, withInheritance: Boolean = false, private val maker: (String) -> T = clazz.getConstructor(String::class.java).let { `constructor` -> { string -> `constructor`.newInstance(string) } diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/CustomSerializerRegistry.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/CustomSerializerRegistry.kt index 76a99a2bf8..b482b6dce7 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/CustomSerializerRegistry.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/CustomSerializerRegistry.kt @@ -6,9 +6,9 @@ import net.corda.core.serialization.CordaSerializable import net.corda.core.utilities.contextLogger import net.corda.core.utilities.debug import net.corda.core.utilities.trace -import net.corda.serialization.internal.model.DefaultCacheProvider import net.corda.serialization.internal.model.TypeIdentifier import java.lang.reflect.Type +import java.util.concurrent.ConcurrentHashMap /** * Thrown when a [CustomSerializer] offers to serialize a type for which custom serialization is not permitted, because @@ -87,7 +87,7 @@ class CachingCustomSerializerRegistry( data class CustomSerializerFound(override val serializerIfFound: AMQPSerializer) : CustomSerializerLookupResult() } - private val customSerializersCache: MutableMap = DefaultCacheProvider.createCache() + private val customSerializersCache: MutableMap = ConcurrentHashMap() private val customSerializers: MutableList = mutableListOf() /** @@ -108,16 +108,8 @@ class CachingCustomSerializerRegistry( register(additional) } - for (alias in customSerializer.deserializationAliases) { - val aliasDescriptor = typeDescriptorFor(alias) - if (aliasDescriptor != customSerializer.typeDescriptor) { - descriptorBasedSerializerRegistry[aliasDescriptor.toString()] = customSerializer - } - } - customSerializer } - } override fun registerExternal(customSerializer: CorDappCustomSerializer) { diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/DescriptorBasedSerializerRegistry.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/DescriptorBasedSerializerRegistry.kt index 8adc48fbed..7ff51e2f83 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/DescriptorBasedSerializerRegistry.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/DescriptorBasedSerializerRegistry.kt @@ -1,6 +1,6 @@ package net.corda.serialization.internal.amqp -import net.corda.serialization.internal.model.DefaultCacheProvider +import java.util.concurrent.ConcurrentHashMap /** * The quickest way to find a serializer, if one has already been generated, is to look it up by type descriptor. @@ -14,9 +14,9 @@ interface DescriptorBasedSerializerRegistry { fun getOrBuild(descriptor: String, builder: () -> AMQPSerializer): AMQPSerializer } -class DefaultDescriptorBasedSerializerRegistry: DescriptorBasedSerializerRegistry { +class DefaultDescriptorBasedSerializerRegistry : DescriptorBasedSerializerRegistry { - private val registry: MutableMap> = DefaultCacheProvider.createCache() + private val registry: MutableMap> = ConcurrentHashMap() override fun get(descriptor: String): AMQPSerializer? = registry[descriptor] diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/DeserializationInput.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/DeserializationInput.kt index 396befd4ae..6ee023d1a1 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/DeserializationInput.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/DeserializationInput.kt @@ -1,6 +1,5 @@ package net.corda.serialization.internal.amqp -import net.corda.core.KeepForDJVM import net.corda.core.internal.LazyPool import net.corda.core.internal.VisibleForTesting import net.corda.core.serialization.AMQP_ENVELOPE_CACHE_PROPERTY @@ -37,7 +36,6 @@ data class ObjectAndEnvelope(val obj: T, val envelope: Envelope) * @param serializerFactory This is the factory for [AMQPSerializer] instances and can be shared across multiple * instances and threads. */ -@KeepForDJVM class DeserializationInput constructor( private val serializerFactory: SerializerFactory ) { diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/Envelope.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/Envelope.kt index 272c719a91..0c7afbec92 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/Envelope.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/Envelope.kt @@ -1,6 +1,5 @@ package net.corda.serialization.internal.amqp -import net.corda.core.KeepForDJVM import org.apache.qpid.proton.ProtonException import org.apache.qpid.proton.amqp.DescribedType import org.apache.qpid.proton.codec.Data @@ -15,7 +14,6 @@ import java.nio.ByteBuffer * internal utilities to decompose and recompose with/without schema etc so that e.g. we can store objects with a * (relationally) normalised out schema to avoid excessive duplication. */ -@KeepForDJVM class Envelope(val obj: Any?, resolveSchema: () -> Pair) : DescribedType { val resolvedSchema: Pair by lazy(resolveSchema) diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/EvolutionSerializerFactory.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/EvolutionSerializerFactory.kt index b16a6d2213..13c298e1ac 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/EvolutionSerializerFactory.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/EvolutionSerializerFactory.kt @@ -20,8 +20,6 @@ interface EvolutionSerializerFactory { /** * A mapping between Java object types and their equivalent Java primitive types. - * Predominantly for the sake of the DJVM sandbox where e.g. `char` will map to - * sandbox.java.lang.Character instead of java.lang.Character. */ val primitiveTypes: Map, Class<*>> } diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/LocalSerializerFactory.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/LocalSerializerFactory.kt index 9b0ce7b9ae..48e82e1a2f 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/LocalSerializerFactory.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/LocalSerializerFactory.kt @@ -11,6 +11,7 @@ import org.apache.qpid.proton.amqp.Symbol import java.lang.reflect.ParameterizedType import java.lang.reflect.Type import java.util.* +import java.util.concurrent.ConcurrentHashMap import java.util.function.Function import java.util.function.Predicate import javax.annotation.concurrent.ThreadSafe @@ -111,9 +112,9 @@ class DefaultLocalSerializerFactory( private data class ActualAndDeclaredType(val actualType: Class<*>, val declaredType: Type) - private val serializersByActualAndDeclaredType: MutableMap> = DefaultCacheProvider.createCache() - private val serializersByTypeId: MutableMap> = DefaultCacheProvider.createCache() - private val typesByName = DefaultCacheProvider.createCache>() + private val serializersByActualAndDeclaredType: MutableMap> = ConcurrentHashMap() + private val serializersByTypeId: MutableMap> = ConcurrentHashMap() + private val typesByName = ConcurrentHashMap>() override fun createDescriptor(typeInformation: LocalTypeInformation): Symbol = Symbol.valueOf("$DESCRIPTOR_DOMAIN:${fingerPrinter.fingerprint(typeInformation)}") diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/MapSerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/MapSerializer.kt index 2e00e8d206..6b1d7e7b12 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/MapSerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/MapSerializer.kt @@ -1,7 +1,5 @@ package net.corda.serialization.internal.amqp -import net.corda.core.KeepForDJVM -import net.corda.core.StubOutForDJVM import net.corda.core.internal.uncheckedCast import net.corda.core.serialization.SerializationContext import net.corda.serialization.internal.model.LocalTypeInformation @@ -19,7 +17,6 @@ private typealias MapCreationFunction = (Map<*, *>) -> Map<*, *> /** * Serialization / deserialization of certain supported [Map] types. */ -@KeepForDJVM class MapSerializer(private val declaredType: ParameterizedType, factory: LocalSerializerFactory) : AMQPSerializer { override val type: Type = declaredType @@ -150,11 +147,6 @@ private fun Class<*>.checkHashMap() { } } -/** - * The [WeakHashMap] class does not exist within the DJVM, and so we need - * to isolate this reference. - */ -@StubOutForDJVM private fun Class<*>.checkWeakHashMap() { if (WeakHashMap::class.java.isAssignableFrom(this)) { throw IllegalArgumentException("Weak references with map types not supported. Suggested fix: " diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/PropertyDescriptor.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/PropertyDescriptor.kt index 94dcf72281..39c8103fb8 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/PropertyDescriptor.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/PropertyDescriptor.kt @@ -1,7 +1,6 @@ package net.corda.serialization.internal.amqp import com.google.common.reflect.TypeToken -import net.corda.core.KeepForDJVM import net.corda.core.internal.isPublic import net.corda.core.serialization.SerializableCalculatedProperty import net.corda.serialization.internal.amqp.MethodClassifier.* @@ -19,7 +18,6 @@ import java.util.* * @property getter the method of a class that returns a fields value. Determined by * locating a function named getXyz for the property named in field as xyz. */ -@KeepForDJVM data class PropertyDescriptor(val field: Field?, val setter: Method?, val getter: Method?) { override fun toString() = StringBuilder("").apply { appendln("Property - ${field?.name ?: "null field"}\n") diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/RemoteSerializerFactory.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/RemoteSerializerFactory.kt index 355827f1d9..3937e302e5 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/RemoteSerializerFactory.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/RemoteSerializerFactory.kt @@ -6,7 +6,6 @@ import net.corda.core.utilities.contextLogger import net.corda.core.utilities.trace import net.corda.serialization.internal.model.* import java.io.NotSerializableException -import java.util.Collections.singletonList /** * A factory that knows how to create serializers to deserialize values sent to us by remote parties. @@ -77,7 +76,7 @@ class DefaultRemoteSerializerFactory( // This will save us having to re-interpret the entire schema on re-entry when deserialising individual property values. val serializers = reflected.mapValues { (descriptor, remoteLocalPair) -> descriptorBasedSerializerRegistry.getOrBuild(descriptor) { - getUncached(remoteLocalPair.remoteTypeInformation, remoteLocalPair.localTypeInformation, context) + getUncached(remoteLocalPair.remoteTypeInformation, remoteLocalPair.localTypeInformation) } } @@ -90,8 +89,7 @@ class DefaultRemoteSerializerFactory( private fun getUncached( remoteTypeInformation: RemoteTypeInformation, - localTypeInformation: LocalTypeInformation, - context: SerializationContext + localTypeInformation: LocalTypeInformation ): AMQPSerializer { val remoteDescriptor = remoteTypeInformation.typeDescriptor @@ -112,13 +110,6 @@ class DefaultRemoteSerializerFactory( evolutionSerializerFactory.getEvolutionSerializer(remoteTypeInformation, localTypeInformation) ?: localSerializer - // The type descriptors are never going to match when we deserialise into - // the DJVM's sandbox, but we don't want the node logs to fill up with - // Big 'n Scary warnings either. Assume that the local serializer is fine - // provided the local type is the same one we expect when loading the - // remote class. - remoteTypeInformation.isCompatibleWith(localTypeInformation, context) -> localSerializer - // Descriptors don't match, and something is probably broken, but we let the framework do what it can with the local // serialiser (BlobInspectorTest uniquely breaks if we throw an exception here, and passes if we just warn and continue). else -> { @@ -158,13 +149,4 @@ ${localTypeInformation.prettyPrint(false)} this is RemoteTypeInformation.Parameterised && (localTypeInformation is LocalTypeInformation.ACollection || localTypeInformation is LocalTypeInformation.AMap) - - private fun RemoteTypeInformation.isCompatibleWith( - localTypeInformation: LocalTypeInformation, - context: SerializationContext - ): Boolean { - val localTypes = typeLoader.load(singletonList(this), context) - return localTypes.size == 1 - && localTypeInformation.observedType == localTypes.values.first() - } -} \ No newline at end of file +} diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/Schema.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/Schema.kt index 13e0f1bc1a..46d85fbd1a 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/Schema.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/Schema.kt @@ -1,6 +1,5 @@ package net.corda.serialization.internal.amqp -import net.corda.core.KeepForDJVM import net.corda.core.internal.uncheckedCast import net.corda.serialization.internal.CordaSerializationMagic import net.corda.serialization.internal.amqp.AMQPTypeIdentifiers.isPrimitive @@ -51,7 +50,6 @@ private class RedescribedType( * This and the classes below are OO representations of the AMQP XML schema described in the specification. Their * [toString] representations generate the associated XML form. */ -@KeepForDJVM data class Schema(val types: List) : DescribedType { companion object : DescribedTypeConstructor { val DESCRIPTOR = AMQPDescriptorRegistry.SCHEMA.amqpDescriptor @@ -80,7 +78,6 @@ data class Schema(val types: List) : DescribedType { override fun toString(): String = types.joinToString("\n") } -@KeepForDJVM data class Descriptor(val name: Symbol?, val code: UnsignedLong? = null) : DescribedType { constructor(name: String?) : this(Symbol.valueOf(name)) @@ -121,7 +118,6 @@ data class Descriptor(val name: Symbol?, val code: UnsignedLong? = null) : Descr } } -@KeepForDJVM data class Field( val name: String, val type: String, @@ -189,7 +185,6 @@ sealed class TypeNotation : DescribedType { abstract val descriptor: Descriptor } -@KeepForDJVM data class CompositeType( override val name: String, override val label: String?, @@ -240,7 +235,6 @@ data class CompositeType( } } -@KeepForDJVM data class RestrictedType(override val name: String, override val label: String?, override val provides: List, @@ -291,7 +285,6 @@ data class RestrictedType(override val name: String, } } -@KeepForDJVM data class Choice(val name: String, val value: String) : DescribedType { companion object : DescribedTypeConstructor { val DESCRIPTOR = AMQPDescriptorRegistry.CHOICE.amqpDescriptor @@ -321,7 +314,6 @@ data class Choice(val name: String, val value: String) : DescribedType { } } -@KeepForDJVM data class ReferencedObject(private val refCounter: Int) : DescribedType { companion object : DescribedTypeConstructor { val DESCRIPTOR = AMQPDescriptorRegistry.REFERENCED_OBJECT.amqpDescriptor diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializationOutput.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializationOutput.kt index 73b7eacae0..b0864af91f 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializationOutput.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializationOutput.kt @@ -1,6 +1,5 @@ package net.corda.serialization.internal.amqp -import net.corda.core.KeepForDJVM import net.corda.core.serialization.SerializationContext import net.corda.core.serialization.SerializedBytes import net.corda.core.utilities.contextLogger @@ -16,7 +15,6 @@ import java.lang.reflect.WildcardType import java.util.* import kotlin.collections.LinkedHashSet -@KeepForDJVM data class BytesAndSchemas( val obj: SerializedBytes, val schema: Schema, @@ -28,7 +26,6 @@ data class BytesAndSchemas( * @param serializerFactory This is the factory for [AMQPSerializer] instances and can be shared across multiple * instances and threads. */ -@KeepForDJVM open class SerializationOutput constructor( internal val serializerFactory: LocalSerializerFactory ) { diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializerFactory.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializerFactory.kt index 0eaeb7c6e2..70fcbbdbd8 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializerFactory.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializerFactory.kt @@ -1,10 +1,8 @@ package net.corda.serialization.internal.amqp -import net.corda.core.KeepForDJVM import java.io.NotSerializableException import javax.annotation.concurrent.ThreadSafe -@KeepForDJVM class SerializationSchemas(resolveSchema: () -> Pair) { constructor(schema: Schema, transforms: TransformsSchema) : this({ schema to transforms }) @@ -26,7 +24,6 @@ class SerializationSchemas(resolveSchema: () -> Pair) * @property onlyCustomSerializers used for testing, when set will cause the factory to throw a * [NotSerializableException] if it cannot find a registered custom serializer for a given type */ -@KeepForDJVM @ThreadSafe interface SerializerFactory : LocalSerializerFactory, RemoteSerializerFactory, CustomSerializerRegistry diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializerFactoryBuilder.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializerFactoryBuilder.kt index e1d0aaee77..8dca0a51a8 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializerFactoryBuilder.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializerFactoryBuilder.kt @@ -1,8 +1,6 @@ package net.corda.serialization.internal.amqp import com.google.common.primitives.Primitives -import net.corda.core.DeleteForDJVM -import net.corda.core.KeepForDJVM import net.corda.core.serialization.ClassWhitelist import net.corda.serialization.internal.carpenter.ClassCarpenter import net.corda.serialization.internal.carpenter.ClassCarpenterImpl @@ -12,11 +10,9 @@ import java.util.Collections.unmodifiableMap import java.util.function.Function import java.util.function.Predicate -@KeepForDJVM object SerializerFactoryBuilder { /** * The standard mapping of Java object types to Java primitive types. - * The DJVM will need to override these, but probably not anyone else. */ @Suppress("unchecked_cast") private val javaPrimitiveTypes: Map, Class<*>> = unmodifiableMap(listOf( @@ -46,7 +42,6 @@ object SerializerFactoryBuilder { } @JvmStatic - @DeleteForDJVM fun build( whitelist: ClassWhitelist, classCarpenter: ClassCarpenter, @@ -67,7 +62,6 @@ object SerializerFactoryBuilder { } @JvmStatic - @DeleteForDJVM fun build( whitelist: ClassWhitelist, carpenterClassLoader: ClassLoader, diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SupportedTransforms.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SupportedTransforms.kt index 8c2bd34086..e93a519133 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SupportedTransforms.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SupportedTransforms.kt @@ -1,6 +1,5 @@ package net.corda.serialization.internal.amqp -import net.corda.core.KeepForDJVM import net.corda.core.serialization.CordaSerializationTransformEnumDefault import net.corda.core.serialization.CordaSerializationTransformEnumDefaults import net.corda.core.serialization.CordaSerializationTransformRename @@ -16,7 +15,6 @@ import net.corda.core.serialization.CordaSerializationTransformRenames * that reference the transform. Notionally this allows the code that extracts transforms to work on single instances * of a transform or a meta list of them. */ -@KeepForDJVM data class SupportedTransform( val type: Class, val enum: TransformTypes, diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/TransformTypes.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/TransformTypes.kt index 6ea172604e..fc8b159865 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/TransformTypes.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/TransformTypes.kt @@ -1,6 +1,5 @@ package net.corda.serialization.internal.amqp -import net.corda.core.KeepForDJVM import net.corda.core.serialization.CordaSerializationTransformEnumDefault import net.corda.core.serialization.CordaSerializationTransformEnumDefaults import net.corda.core.serialization.CordaSerializationTransformRename @@ -20,7 +19,6 @@ import org.apache.qpid.proton.codec.DescribedTypeConstructor */ // TODO: it would be awesome to auto build this list by scanning for transform annotations themselves // TODO: annotated with some annotation -@KeepForDJVM enum class TransformTypes(val build: (Annotation) -> Transform) : DescribedType { /** * Placeholder entry for future transforms where a node receives a transform we've subsequently diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/TransformsSchema.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/TransformsSchema.kt index 09a3a09308..3cfc0cd69f 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/TransformsSchema.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/TransformsSchema.kt @@ -1,6 +1,5 @@ package net.corda.serialization.internal.amqp -import net.corda.core.KeepForDJVM import net.corda.core.serialization.CordaSerializationTransformEnumDefault import net.corda.core.serialization.CordaSerializationTransformRename import net.corda.serialization.internal.model.LocalTypeInformation @@ -151,7 +150,6 @@ class EnumDefaultSchemaTransform(val old: String, val new: String) : Transform() * @property from the name of the property or constant prior to being changed, i.e. what it was * @property to the new name of the property or constant after the change has been made, i.e. what it is now */ -@KeepForDJVM class RenameSchemaTransform(val from: String, val to: String) : Transform() { companion object : DescribedTypeConstructor { /** @@ -196,7 +194,6 @@ object TransformsAnnotationProcessor { */ fun getTransformsSchema(type: Class<*>): TransformsMap { return when { - // This only detects Enum classes that are outside the DJVM sandbox. type.isEnum -> getEnumTransformsSchema(type) // We only have transforms for enums at present. @@ -316,7 +313,6 @@ data class TransformsSchema(val types: Map, val elements: List) } \ No newline at end of file diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/InstantSerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/InstantSerializer.kt index 55807fbda4..a3ff2b01b9 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/InstantSerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/InstantSerializer.kt @@ -1,6 +1,5 @@ package net.corda.serialization.internal.amqp.custom -import net.corda.core.KeepForDJVM import net.corda.serialization.internal.amqp.CustomSerializer import net.corda.serialization.internal.amqp.SerializerFactory import java.time.Instant @@ -19,6 +18,5 @@ class InstantSerializer( override fun fromProxy(proxy: InstantProxy): Instant = Instant.ofEpochSecond(proxy.epochSeconds, proxy.nanos.toLong()) - @KeepForDJVM data class InstantProxy(val epochSeconds: Long, val nanos: Int) } \ No newline at end of file diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/LocalDateSerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/LocalDateSerializer.kt index 5801f6eb99..a6e1853f51 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/LocalDateSerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/LocalDateSerializer.kt @@ -1,6 +1,5 @@ package net.corda.serialization.internal.amqp.custom -import net.corda.core.KeepForDJVM import net.corda.serialization.internal.amqp.CustomSerializer import net.corda.serialization.internal.amqp.SerializerFactory import java.time.LocalDate @@ -19,6 +18,5 @@ class LocalDateSerializer( override fun fromProxy(proxy: LocalDateProxy): LocalDate = LocalDate.of(proxy.year, proxy.month.toInt(), proxy.day.toInt()) - @KeepForDJVM data class LocalDateProxy(val year: Int, val month: Byte, val day: Byte) } \ No newline at end of file diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/LocalDateTimeSerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/LocalDateTimeSerializer.kt index 79d5b5a8ed..69d3a25ff9 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/LocalDateTimeSerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/LocalDateTimeSerializer.kt @@ -1,6 +1,5 @@ package net.corda.serialization.internal.amqp.custom -import net.corda.core.KeepForDJVM import net.corda.serialization.internal.amqp.CustomSerializer import net.corda.serialization.internal.amqp.SerializerFactory import java.time.LocalDate @@ -26,6 +25,5 @@ class LocalDateTimeSerializer( override fun fromProxy(proxy: LocalDateTimeProxy): LocalDateTime = LocalDateTime.of(proxy.date, proxy.time) - @KeepForDJVM data class LocalDateTimeProxy(val date: LocalDate, val time: LocalTime) } \ No newline at end of file diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/LocalTimeSerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/LocalTimeSerializer.kt index 8637baecb3..5e91314bc2 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/LocalTimeSerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/LocalTimeSerializer.kt @@ -1,6 +1,5 @@ package net.corda.serialization.internal.amqp.custom -import net.corda.core.KeepForDJVM import net.corda.serialization.internal.amqp.CustomSerializer import net.corda.serialization.internal.amqp.SerializerFactory import java.time.LocalTime @@ -29,6 +28,5 @@ class LocalTimeSerializer( proxy.nano ) - @KeepForDJVM data class LocalTimeProxy(val hour: Byte, val minute: Byte, val second: Byte, val nano: Int) -} \ No newline at end of file +} diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/MonthDaySerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/MonthDaySerializer.kt index 1686bb0627..027b8504f8 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/MonthDaySerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/MonthDaySerializer.kt @@ -1,6 +1,5 @@ package net.corda.serialization.internal.amqp.custom -import net.corda.core.KeepForDJVM import net.corda.serialization.internal.amqp.CustomSerializer import net.corda.serialization.internal.amqp.SerializerFactory import java.time.MonthDay @@ -17,6 +16,5 @@ class MonthDaySerializer( override fun fromProxy(proxy: MonthDayProxy): MonthDay = MonthDay.of(proxy.month.toInt(), proxy.day.toInt()) - @KeepForDJVM data class MonthDayProxy(val month: Byte, val day: Byte) -} \ No newline at end of file +} diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/OffsetDateTimeSerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/OffsetDateTimeSerializer.kt index a8b33515a2..3bec0611b4 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/OffsetDateTimeSerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/OffsetDateTimeSerializer.kt @@ -1,6 +1,5 @@ package net.corda.serialization.internal.amqp.custom -import net.corda.core.KeepForDJVM import net.corda.serialization.internal.amqp.CustomSerializer import net.corda.serialization.internal.amqp.SerializerFactory import java.time.LocalDateTime @@ -26,6 +25,5 @@ class OffsetDateTimeSerializer( override fun fromProxy(proxy: OffsetDateTimeProxy): OffsetDateTime = OffsetDateTime.of(proxy.dateTime, proxy.offset) - @KeepForDJVM data class OffsetDateTimeProxy(val dateTime: LocalDateTime, val offset: ZoneOffset) -} \ No newline at end of file +} diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/OffsetTimeSerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/OffsetTimeSerializer.kt index 5e2fec1b55..190c81e51e 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/OffsetTimeSerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/OffsetTimeSerializer.kt @@ -1,6 +1,5 @@ package net.corda.serialization.internal.amqp.custom -import net.corda.core.KeepForDJVM import net.corda.serialization.internal.amqp.CustomSerializer import net.corda.serialization.internal.amqp.SerializerFactory import java.time.LocalTime @@ -26,6 +25,5 @@ class OffsetTimeSerializer( override fun fromProxy(proxy: OffsetTimeProxy): OffsetTime = OffsetTime.of(proxy.time, proxy.offset) - @KeepForDJVM data class OffsetTimeProxy(val time: LocalTime, val offset: ZoneOffset) -} \ No newline at end of file +} diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/OptionalSerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/OptionalSerializer.kt index be5020d06c..ce27854937 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/OptionalSerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/OptionalSerializer.kt @@ -1,6 +1,5 @@ package net.corda.serialization.internal.amqp.custom -import net.corda.core.KeepForDJVM import net.corda.serialization.internal.amqp.CustomSerializer import net.corda.serialization.internal.amqp.SerializerFactory import java.util.* @@ -24,6 +23,5 @@ class OptionalSerializer( return Optional.ofNullable(proxy.item) } - @KeepForDJVM data class OptionalProxy(val item: Any?) -} \ No newline at end of file +} diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/PeriodSerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/PeriodSerializer.kt index 5b01aee63c..43584a0d54 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/PeriodSerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/PeriodSerializer.kt @@ -1,6 +1,5 @@ package net.corda.serialization.internal.amqp.custom -import net.corda.core.KeepForDJVM import net.corda.serialization.internal.amqp.CustomSerializer import net.corda.serialization.internal.amqp.SerializerFactory import java.time.Period @@ -19,6 +18,5 @@ class PeriodSerializer( override fun fromProxy(proxy: PeriodProxy): Period = Period.of(proxy.years, proxy.months, proxy.days) - @KeepForDJVM data class PeriodProxy(val years: Int, val months: Int, val days: Int) -} \ No newline at end of file +} diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/PrivateKeySerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/PrivateKeySerializer.kt index a7c3bf33e6..983b1953b2 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/PrivateKeySerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/PrivateKeySerializer.kt @@ -1,6 +1,5 @@ package net.corda.serialization.internal.amqp.custom -import net.corda.core.DeleteForDJVM import net.corda.core.crypto.Crypto import net.corda.core.serialization.SerializationContext import net.corda.core.serialization.SerializationContext.UseCase.Storage @@ -10,7 +9,6 @@ import org.apache.qpid.proton.codec.Data import java.lang.reflect.Type import java.security.PrivateKey -@DeleteForDJVM object PrivateKeySerializer : CustomSerializer.Implements( PrivateKey::class.java diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/SimpleStringSerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/SimpleStringSerializer.kt index 74abcf125d..083c280835 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/SimpleStringSerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/SimpleStringSerializer.kt @@ -1,11 +1,9 @@ package net.corda.serialization.internal.amqp.custom -import net.corda.core.DeleteForDJVM import net.corda.serialization.internal.amqp.CustomSerializer import org.apache.activemq.artemis.api.core.SimpleString /** * A serializer for [SimpleString]. */ -@DeleteForDJVM object SimpleStringSerializer : CustomSerializer.ToString(SimpleString::class.java) \ No newline at end of file diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/ThrowableSerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/ThrowableSerializer.kt index 3f361e0b17..323f5a20a4 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/ThrowableSerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/ThrowableSerializer.kt @@ -2,7 +2,6 @@ package net.corda.serialization.internal.amqp.custom import net.corda.core.CordaRuntimeException import net.corda.core.CordaThrowable -import net.corda.core.KeepForDJVM import net.corda.core.serialization.SerializationFactory import net.corda.core.utilities.contextLogger import net.corda.serialization.internal.amqp.* @@ -10,7 +9,6 @@ import net.corda.serialization.internal.model.LocalConstructorInformation import net.corda.serialization.internal.model.LocalTypeInformation import java.io.NotSerializableException -@KeepForDJVM class ThrowableSerializer( factory: LocalSerializerFactory ) : CustomSerializer.Proxy( @@ -111,6 +109,5 @@ class StackTraceElementSerializer(factory: LocalSerializerFactory) : CustomSeria override fun fromProxy(proxy: StackTraceElementProxy): StackTraceElement = StackTraceElement(proxy.declaringClass, proxy.methodName, proxy.fileName, proxy.lineNumber) - @KeepForDJVM data class StackTraceElementProxy(val declaringClass: String, val methodName: String, val fileName: String?, val lineNumber: Int) } diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/YearMonthSerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/YearMonthSerializer.kt index d7aea2eefb..3c34a73004 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/YearMonthSerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/YearMonthSerializer.kt @@ -1,6 +1,5 @@ package net.corda.serialization.internal.amqp.custom -import net.corda.core.KeepForDJVM import net.corda.serialization.internal.amqp.CustomSerializer import net.corda.serialization.internal.amqp.SerializerFactory import java.time.YearMonth @@ -19,6 +18,5 @@ class YearMonthSerializer( override fun fromProxy(proxy: YearMonthProxy): YearMonth = YearMonth.of(proxy.year, proxy.month.toInt()) - @KeepForDJVM data class YearMonthProxy(val year: Int, val month: Byte) } \ No newline at end of file diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/YearSerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/YearSerializer.kt index 79b6248f77..705e4a5e61 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/YearSerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/YearSerializer.kt @@ -1,6 +1,5 @@ package net.corda.serialization.internal.amqp.custom -import net.corda.core.KeepForDJVM import net.corda.serialization.internal.amqp.CustomSerializer import net.corda.serialization.internal.amqp.SerializerFactory import java.time.Year @@ -19,6 +18,5 @@ class YearSerializer( override fun fromProxy(proxy: YearProxy): Year = Year.of(proxy.year) - @KeepForDJVM data class YearProxy(val year: Int) } \ No newline at end of file diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/ZoneIdSerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/ZoneIdSerializer.kt index a0340eae57..34856cecef 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/ZoneIdSerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/ZoneIdSerializer.kt @@ -1,6 +1,5 @@ package net.corda.serialization.internal.amqp.custom -import net.corda.core.KeepForDJVM import net.corda.serialization.internal.amqp.CustomSerializer import net.corda.serialization.internal.amqp.SerializerFactory import java.time.ZoneId @@ -21,6 +20,5 @@ class ZoneIdSerializer( override fun fromProxy(proxy: ZoneIdProxy): ZoneId = ZoneId.of(proxy.id) - @KeepForDJVM data class ZoneIdProxy(val id: String) } \ No newline at end of file diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/ZonedDateTimeSerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/ZonedDateTimeSerializer.kt index 57b2e36bb7..04ce52a65e 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/ZonedDateTimeSerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/custom/ZonedDateTimeSerializer.kt @@ -1,6 +1,5 @@ package net.corda.serialization.internal.amqp.custom -import net.corda.core.KeepForDJVM import net.corda.serialization.internal.amqp.CustomSerializer import net.corda.serialization.internal.amqp.SerializerFactory import java.lang.reflect.Method @@ -48,6 +47,5 @@ class ZonedDateTimeSerializer( proxy.zone ) as ZonedDateTime - @KeepForDJVM data class ZonedDateTimeProxy(val dateTime: LocalDateTime, val offset: ZoneOffset, val zone: ZoneId) } \ No newline at end of file diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/carpenter/ClassCarpenter.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/carpenter/ClassCarpenter.kt index 940b242064..aea420e237 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/carpenter/ClassCarpenter.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/carpenter/ClassCarpenter.kt @@ -1,9 +1,7 @@ -@file:DeleteForDJVM + package net.corda.serialization.internal.carpenter import com.google.common.base.MoreObjects -import net.corda.core.DeleteForDJVM -import net.corda.core.KeepForDJVM import net.corda.core.serialization.ClassWhitelist import net.corda.core.serialization.CordaSerializable import net.corda.core.utilities.contextLogger @@ -26,7 +24,6 @@ interface SimpleFieldAccess { operator fun get(name: String): Any? } -@DeleteForDJVM class CarpenterClassLoader(private val parentClassLoader: ClassLoader = Thread.currentThread().contextClassLoader) : ClassLoader(parentClassLoader) { @Throws(ClassNotFoundException::class) @@ -68,7 +65,6 @@ private val moreObjects: String = Type.getInternalName(MoreObjects::class.java) private val toStringHelper: String = Type.getInternalName(MoreObjects.ToStringHelper::class.java) // Allow us to create alternative ClassCarpenters. -@KeepForDJVM interface ClassCarpenter { val whitelist: ClassWhitelist val classloader: ClassLoader @@ -119,7 +115,6 @@ interface ClassCarpenter { * * Equals/hashCode methods are not yet supported. */ -@DeleteForDJVM class ClassCarpenterImpl @JvmOverloads constructor (override val whitelist: ClassWhitelist, cl: ClassLoader = Thread.currentThread().contextClassLoader, private val lenient: Boolean = false diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/carpenter/Exceptions.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/carpenter/Exceptions.kt index ffd6096f04..bed6b38a56 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/carpenter/Exceptions.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/carpenter/Exceptions.kt @@ -1,7 +1,6 @@ package net.corda.serialization.internal.carpenter import net.corda.core.CordaRuntimeException -import net.corda.core.DeleteForDJVM import org.objectweb.asm.Type /** @@ -17,7 +16,6 @@ abstract class InterfaceMismatchException(msg: String) : ClassCarpenterException class DuplicateNameException(val name: String) : ClassCarpenterException( "An attempt was made to register two classes with the name '$name' within the same ClassCarpenter namespace.") -@DeleteForDJVM class NullablePrimitiveException(val name: String, val field: Class) : ClassCarpenterException( "Field $name is primitive type ${Type.getDescriptor(field)} and thus cannot be nullable") diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/carpenter/Schema.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/carpenter/Schema.kt index 90a1034f70..1278e685ce 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/carpenter/Schema.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/carpenter/Schema.kt @@ -1,13 +1,9 @@ -@file:DeleteForDJVM package net.corda.serialization.internal.carpenter -import net.corda.core.DeleteForDJVM -import net.corda.core.KeepForDJVM import org.objectweb.asm.ClassWriter import org.objectweb.asm.Opcodes.* import java.util.* -@KeepForDJVM enum class SchemaFlags { SimpleFieldAccess, CordaSerializable } @@ -20,7 +16,6 @@ enum class SchemaFlags { * - [InterfaceSchema] * - [EnumSchema] */ -@KeepForDJVM abstract class Schema( val name: String, var fields: Map, @@ -45,7 +40,6 @@ abstract class Schema( fun descriptorsIncludingSuperclasses(): Map = (superclass?.descriptorsIncludingSuperclasses() ?: emptyMap()) + fields.descriptors() - @DeleteForDJVM abstract fun generateFields(cw: ClassWriter) val jvmName: String @@ -70,7 +64,6 @@ fun EnumMap.simpleFieldAccess(): Boolean { /** * Represents a concrete object. */ -@DeleteForDJVM class ClassSchema( name: String, fields: Map, @@ -86,7 +79,6 @@ class ClassSchema( * Represents an interface. Carpented interfaces can be used within [ClassSchema]s * if that class should be implementing that interface. */ -@DeleteForDJVM class InterfaceSchema( name: String, fields: Map, @@ -101,7 +93,6 @@ class InterfaceSchema( /** * Represents an enumerated type. */ -@DeleteForDJVM class EnumSchema( name: String, fields: Map @@ -123,7 +114,6 @@ class EnumSchema( * Factory object used by the serializer when building [Schema]s based * on an AMQP schema. */ -@DeleteForDJVM object CarpenterSchemaFactory { fun newInstance( name: String, diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/carpenter/SchemaFields.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/carpenter/SchemaFields.kt index 8e639ce810..03cc142a65 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/carpenter/SchemaFields.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/carpenter/SchemaFields.kt @@ -1,6 +1,5 @@ package net.corda.serialization.internal.carpenter -import net.corda.core.DeleteForDJVM import org.objectweb.asm.ClassWriter import org.objectweb.asm.MethodVisitor import org.objectweb.asm.Opcodes.* @@ -16,9 +15,8 @@ abstract class Field(val field: Class) { var name: String = unsetName abstract val type: String - @DeleteForDJVM abstract fun generateField(cw: ClassWriter) - @DeleteForDJVM + abstract fun visitParameter(mv: MethodVisitor, idx: Int) } @@ -29,7 +27,6 @@ abstract class Field(val field: Class) { * - [NullableField] * - [NonNullableField] */ -@DeleteForDJVM abstract class ClassField(field: Class) : Field(field) { abstract val nullabilityAnnotation: String abstract fun nullTest(mv: MethodVisitor, slot: Int) @@ -63,7 +60,6 @@ abstract class ClassField(field: Class) : Field(field) { * * maps to AMQP mandatory = true fields */ -@DeleteForDJVM open class NonNullableField(field: Class) : ClassField(field) { override val nullabilityAnnotation = "Ljavax/annotation/Nonnull;" @@ -93,7 +89,6 @@ open class NonNullableField(field: Class) : ClassField(field) { * * maps to AMQP mandatory = false fields */ -@DeleteForDJVM class NullableField(field: Class) : ClassField(field) { override val nullabilityAnnotation = "Ljavax/annotation/Nullable;" @@ -115,7 +110,6 @@ class NullableField(field: Class) : ClassField(field) { /** * Represents enum constants within an enum */ -@DeleteForDJVM class EnumField : Field(Enum::class.java) { override var descriptor: String? = null @@ -136,7 +130,6 @@ class EnumField : Field(Enum::class.java) { * Constructs a Field Schema object of the correct type depending weather * the AMQP schema indicates it's mandatory (non nullable) or not (nullable) */ -@DeleteForDJVM object FieldFactory { fun newInstance(mandatory: Boolean, name: String, field: Class) = if (mandatory) NonNullableField(name, field) else NullableField(name, field) diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/model/DefaultCacheProvider.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/model/DefaultCacheProvider.kt deleted file mode 100644 index 4995c1dbb2..0000000000 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/model/DefaultCacheProvider.kt +++ /dev/null @@ -1,11 +0,0 @@ -package net.corda.serialization.internal.model - -import java.util.concurrent.ConcurrentHashMap - -/** - * We can't have [ConcurrentHashMap]s in the DJVM, so it must supply its own version of this object which returns - * plain old [MutableMap]s instead. - */ -object DefaultCacheProvider { - fun createCache(): MutableMap = ConcurrentHashMap() -} \ No newline at end of file diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/model/LocalTypeModel.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/model/LocalTypeModel.kt index 7cfdfa3cfc..97d5016289 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/model/LocalTypeModel.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/model/LocalTypeModel.kt @@ -1,6 +1,7 @@ package net.corda.serialization.internal.model import java.lang.reflect.* +import java.util.concurrent.ConcurrentHashMap import java.util.function.Function import java.util.function.Predicate @@ -55,9 +56,9 @@ interface LocalTypeModel { * * @param typeModelConfiguration Configuration controlling the behaviour of the [LocalTypeModel]'s type inspection. */ -class ConfigurableLocalTypeModel(private val typeModelConfiguration: LocalTypeModelConfiguration): LocalTypeModel { +class ConfigurableLocalTypeModel(private val typeModelConfiguration: LocalTypeModelConfiguration) : LocalTypeModel { - private val typeInformationCache = DefaultCacheProvider.createCache() + private val typeInformationCache = ConcurrentHashMap() /** * We need to provide the [LocalTypeInformationBuilder] with a temporary local cache, so that it doesn't leak diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/model/TypeIdentifier.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/model/TypeIdentifier.kt index 3477c02a48..9c4787a880 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/model/TypeIdentifier.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/model/TypeIdentifier.kt @@ -206,11 +206,7 @@ sealed class TypeIdentifier { override fun toString() = "Parameterised(${prettyPrint()})" override fun getLocalType(classLoader: ClassLoader): Type { - // We need to invoke ClassLoader.loadClass() directly, because - // the JVM will complain if Class.forName() returns a class - // that has a name other than the requested one. This will happen - // for "transformative" class loaders, i.e. `A` -> `sandbox.A`. - val rawType = classLoader.loadClass(name) + val rawType = Class.forName(name, false, classLoader) if (rawType.typeParameters.size != parameters.size) { throw IncompatibleTypeIdentifierException( "Class $rawType expects ${rawType.typeParameters.size} type arguments, " + diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/model/TypeLoader.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/model/TypeLoader.kt index 046abb138d..bf604846f1 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/model/TypeLoader.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/model/TypeLoader.kt @@ -3,6 +3,7 @@ package net.corda.serialization.internal.model import net.corda.core.serialization.SerializationContext import net.corda.serialization.internal.carpenter.* import java.lang.reflect.Type +import java.util.concurrent.ConcurrentHashMap /** * A [TypeLoader] obtains local types whose [TypeIdentifier]s will reflect those of remote types. @@ -20,9 +21,9 @@ interface TypeLoader { * A [TypeLoader] that uses the [ClassCarpenter] to build a class matching the supplied [RemoteTypeInformation] if none * is visible from the current classloader. */ -class ClassCarpentingTypeLoader(private val carpenter: RemoteTypeCarpenter, private val classLoader: ClassLoader): TypeLoader { +class ClassCarpentingTypeLoader(private val carpenter: RemoteTypeCarpenter, private val classLoader: ClassLoader) : TypeLoader { - val cache = DefaultCacheProvider.createCache() + val cache = ConcurrentHashMap() override fun load( remoteTypeInformation: Collection, diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/model/TypeModellingFingerPrinter.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/model/TypeModellingFingerPrinter.kt index 8965a5c8e1..429dddd044 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/model/TypeModellingFingerPrinter.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/model/TypeModellingFingerPrinter.kt @@ -6,6 +6,7 @@ import net.corda.core.utilities.toBase64 import net.corda.serialization.internal.amqp.* import net.corda.serialization.internal.model.TypeIdentifier.* import java.lang.reflect.ParameterizedType +import java.util.concurrent.ConcurrentHashMap /** * A fingerprinter that fingerprints [LocalTypeInformation]. @@ -33,7 +34,7 @@ class TypeModellingFingerPrinter( private val classLoader: ClassLoader, private val debugEnabled: Boolean = false) : FingerPrinter { - private val cache: MutableMap = DefaultCacheProvider.createCache() + private val cache: MutableMap = ConcurrentHashMap() override fun fingerprint(typeInformation: LocalTypeInformation): String = /* diff --git a/serialization/src/test/README.md b/serialization/src/test/README.md index a98dff4c03..5c1c4a0c6f 100644 --- a/serialization/src/test/README.md +++ b/serialization/src/test/README.md @@ -2,10 +2,3 @@ Any tests that do not require further Corda dependencies (other than `core`) should be added to this module, anything that requires additional Corda dependencies needs to go into `serialization-tests`. - -The Corda Serialization module should be self-contained and compilable to Java 8 (for the DJVM) bytecode when using a Java 11 compiler. -Prior to this change, it was impossible to use a Java 11 compiler to compile this module to Java 8 bytecode due to its dependencies on other -modules compiled to Java 11 (`node-driver` and transitive dependencies including: `test-utils`, `node`, `test-common`, `common-logging`, `node-api`, -`client-mock`. `tools-cliutils`). -Therefore, any tests that require further Corda dependencies need to be defined in the module `serialization-tests`, which has the full set -of dependencies including `node-driver`. \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 4ec38aca3b..13fa984353 100644 --- a/settings.gradle +++ b/settings.gradle @@ -41,7 +41,6 @@ include 'node-api' include 'node-api-tests' include 'node' include 'node:capsule' -include 'node:djvm' include 'client:jackson' include 'client:jfx' include 'client:mock' @@ -56,7 +55,6 @@ include 'experimental:quasar-hook' include 'experimental:corda-utils' include 'experimental:nodeinfo' include 'experimental:netparams' -include 'jdk8u-deterministic' include 'test-common' include 'test-cli' include 'test-utils' @@ -96,8 +94,6 @@ include 'samples:cordapp-configuration:workflows' include 'samples:network-verifier:contracts' include 'samples:network-verifier:workflows' include 'serialization' -include 'serialization-djvm' -include 'serialization-djvm:deserializers' include 'serialization-tests' include 'testing:cordapps:dbfailure:dbfcontracts' include 'testing:cordapps:dbfailure:dbfworkflows' @@ -119,12 +115,6 @@ project(":common-logging").projectDir = new File("$settingsDir/common/logging") apply from: 'buildCacheSettings.gradle' -include 'core-deterministic' -include 'core-deterministic:testing' -include 'core-deterministic:testing:data' -include 'core-deterministic:testing:verifier' -include 'serialization-deterministic' - include 'detekt-plugins' include 'tools:error-tool' diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt index c0f0e6a120..4e5cf3893a 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt @@ -213,8 +213,6 @@ fun driver(defaultParameters: DriverParameters = DriverParameters(), dsl: Dr notaryCustomOverrides = defaultParameters.notaryCustomOverrides, inMemoryDB = defaultParameters.inMemoryDB, cordappsForAllNodes = uncheckedCast(defaultParameters.cordappsForAllNodes), - djvmBootstrapSource = defaultParameters.djvmBootstrapSource, - djvmCordaSource = defaultParameters.djvmCordaSource, environmentVariables = defaultParameters.environmentVariables, allowHibernateToManageAppSchema = defaultParameters.allowHibernateToManageAppSchema, premigrateH2Database = defaultParameters.premigrateH2Database, @@ -255,8 +253,6 @@ fun driver(defaultParameters: DriverParameters = DriverParameters(), dsl: Dr * the data is not persisted between node restarts). Has no effect if node is configured * in any way to use database other than H2. * @property cordappsForAllNodes [TestCordapp]s that will be added to each node started by the [DriverDSL]. - * @property djvmBootstrapSource Location of a JAR containing the Java APIs for the DJVM to use. - * @property djvmCordaSource Locations of JARs of user-supplied classes to execute within the DJVM sandbox. * @property premigrateH2Database Whether to use a prebuilt H2 database schema or start from an empty schema. * @property notaryHandleTimeout Specifies how long to wait to receive a notary handle. This waiting includes waiting for * the notary to start. @@ -281,8 +277,6 @@ data class DriverParameters( val notaryCustomOverrides: Map = emptyMap(), val inMemoryDB: Boolean = false, val cordappsForAllNodes: Collection? = null, - val djvmBootstrapSource: Path? = null, - val djvmCordaSource: List = emptyList(), val environmentVariables: Map = emptyMap(), val allowHibernateToManageAppSchema: Boolean = true, val premigrateH2Database: Boolean = true, @@ -323,10 +317,6 @@ data class DriverParameters( notaryCustomOverrides, inMemoryDB, cordappsForAllNodes, - - // These fields have been added in v4.4 - djvmBootstrapSource = null, - djvmCordaSource = emptyList(), environmentVariables = emptyMap() ) @@ -410,8 +400,6 @@ data class DriverParameters( notaryCustomOverrides: Map = emptyMap(), inMemoryDB: Boolean = false, cordappsForAllNodes: Collection? = null, - djvmBootstrapSource: Path? = null, - djvmCordaSource: List = emptyList(), environmentVariables: Map = emptyMap(), allowHibernateToManageAppSchema: Boolean = true ) : this( @@ -430,8 +418,6 @@ data class DriverParameters( notaryCustomOverrides, inMemoryDB, cordappsForAllNodes, - djvmBootstrapSource, - djvmCordaSource, environmentVariables, allowHibernateToManageAppSchema, premigrateH2Database = true @@ -485,8 +471,6 @@ data class DriverParameters( notaryCustomOverrides: Map, inMemoryDB: Boolean, cordappsForAllNodes: Collection?, - djvmBootstrapSource: Path?, - djvmCordaSource: List, environmentVariables: Map, allowHibernateToManageAppSchema: Boolean, premigrateH2Database: Boolean = true @@ -506,8 +490,6 @@ data class DriverParameters( notaryCustomOverrides, inMemoryDB, cordappsForAllNodes, - djvmBootstrapSource, - djvmCordaSource, environmentVariables, allowHibernateToManageAppSchema, premigrateH2Database, @@ -533,8 +515,6 @@ data class DriverParameters( fun withNotaryCustomOverrides(notaryCustomOverrides: Map): DriverParameters = copy(notaryCustomOverrides = notaryCustomOverrides) fun withInMemoryDB(inMemoryDB: Boolean): DriverParameters = copy(inMemoryDB = inMemoryDB) fun withCordappsForAllNodes(cordappsForAllNodes: Collection?): DriverParameters = copy(cordappsForAllNodes = cordappsForAllNodes) - fun withDjvmBootstrapSource(djvmBootstrapSource: Path?): DriverParameters = copy(djvmBootstrapSource = djvmBootstrapSource) - fun withDjvmCordaSource(djvmCordaSource: List): DriverParameters = copy(djvmCordaSource = djvmCordaSource) fun withEnvironmentVariables(variables: Map): DriverParameters = copy(environmentVariables = variables) fun withAllowHibernateToManageAppSchema(value: Boolean): DriverParameters = copy(allowHibernateToManageAppSchema = value) fun withNotaryHandleTimeout(value: Duration): DriverParameters = copy(notaryHandleTimeout = value) @@ -633,9 +613,6 @@ data class DriverParameters( notaryCustomOverrides = notaryCustomOverrides, inMemoryDB = inMemoryDB, cordappsForAllNodes = cordappsForAllNodes, - // These fields have been added in v4.4 - djvmBootstrapSource = djvmBootstrapSource, - djvmCordaSource = djvmCordaSource, environmentVariables = environmentVariables ) @@ -656,8 +633,6 @@ data class DriverParameters( notaryCustomOverrides: Map, inMemoryDB: Boolean, cordappsForAllNodes: Collection?, - djvmBootstrapSource: Path?, - djvmCordaSource: List, environmentVariables: Map, allowHibernateToManageAppSchema: Boolean ) = this.copy( @@ -676,8 +651,6 @@ data class DriverParameters( notaryCustomOverrides = notaryCustomOverrides, inMemoryDB = inMemoryDB, cordappsForAllNodes = cordappsForAllNodes, - djvmBootstrapSource = djvmBootstrapSource, - djvmCordaSource = djvmCordaSource, environmentVariables = environmentVariables, allowHibernateToManageAppSchema = allowHibernateToManageAppSchema, premigrateH2Database = true @@ -700,8 +673,6 @@ data class DriverParameters( notaryCustomOverrides: Map, inMemoryDB: Boolean, cordappsForAllNodes: Collection?, - djvmBootstrapSource: Path?, - djvmCordaSource: List, environmentVariables: Map, allowHibernateToManageAppSchema: Boolean, premigrateH2Database: Boolean @@ -721,8 +692,6 @@ data class DriverParameters( notaryCustomOverrides = notaryCustomOverrides, inMemoryDB = inMemoryDB, cordappsForAllNodes = cordappsForAllNodes, - djvmBootstrapSource = djvmBootstrapSource, - djvmCordaSource = djvmCordaSource, environmentVariables = environmentVariables, allowHibernateToManageAppSchema = allowHibernateToManageAppSchema, premigrateH2Database = premigrateH2Database, diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt index 5dc744d1bf..b4b7d673e8 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt @@ -150,8 +150,6 @@ class DriverDSLImpl( val notaryCustomOverrides: Map, val inMemoryDB: Boolean, val cordappsForAllNodes: Collection?, - val djvmBootstrapSource: Path?, - val djvmCordaSource: List, val environmentVariables: Map, val allowHibernateToManageAppSchema: Boolean = true, val premigrateH2Database: Boolean = true, @@ -353,7 +351,7 @@ class DriverDSLImpl( baseDirectory = baseDirectory, allowMissingConfig = true, configOverrides = if (overrides.hasPath("devMode")) overrides else overrides + mapOf("devMode" to true) - ).withDJVMConfig(djvmBootstrapSource, djvmCordaSource) + ) ).checkAndOverrideForInMemoryDB() } @@ -892,24 +890,6 @@ class DriverDSLImpl( CORDAPP_WORKFLOW_VERSION )) - /** - * Add the DJVM's sources to the node's configuration file. - * These will all be ignored unless devMode is also true. - */ - private fun Config.withDJVMConfig(bootstrapSource: Path?, cordaSource: List): Config { - return if (hasPath("devMode")) { - if (getBoolean("devMode")) { - withOptionalValue("devModeOptions.djvm.bootstrapSource", bootstrapSource) { path -> - valueFor(path.toString()) - }.withValue("devModeOptions.djvm.cordaSource", valueFor(cordaSource.map(Path::toString))) - } else { - withoutPath("devModeOptions") - } - } else { - this - } - } - private inline fun Config.withOptionalValue(key: String, obj: T?, body: (T) -> ConfigValue): Config { return if (obj == null) { this @@ -989,13 +969,13 @@ class DriverDSLImpl( val excludePackagePattern = "x(antlr**;bftsmart**;ch**;co.paralleluniverse**;com.codahale**;com.esotericsoftware**;" + "com.fasterxml**;com.google**;com.ibm**;com.intellij**;com.jcabi**;com.nhaarman**;com.opengamma**;" + "com.typesafe**;com.zaxxer**;de.javakaffee**;groovy**;groovyjarjarantlr**;groovyjarjarasm**;io.atomix**;" + - "io.github**;io.netty**;jdk**;joptsimple**;junit**;kotlin**;net.corda.djvm**;djvm.**;net.bytebuddy**;" + + "io.github**;io.netty**;jdk**;joptsimple**;junit**;kotlin**;net.bytebuddy**;" + "net.i2p**;org.apache**;" + "org.assertj**;org.bouncycastle**;org.codehaus**;org.crsh**;org.dom4j**;org.fusesource**;org.h2**;" + "org.hamcrest**;org.hibernate**;org.jboss**;org.jcp**;org.joda**;org.junit**;org.mockito**;org.objectweb**;" + "org.objenesis**;org.slf4j**;org.w3c**;org.xml**;org.yaml**;reflectasm**;rx**;org.jolokia**;" + "com.lmax**;picocli**;liquibase**;com.github.benmanes**;org.json**;org.postgresql**;nonapi.io.github.classgraph**;)" - val excludeClassloaderPattern = "l(net.corda.djvm.**;net.corda.core.serialization.internal.**)" + val excludeClassloaderPattern = "l(net.corda.core.serialization.internal.**)" val extraJvmArguments = systemProperties.removeResolvedClasspath().map { "-D${it.key}=${it.value}" } + "-javaagent:$quasarJarPath=$excludePackagePattern$excludeClassloaderPattern" @@ -1328,8 +1308,6 @@ fun genericDriver( notaryCustomOverrides = defaultParameters.notaryCustomOverrides, inMemoryDB = defaultParameters.inMemoryDB, cordappsForAllNodes = uncheckedCast(defaultParameters.cordappsForAllNodes), - djvmBootstrapSource = defaultParameters.djvmBootstrapSource, - djvmCordaSource = defaultParameters.djvmCordaSource, environmentVariables = defaultParameters.environmentVariables, allowHibernateToManageAppSchema = defaultParameters.allowHibernateToManageAppSchema, premigrateH2Database = defaultParameters.premigrateH2Database, @@ -1428,8 +1406,6 @@ fun internalDriver( notaryCustomOverrides: Map = DriverParameters().notaryCustomOverrides, inMemoryDB: Boolean = DriverParameters().inMemoryDB, cordappsForAllNodes: Collection? = null, - djvmBootstrapSource: Path? = null, - djvmCordaSource: List = emptyList(), environmentVariables: Map = emptyMap(), allowHibernateToManageAppSchema: Boolean = true, premigrateH2Database: Boolean = true, @@ -1454,8 +1430,6 @@ fun internalDriver( notaryCustomOverrides = notaryCustomOverrides, inMemoryDB = inMemoryDB, cordappsForAllNodes = cordappsForAllNodes, - djvmBootstrapSource = djvmBootstrapSource, - djvmCordaSource = djvmCordaSource, environmentVariables = environmentVariables, allowHibernateToManageAppSchema = allowHibernateToManageAppSchema, premigrateH2Database = premigrateH2Database, diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/RPCDriver.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/RPCDriver.kt index 1d8106993d..dcb12782c5 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/RPCDriver.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/RPCDriver.kt @@ -134,8 +134,6 @@ fun rpcDriver( notaryCustomOverrides: Map = emptyMap(), inMemoryDB: Boolean = true, cordappsForAllNodes: Collection? = null, - djvmBootstrapSource: Path? = null, - djvmCordaSource: List = emptyList(), environmentVariables: Map = emptyMap(), dsl: RPCDriverDSL.() -> A ): A { @@ -158,8 +156,6 @@ fun rpcDriver( notaryCustomOverrides = notaryCustomOverrides, inMemoryDB = inMemoryDB, cordappsForAllNodes = cordappsForAllNodes, - djvmBootstrapSource = djvmBootstrapSource, - djvmCordaSource = djvmCordaSource, environmentVariables = environmentVariables ), externalTrace ), diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeController.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeController.kt index 1d3452bca1..bfd2747f05 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeController.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeController.kt @@ -1,6 +1,5 @@ package net.corda.demobench.model -import javafx.application.Application.Parameters import javafx.application.Platform import javafx.beans.binding.IntegerExpression import javafx.beans.property.SimpleBooleanProperty @@ -31,7 +30,6 @@ import java.util.logging.Level import kotlin.math.max class NodeController( - djvmEnabled: Boolean = readDJVMEnabled(), check: atRuntime = ::checkExists ) : Controller() { companion object { @@ -42,22 +40,8 @@ class NodeController( private const val MB = 1024 * 1024 const val maxMessageSize = 10 * MB const val maxTransactionSize = 10 * MB - - private fun readDJVMEnabled(): Boolean { - return FX.application.parameters?.let(::parseDJVMEnabled) ?: false - } - - private fun parseDJVMEnabled(parameters: Parameters): Boolean { - val isEnabled = parameters.named["djvm"] - return if (isEnabled == null) { - parameters.unnamed.contains("--djvm") - } else { - java.lang.Boolean.parseBoolean(isEnabled) - } - } } - val djvmEnabled = SimpleBooleanProperty(djvmEnabled) val allowHibernateToManageAppSchema = SimpleBooleanProperty(false) private val jvm by inject() @@ -113,7 +97,6 @@ class NodeController( h2port = nodeData.h2Port.value, issuableCurrencies = nodeData.extraServices.filterIsInstance().map { it.currency.toString() }, systemProperties = mapOf( - "net.corda.djvm" to djvmEnabled.value, "co.paralleluniverse.fibers.verifyInstrumentation" to false ) ) diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/views/NodeTabView.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/views/NodeTabView.kt index c308d1771f..595844406f 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/views/NodeTabView.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/views/NodeTabView.kt @@ -152,9 +152,6 @@ class NodeTabView : Fragment() { } separator() - checkbox("Deterministic Contract Verification", nodeController.djvmEnabled).apply { - styleClass += "djvm" - } checkbox("Allow Hibernate to manage app schema", nodeController.allowHibernateToManageAppSchema).apply { styleClass += "hibernate" } diff --git a/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeControllerTest.kt b/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeControllerTest.kt index d6f407cbd5..6cfc9344de 100644 --- a/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeControllerTest.kt +++ b/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeControllerTest.kt @@ -11,7 +11,7 @@ import kotlin.test.* class NodeControllerTest { private val baseDir: Path = Paths.get(".").toAbsolutePath() - private val controller = NodeController(false) { _, _ -> } + private val controller = NodeController { _, _ -> } private val node1Name = "Organisation 1" private val organisation2Name = "Organisation 2" @@ -70,7 +70,6 @@ class NodeControllerTest { val wrapper = controller.validate(data) ?: fail("No wrapped configuration!") val systemProperties = wrapper.nodeConfig.systemProperties - assertFalse(systemProperties["net.corda.djvm"] as Boolean) assertFalse(systemProperties["co.paralleluniverse.fibers.verifyInstrumentation"] as Boolean) } From 0130914c893b6e8e2c8c14f0e699da5943a48845 Mon Sep 17 00:00:00 2001 From: Jose Coll Date: Tue, 15 Aug 2023 15:32:00 +0100 Subject: [PATCH 053/126] ENT-9927 Ledger Recovery: synchronise changes from ENT -> OS. (#7445) --- .../coretests/flows/FinalityFlowTests.kt | 11 +- .../DBTransactionStorageLedgerRecovery.kt | 174 ++++++++++++------ .../migration/node-core.changelog-v25.xml | 47 ++--- ...DBTransactionStorageLedgerRecoveryTests.kt | 39 +++- 4 files changed, 160 insertions(+), 111 deletions(-) diff --git a/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt b/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt index fefbac09c1..e34b0a4c2b 100644 --- a/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt +++ b/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt @@ -354,12 +354,11 @@ class FinalityFlowTests : WithFinality { getSenderRecoveryData(stx.id, aliceNode.database).apply { assertEquals(1, this.size) - assertEquals(StatesToRecord.ONLY_RELEVANT, this[0].statesToRecord) + assertEquals(StatesToRecord.ALL_VISIBLE, this[0].statesToRecord) assertEquals(BOB_NAME.hashCode().toLong(), this[0].peerPartyId) } getReceiverRecoveryData(stx.id, bobNode.database).apply { - assertEquals(StatesToRecord.ALL_VISIBLE, this?.statesToRecord) - assertEquals(StatesToRecord.ONLY_RELEVANT, this?.senderStatesToRecord) + assertEquals(StatesToRecord.ONLY_RELEVANT, this?.statesToRecord) assertEquals(aliceNode.info.singleIdentity().name.hashCode().toLong(), this?.initiatorPartyId) assertEquals(mapOf(BOB_NAME.hashCode().toLong() to StatesToRecord.ALL_VISIBLE), this?.peersToStatesToRecord) } @@ -387,12 +386,10 @@ class FinalityFlowTests : WithFinality { assertEquals(2, this.size) assertEquals(StatesToRecord.ONLY_RELEVANT, this[0].statesToRecord) assertEquals(BOB_NAME.hashCode().toLong(), this[0].peerPartyId) - assertEquals(StatesToRecord.ONLY_RELEVANT, this[1].statesToRecord) + assertEquals(StatesToRecord.ALL_VISIBLE, this[1].statesToRecord) assertEquals(CHARLIE_NAME.hashCode().toLong(), this[1].peerPartyId) } getReceiverRecoveryData(stx.id, bobNode.database).apply { - assertEquals(StatesToRecord.ONLY_RELEVANT, this?.statesToRecord) - assertEquals(StatesToRecord.ONLY_RELEVANT, this?.senderStatesToRecord) assertEquals(aliceNode.info.singleIdentity().name.hashCode().toLong(), this?.initiatorPartyId) // note: Charlie assertion here is using the hinted StatesToRecord value passed to it from Alice assertEquals(mapOf(BOB_NAME.hashCode().toLong() to StatesToRecord.ONLY_RELEVANT, @@ -434,8 +431,6 @@ class FinalityFlowTests : WithFinality { assertEquals(BOB_NAME.hashCode().toLong(), this[0].peerPartyId) } getReceiverRecoveryData(stx.id, bobNode.database).apply { - assertEquals(StatesToRecord.ONLY_RELEVANT, this?.statesToRecord) - assertEquals(StatesToRecord.ONLY_RELEVANT, this?.senderStatesToRecord) assertEquals(aliceNode.info.singleIdentity().name.hashCode().toLong(), this?.initiatorPartyId) assertEquals(mapOf(BOB_NAME.hashCode().toLong() to StatesToRecord.ONLY_RELEVANT), this?.peersToStatesToRecord) } diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecovery.kt b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecovery.kt index 8548dc0d80..6a064f6bc8 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecovery.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecovery.kt @@ -5,6 +5,7 @@ import net.corda.core.flows.RecoveryTimeWindow import net.corda.core.flows.TransactionMetadata import net.corda.core.identity.CordaX500Name import net.corda.core.internal.NamedCacheFactory +import net.corda.core.internal.VisibleForTesting import net.corda.core.node.StatesToRecord import net.corda.core.node.services.vault.Sort import net.corda.core.serialization.CordaSerializable @@ -20,7 +21,7 @@ import java.io.DataInputStream import java.io.DataOutputStream import java.io.Serializable import java.time.Instant -import java.util.concurrent.atomic.AtomicLong +import java.util.concurrent.atomic.AtomicInteger import javax.persistence.Column import javax.persistence.Embeddable import javax.persistence.EmbeddedId @@ -33,20 +34,26 @@ import kotlin.streams.toList class DBTransactionStorageLedgerRecovery(private val database: CordaPersistence, cacheFactory: NamedCacheFactory, val clock: CordaClock, - private val cryptoService: CryptoService, + val cryptoService: CryptoService, private val partyInfoCache: PersistentPartyInfoCache) : DBTransactionStorage(database, cacheFactory, clock) { @Embeddable @Immutable data class PersistentKey( - @Column(name = "sequence_number", nullable = false) - var sequenceNumber: Long, + /** PartyId of flow peer **/ + @Column(name = "peer_party_id", nullable = false) + var peerPartyId: Long, @Column(name = "timestamp", nullable = false) - var timestamp: Instant + var timestamp: Instant, + + @Column(name = "timestamp_discriminator", nullable = false) + var timestampDiscriminator: Int + ) : Serializable { - constructor(key: Key) : this(key.sequenceNumber, key.timestamp) + constructor(key: Key) : this(key.partyId, key.timestamp, key.timestampDiscriminator) } + @CordaSerializable @Entity @Table(name = "${NODE_DATABASE_PREFIX}sender_distribution_records") data class DBSenderDistributionRecord( @@ -56,10 +63,6 @@ class DBTransactionStorageLedgerRecovery(private val database: CordaPersistence, @Column(name = "transaction_id", length = 144, nullable = false) var txId: String, - /** PartyId of flow peer **/ - @Column(name = "receiver_party_id", nullable = false) - val receiverPartyId: Long, - /** states to record: NONE, ALL_VISIBLE, ONLY_RELEVANT */ @Column(name = "states_to_record", nullable = false) var statesToRecord: StatesToRecord @@ -68,12 +71,13 @@ class DBTransactionStorageLedgerRecovery(private val database: CordaPersistence, fun toSenderDistributionRecord() = SenderDistributionRecord( SecureHash.parse(this.txId), - this.receiverPartyId, + this.compositeKey.peerPartyId, this.statesToRecord, this.compositeKey.timestamp ) } + @CordaSerializable @Entity @Table(name = "${NODE_DATABASE_PREFIX}receiver_distribution_records") data class DBReceiverDistributionRecord( @@ -83,34 +87,23 @@ class DBTransactionStorageLedgerRecovery(private val database: CordaPersistence, @Column(name = "transaction_id", length = 144, nullable = false) var txId: String, - /** PartyId of flow initiator **/ - @Column(name = "sender_party_id", nullable = true) - val senderPartyId: Long, - /** Encrypted recovery information for sole use by Sender **/ @Lob @Column(name = "distribution_list", nullable = false) - val distributionList: ByteArray, - - /** states to record: NONE, ALL_VISIBLE, ONLY_RELEVANT */ - @Column(name = "receiver_states_to_record", nullable = false) - val receiverStatesToRecord: StatesToRecord + val distributionList: ByteArray ) { - constructor(key: Key, txId: SecureHash, initiatorPartyId: Long, encryptedDistributionList: ByteArray, receiverStatesToRecord: StatesToRecord) : + constructor(key: Key, txId: SecureHash, encryptedDistributionList: ByteArray) : this(PersistentKey(key), txId = txId.toString(), - senderPartyId = initiatorPartyId, - distributionList = encryptedDistributionList, - receiverStatesToRecord = receiverStatesToRecord + distributionList = encryptedDistributionList ) fun toReceiverDistributionRecord(cryptoService: CryptoService): ReceiverDistributionRecord { val hashedDL = HashedDistributionList.deserialize(cryptoService.decrypt(this.distributionList)) return ReceiverDistributionRecord( SecureHash.parse(this.txId), - this.senderPartyId, + this.compositeKey.peerPartyId, hashedDL.peerHashToStatesToRecord, - this.receiverStatesToRecord, hashedDL.senderStatesToRecord, this.compositeKey.timestamp ) @@ -130,23 +123,29 @@ class DBTransactionStorageLedgerRecovery(private val database: CordaPersistence, val partyName: String ) + data class TimestampKey(val timestamp: Instant, val timestampDiscriminator: Int) + class Key( + val partyId: Long, val timestamp: Instant, - val sequenceNumber: Long = nextSequenceNumber.andIncrement + val timestampDiscriminator: Int = nextDiscriminatorNumber.andIncrement ) { + constructor(key: TimestampKey, partyId: Long): this(partyId = partyId, timestamp = key.timestamp, timestampDiscriminator = key.timestampDiscriminator) companion object { - private val nextSequenceNumber = AtomicLong() + val nextDiscriminatorNumber = AtomicInteger() } } override fun addSenderTransactionRecoveryMetadata(id: SecureHash, metadata: TransactionMetadata): ByteArray { + val senderRecordingTimestamp = clock.instant() return database.transaction { - val senderRecordingTimestamp = clock.instant() - metadata.distributionList.peersToStatesToRecord.forEach { (peer, _) -> - val senderDistributionRecord = DBSenderDistributionRecord(PersistentKey(Key(senderRecordingTimestamp)), + // sender distribution records must be unique per txnId and timestamp + val timeDiscriminator = Key.nextDiscriminatorNumber.andIncrement + metadata.distributionList.peersToStatesToRecord.map { (peerCordaX500Name, peerStatesToRecord) -> + val senderDistributionRecord = DBSenderDistributionRecord( + PersistentKey(Key(TimestampKey(senderRecordingTimestamp, timeDiscriminator), partyInfoCache.getPartyIdByCordaX500Name(peerCordaX500Name))), id.toString(), - partyInfoCache.getPartyIdByCordaX500Name(peer), - metadata.distributionList.senderStatesToRecord) + peerStatesToRecord) session.save(senderDistributionRecord) } val hashedPeersToStatesToRecord = metadata.distributionList.peersToStatesToRecord.map { (peer, statesToRecord) -> @@ -156,19 +155,48 @@ class DBTransactionStorageLedgerRecovery(private val database: CordaPersistence, } } + fun createReceiverTransactionRecoverMetadata(txId: SecureHash, + senderPartyId: Long, + senderStatesToRecord: StatesToRecord, + senderRecords: List): List { + val senderRecordsByTimestampKey = senderRecords.groupBy { TimestampKey(it.compositeKey.timestamp, it.compositeKey.timestampDiscriminator) } + return senderRecordsByTimestampKey.map { + val hashedDistributionList = HashedDistributionList( + senderStatesToRecord = senderStatesToRecord, + peerHashToStatesToRecord = senderRecords.map { it.compositeKey.peerPartyId to it.statesToRecord }.toMap(), + senderRecordedTimestamp = it.key.timestamp + ) + DBReceiverDistributionRecord( + compositeKey = PersistentKey(Key(TimestampKey(it.key.timestamp, it.key.timestampDiscriminator), senderPartyId)), + txId = txId.toString(), + distributionList = cryptoService.encrypt(hashedDistributionList.serialize()) + ) + } + } + + fun addSenderTransactionRecoveryMetadata(record: DBSenderDistributionRecord) { + return database.transaction { + session.save(record) + } + } + override fun addReceiverTransactionRecoveryMetadata(id: SecureHash, sender: CordaX500Name, receiver: CordaX500Name, receiverStatesToRecord: StatesToRecord, encryptedDistributionList: ByteArray) { val senderRecordedTimestamp = HashedDistributionList.deserialize(cryptoService.decrypt(encryptedDistributionList)).senderRecordedTimestamp database.transaction { val receiverDistributionRecord = - DBReceiverDistributionRecord(Key(senderRecordedTimestamp), + DBReceiverDistributionRecord(Key(partyInfoCache.getPartyIdByCordaX500Name(sender), senderRecordedTimestamp), id, - partyInfoCache.getPartyIdByCordaX500Name(sender), - encryptedDistributionList, - receiverStatesToRecord) + encryptedDistributionList) session.save(receiverDistributionRecord) } } + fun addReceiverTransactionRecoveryMetadata(record: DBReceiverDistributionRecord) { + return database.transaction { + session.save(record) + } + } + override fun removeUnnotarisedTransaction(id: SecureHash): Boolean { return database.transaction { super.removeUnnotarisedTransaction(id) @@ -187,27 +215,30 @@ class DBTransactionStorageLedgerRecovery(private val database: CordaPersistence, fun queryDistributionRecords(timeWindow: RecoveryTimeWindow, recordType: DistributionRecordType = DistributionRecordType.ALL, - excludingTxnIds: Set? = null, + excludingTxnIds: Set = emptySet(), orderByTimestamp: Sort.Direction? = null - ): List { + ): DistributionRecords { return when(recordType) { DistributionRecordType.SENDER -> - querySenderDistributionRecords(timeWindow, excludingTxnIds = excludingTxnIds, orderByTimestamp = orderByTimestamp) + DistributionRecords(senderRecords = + querySenderDistributionRecords(timeWindow, excludingTxnIds = excludingTxnIds, orderByTimestamp = orderByTimestamp)) DistributionRecordType.RECEIVER -> - queryReceiverDistributionRecords(timeWindow, excludingTxnIds = excludingTxnIds, orderByTimestamp = orderByTimestamp) + DistributionRecords(receiverRecords = + queryReceiverDistributionRecords(timeWindow, excludingTxnIds = excludingTxnIds, orderByTimestamp = orderByTimestamp)) DistributionRecordType.ALL -> - querySenderDistributionRecords(timeWindow, excludingTxnIds = excludingTxnIds, orderByTimestamp = orderByTimestamp).plus( - queryReceiverDistributionRecords(timeWindow, excludingTxnIds = excludingTxnIds, orderByTimestamp = orderByTimestamp) - ) + DistributionRecords(senderRecords = + querySenderDistributionRecords(timeWindow, excludingTxnIds = excludingTxnIds, orderByTimestamp = orderByTimestamp), + receiverRecords = + queryReceiverDistributionRecords(timeWindow, excludingTxnIds = excludingTxnIds, orderByTimestamp = orderByTimestamp)) } } @Suppress("SpreadOperator") fun querySenderDistributionRecords(timeWindow: RecoveryTimeWindow, peers: Set = emptySet(), - excludingTxnIds: Set? = null, + excludingTxnIds: Set = emptySet(), orderByTimestamp: Sort.Direction? = null - ): List { + ): List { return database.transaction { val criteriaBuilder = session.criteriaBuilder val criteriaQuery = criteriaBuilder.createQuery(DBSenderDistributionRecord::class.java) @@ -216,13 +247,13 @@ class DBTransactionStorageLedgerRecovery(private val database: CordaPersistence, val compositeKey = txnMetadata.get("compositeKey") predicates.add(criteriaBuilder.greaterThanOrEqualTo(compositeKey.get(PersistentKey::timestamp.name), timeWindow.fromTime)) predicates.add(criteriaBuilder.and(criteriaBuilder.lessThanOrEqualTo(compositeKey.get(PersistentKey::timestamp.name), timeWindow.untilTime))) - excludingTxnIds?.let { excludingTxnIds -> - predicates.add(criteriaBuilder.and(criteriaBuilder.notEqual(txnMetadata.get(DBSenderDistributionRecord::txId.name), - excludingTxnIds.map { it.toString() }))) + if (excludingTxnIds.isNotEmpty()) { + predicates.add(criteriaBuilder.and(criteriaBuilder.not(txnMetadata.get(DBSenderDistributionRecord::txId.name).`in`( + excludingTxnIds.map { it.toString() })))) } if (peers.isNotEmpty()) { val peerPartyIds = peers.map { partyInfoCache.getPartyIdByCordaX500Name(it) } - predicates.add(criteriaBuilder.and(txnMetadata.get(DBSenderDistributionRecord::receiverPartyId.name).`in`(peerPartyIds))) + predicates.add(criteriaBuilder.and(compositeKey.get(PersistentKey::peerPartyId.name).`in`(peerPartyIds))) } criteriaQuery.where(*predicates.toTypedArray()) // optionally order by timestamp @@ -236,16 +267,27 @@ class DBTransactionStorageLedgerRecovery(private val database: CordaPersistence, criteriaQuery.orderBy(orderCriteria) } val results = session.createQuery(criteriaQuery).stream() - results.map { it.toSenderDistributionRecord() }.toList() + results.toList() + } + } + + fun querySenderDistributionRecordsByTxId(txId: SecureHash): List { + return database.transaction { + val criteriaBuilder = session.criteriaBuilder + val criteriaQuery = criteriaBuilder.createQuery(DBSenderDistributionRecord::class.java) + val txnMetadata = criteriaQuery.from(DBSenderDistributionRecord::class.java) + criteriaQuery.where(criteriaBuilder.equal(txnMetadata.get(DBSenderDistributionRecord::txId.name), txId.toString())) + val results = session.createQuery(criteriaQuery).stream() + results.toList() } } @Suppress("SpreadOperator") fun queryReceiverDistributionRecords(timeWindow: RecoveryTimeWindow, initiators: Set = emptySet(), - excludingTxnIds: Set? = null, + excludingTxnIds: Set = emptySet(), orderByTimestamp: Sort.Direction? = null - ): List { + ): List { return database.transaction { val criteriaBuilder = session.criteriaBuilder val criteriaQuery = criteriaBuilder.createQuery(DBReceiverDistributionRecord::class.java) @@ -254,13 +296,13 @@ class DBTransactionStorageLedgerRecovery(private val database: CordaPersistence, val compositeKey = txnMetadata.get("compositeKey") predicates.add(criteriaBuilder.greaterThanOrEqualTo(compositeKey.get(PersistentKey::timestamp.name), timeWindow.fromTime)) predicates.add(criteriaBuilder.and(criteriaBuilder.lessThanOrEqualTo(compositeKey.get(PersistentKey::timestamp.name), timeWindow.untilTime))) - excludingTxnIds?.let { excludingTxnIds -> - predicates.add(criteriaBuilder.and(criteriaBuilder.notEqual(txnMetadata.get(DBReceiverDistributionRecord::txId.name), - excludingTxnIds.map { it.toString() }))) + if (excludingTxnIds.isNotEmpty()) { + predicates.add(criteriaBuilder.and(criteriaBuilder.not(txnMetadata.get(DBSenderDistributionRecord::txId.name).`in`( + excludingTxnIds.map { it.toString() })))) } if (initiators.isNotEmpty()) { val initiatorPartyIds = initiators.map { partyInfoCache.getPartyIdByCordaX500Name(it) } - predicates.add(criteriaBuilder.and(txnMetadata.get(DBReceiverDistributionRecord::senderPartyId.name).`in`(initiatorPartyIds))) + predicates.add(criteriaBuilder.and(compositeKey.get(PersistentKey::peerPartyId.name).`in`(initiatorPartyIds))) } criteriaQuery.where(*predicates.toTypedArray()) // optionally order by timestamp @@ -274,13 +316,14 @@ class DBTransactionStorageLedgerRecovery(private val database: CordaPersistence, criteriaQuery.orderBy(orderCriteria) } val results = session.createQuery(criteriaQuery).stream() - results.map { it.toReceiverDistributionRecord(cryptoService) }.toList() + results.toList() } } } // TO DO: https://r3-cev.atlassian.net/browse/ENT-9876 -private fun CryptoService.decrypt(bytes: ByteArray): ByteArray { +@VisibleForTesting +fun CryptoService.decrypt(bytes: ByteArray): ByteArray { return bytes } @@ -289,6 +332,18 @@ fun CryptoService.encrypt(bytes: ByteArray): ByteArray { return bytes } +@CordaSerializable +class DistributionRecords( + val senderRecords: List = emptyList(), + val receiverRecords: List = emptyList() +) { + init { + assert(senderRecords.isNotEmpty() || receiverRecords.isNotEmpty()) { "Must set senderRecords or receiverRecords or both." } + } + + val size = senderRecords.size + receiverRecords.size +} + @CordaSerializable open class DistributionRecord( open val txId: SecureHash, @@ -310,7 +365,6 @@ data class ReceiverDistributionRecord( val initiatorPartyId: Long, // CordaX500Name hashCode() val peersToStatesToRecord: Map, // CordaX500Name hashCode() -> StatesToRecord override val statesToRecord: StatesToRecord, - val senderStatesToRecord: StatesToRecord, override val timestamp: Instant ) : DistributionRecord(txId, statesToRecord, timestamp) diff --git a/node/src/main/resources/migration/node-core.changelog-v25.xml b/node/src/main/resources/migration/node-core.changelog-v25.xml index 0a81edd870..a199a65df8 100644 --- a/node/src/main/resources/migration/node-core.changelog-v25.xml +++ b/node/src/main/resources/migration/node-core.changelog-v25.xml @@ -12,16 +12,16 @@ - + - + - + @@ -31,54 +31,35 @@ - - - - - - - - - - + - + - + - - - - - - - - - - - - @@ -94,15 +75,15 @@ - - + - - + diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecoveryTests.kt b/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecoveryTests.kt index 2bc406601c..39836dfdd1 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecoveryTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecoveryTests.kt @@ -109,6 +109,21 @@ class DBTransactionStorageLedgerRecoveryTests { val timeWindow = RecoveryTimeWindow(fromTime = now().minus(1, ChronoUnit.DAYS)) val results = transactionRecovery.querySenderDistributionRecords(timeWindow, excludingTxnIds = setOf(transaction1.id)) assertEquals(1, results.size) + assertEquals(transaction2.id.toString(), results[0].txId) + } + + @Test(timeout = 300_000) + fun `query local ledger for transactions within timeWindow and for given peers`() { + val transaction1 = newTransaction() + transactionRecovery.addUnnotarisedTransaction(transaction1) + transactionRecovery.addSenderTransactionRecoveryMetadata(transaction1.id, TransactionMetadata(ALICE_NAME, DistributionList(ALL_VISIBLE, mapOf(BOB_NAME to ONLY_RELEVANT)))) + val transaction2 = newTransaction() + transactionRecovery.addUnnotarisedTransaction(transaction2) + transactionRecovery.addSenderTransactionRecoveryMetadata(transaction2.id, TransactionMetadata(ALICE_NAME, DistributionList(ALL_VISIBLE, mapOf(CHARLIE_NAME to ONLY_RELEVANT)))) + val timeWindow = RecoveryTimeWindow(fromTime = now().minus(1, ChronoUnit.DAYS)) + val results = transactionRecovery.querySenderDistributionRecords(timeWindow, peers = setOf(CHARLIE_NAME)) + assertEquals(1, results.size) + assertEquals(transaction2.id.toString(), results[0].txId) } @Test(timeout = 300_000) @@ -125,13 +140,13 @@ class DBTransactionStorageLedgerRecoveryTests { val timeWindow = RecoveryTimeWindow(fromTime = now().minus(1, ChronoUnit.DAYS)) transactionRecovery.queryDistributionRecords(timeWindow, recordType = DistributionRecordType.SENDER).let { assertEquals(1, it.size) - assertEquals(BOB_NAME.hashCode().toLong(), (it[0] as SenderDistributionRecord).peerPartyId) - assertEquals(ALL_VISIBLE, (it[0] as SenderDistributionRecord).statesToRecord) + assertEquals(BOB_NAME.hashCode().toLong(), it.senderRecords[0].compositeKey.peerPartyId) + assertEquals(ALL_VISIBLE, it.senderRecords[0].statesToRecord) } transactionRecovery.queryDistributionRecords(timeWindow, recordType = DistributionRecordType.RECEIVER).let { assertEquals(1, it.size) - assertEquals(BOB_NAME.hashCode().toLong(), (it[0] as ReceiverDistributionRecord).initiatorPartyId) - assertEquals(ALL_VISIBLE, (it[0] as ReceiverDistributionRecord).statesToRecord) + assertEquals(BOB_NAME.hashCode().toLong(), it.receiverRecords[0].compositeKey.peerPartyId) + assertEquals(ALL_VISIBLE, (transactionRecovery.decrypt(it.receiverRecords[0].distributionList).peerHashToStatesToRecord.map { it.value }[0])) } val resultsAll = transactionRecovery.queryDistributionRecords(timeWindow, recordType = DistributionRecordType.ALL) assertEquals(2, resultsAll.size) @@ -192,9 +207,9 @@ class DBTransactionStorageLedgerRecoveryTests { val timeWindow = RecoveryTimeWindow(fromTime = now().minus(1, ChronoUnit.DAYS)) transactionRecovery.queryReceiverDistributionRecords(timeWindow, initiators = setOf(ALICE_NAME)).let { assertEquals(3, it.size) - assertEquals(it[0].statesToRecord, ALL_VISIBLE) - assertEquals(it[1].statesToRecord, ONLY_RELEVANT) - assertEquals(it[2].statesToRecord, NONE) + assertEquals(transactionRecovery.decrypt(it[0].distributionList).peerHashToStatesToRecord.map { it.value }[0], ALL_VISIBLE) + assertEquals(transactionRecovery.decrypt(it[1].distributionList).peerHashToStatesToRecord.map { it.value }[0], ONLY_RELEVANT) + assertEquals(transactionRecovery.decrypt(it[2].distributionList).peerHashToStatesToRecord.map { it.value }[0], NONE) } assertEquals(1, transactionRecovery.queryReceiverDistributionRecords(timeWindow, initiators = setOf(BOB_NAME)).size) assertEquals(1, transactionRecovery.queryReceiverDistributionRecords(timeWindow, initiators = setOf(CHARLIE_NAME)).size) @@ -229,8 +244,8 @@ class DBTransactionStorageLedgerRecoveryTests { DistributionList(ONLY_RELEVANT, mapOf(BOB_NAME to ALL_VISIBLE)).toWire()) assertEquals(IN_FLIGHT, readTransactionFromDB(receiverTransaction.id).status) readReceiverDistributionRecordFromDB(receiverTransaction.id).let { - assertEquals(ALL_VISIBLE, it.statesToRecord) - assertEquals(ONLY_RELEVANT, it.senderStatesToRecord) + assertEquals(ONLY_RELEVANT, it.statesToRecord) + assertEquals(ALL_VISIBLE, it.peersToStatesToRecord.map { it.value }[0]) assertEquals(ALICE_NAME, partyInfoCache.getCordaX500NameByPartyId(it.initiatorPartyId)) assertEquals(setOf(BOB_NAME), it.peersToStatesToRecord.map { (peer, _) -> partyInfoCache.getCordaX500NameByPartyId(peer) }.toSet() ) } @@ -245,7 +260,7 @@ class DBTransactionStorageLedgerRecoveryTests { assertEquals(VERIFIED, readTransactionFromDB(transaction.id).status) readSenderDistributionRecordFromDB(transaction.id).apply { assertEquals(1, this.size) - assertEquals(ONLY_RELEVANT, this[0].statesToRecord) + assertEquals(ALL_VISIBLE, this[0].statesToRecord) } } @@ -377,3 +392,7 @@ class DBTransactionStorageLedgerRecoveryTests { return cryptoService.encrypt(hashedDistributionList.serialize()) } } + +internal fun DBTransactionStorageLedgerRecovery.decrypt(distributionList: ByteArray): HashedDistributionList { + return HashedDistributionList.deserialize(this.cryptoService.decrypt(distributionList)) +} From d2029b3e0c303b1bd949b9e3361e19e7863f3621 Mon Sep 17 00:00:00 2001 From: Jose Coll Date: Tue, 15 Aug 2023 15:32:54 +0100 Subject: [PATCH 054/126] ENT-10290 Create Enterprise Aliases for all new Recovery Flows (#7440) --- .../corda/core/flows/FinalityRecoveryFlow.kt | 97 +++++++++++++++++++ .../net/corda/core/flows/LedgerRecoverFlow.kt | 85 ++++++++++++++++ 2 files changed, 182 insertions(+) create mode 100644 core/src/main/kotlin/net/corda/core/flows/FinalityRecoveryFlow.kt create mode 100644 core/src/main/kotlin/net/corda/core/flows/LedgerRecoverFlow.kt diff --git a/core/src/main/kotlin/net/corda/core/flows/FinalityRecoveryFlow.kt b/core/src/main/kotlin/net/corda/core/flows/FinalityRecoveryFlow.kt new file mode 100644 index 0000000000..e31f5b4fa8 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/flows/FinalityRecoveryFlow.kt @@ -0,0 +1,97 @@ +package net.corda.core.flows + +import co.paralleluniverse.fibers.Suspendable +import net.corda.core.CordaInternal +import net.corda.core.crypto.SecureHash +import net.corda.core.flows.FinalityFlow.Companion.tracker +import net.corda.core.identity.CordaX500Name +import net.corda.core.serialization.CordaSerializable +import net.corda.core.utilities.ProgressTracker +import java.time.Instant + +/** + * TWO_PHASE_FINALITY Recovery Flow + * This flow is exposed via the Core API for use by any CorDapp but its implementation is available in Enterprise only. + */ +@StartableByRPC +@InitiatingFlow +class FinalityRecoveryFlow( + private val txIds: Collection = emptySet(), + private val flowIds: Collection = emptySet(), + private val matchingCriteria: FlowRecoveryQuery? = null, + private val forceRecover: Boolean = false, + private val recoverAll: Boolean = false, + override val progressTracker: ProgressTracker = ProgressTracker()) : FlowLogic>() { + + @CordaInternal + data class ExtraConstructorArgs(val txIds: Collection, + val flowIds: Collection, + val matchingCriteria: FlowRecoveryQuery?, + val forceRecover: Boolean, + val recoverAll: Boolean) + @CordaInternal + fun getExtraConstructorArgs() = ExtraConstructorArgs(txIds, flowIds, matchingCriteria, forceRecover, recoverAll) + + constructor(txId: SecureHash, forceRecover: Boolean = false) : this(setOf(txId), forceRecover) + constructor(txIds: Collection, forceRecover: Boolean = false, recoverAll: Boolean = false) : this(txIds, emptySet(), null, forceRecover, recoverAll, tracker()) + constructor(flowId: StateMachineRunId, forceRecover: Boolean = false) : this(emptySet(), setOf(flowId), null, forceRecover) + constructor(flowIds: Collection, forceRecover: Boolean = false) : this(emptySet(), flowIds, null, forceRecover, false, tracker()) + constructor(recoverAll: Boolean, forceRecover: Boolean = false) : this(emptySet(), emptySet(), null, forceRecover, recoverAll, tracker()) + constructor(matchingCriteria: FlowRecoveryQuery, forceRecover: Boolean = false) : this(emptySet(), emptySet(), matchingCriteria, forceRecover, false, tracker()) + + @Suspendable + @Throws(FlowRecoveryException::class) + override fun call(): Map { + throw NotImplementedError("Enterprise only feature") + } +} + +@CordaSerializable +class FlowRecoveryException(message: String, cause: Throwable? = null) : FlowException(message, cause) { + constructor(txnId: SecureHash, message: String, cause: Throwable? = null) : this("Flow recovery failed for transaction $txnId: $message", cause) +} + +@CordaSerializable +data class FlowRecoveryQuery( + val timeframe: FlowTimeWindow? = null, + val initiatedBy: CordaX500Name? = null, + val counterParties: List? = null) { + init { + require(timeframe != null || initiatedBy != null || counterParties != null) { + "Must specify at least one recovery criteria" + } + } +} + +@CordaSerializable +data class FlowTimeWindow(val fromTime: Instant? = null, val untilTime: Instant? = null) { + + init { + if (fromTime == null && untilTime == null) + throw IllegalArgumentException("Must specify one or both of fromTime or/and untilTime") + fromTime?.let { startTime -> + untilTime?.let { endTime -> + if (endTime < startTime) { + throw IllegalArgumentException(FlowTimeWindow::fromTime.name + " must be before or equal to " + FlowTimeWindow::untilTime.name) + } + } + } + } + + companion object { + @JvmStatic + fun between(fromTime: Instant, untilTime: Instant): FlowTimeWindow { + return FlowTimeWindow(fromTime, untilTime) + } + + @JvmStatic + fun fromOnly(fromTime: Instant): FlowTimeWindow { + return FlowTimeWindow(fromTime = fromTime) + } + + @JvmStatic + fun untilOnly(untilTime: Instant): FlowTimeWindow { + return FlowTimeWindow(untilTime = untilTime) + } + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/flows/LedgerRecoverFlow.kt b/core/src/main/kotlin/net/corda/core/flows/LedgerRecoverFlow.kt new file mode 100644 index 0000000000..f9d7353ed4 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/flows/LedgerRecoverFlow.kt @@ -0,0 +1,85 @@ +package net.corda.core.flows + +import co.paralleluniverse.fibers.Suspendable +import net.corda.core.CordaInternal +import net.corda.core.crypto.SecureHash +import net.corda.core.identity.CordaX500Name +import net.corda.core.identity.Party +import net.corda.core.serialization.CordaSerializable +import net.corda.core.utilities.ProgressTracker + +/** + * Ledger Recovery Flow (available in Enterprise only). + */ +@StartableByRPC +@InitiatingFlow +class LedgerRecoveryFlow( + private val recoveryPeers: Collection, + private val timeWindow: RecoveryTimeWindow, + private val useAllNetworkNodes: Boolean = false, + private val transactionRole: TransactionRole = TransactionRole.ALL, + private val dryRun: Boolean = false, + private val optimisticInitiatorRecovery: Boolean = false, + override val progressTracker: ProgressTracker = ProgressTracker()) : FlowLogic>() { + + @CordaInternal + data class ExtraConstructorArgs(val recoveryPeers: Collection, + val timeWindow: RecoveryTimeWindow, + val useAllNetworkNodes: Boolean, + val transactionRole: TransactionRole, + val dryRun: Boolean, + val optimisticInitiatorRecovery: Boolean) + @CordaInternal + fun getExtraConstructorArgs() = ExtraConstructorArgs(recoveryPeers, timeWindow, useAllNetworkNodes, transactionRole, dryRun, optimisticInitiatorRecovery) + + // unused constructors added to facilitate Node Shell command invocation + constructor(recoveryPeer: Party, timeWindow: RecoveryTimeWindow) : this(setOf(recoveryPeer), timeWindow, false, TransactionRole.ALL, false, false) + constructor(recoveryPeer: Party, timeWindow: RecoveryTimeWindow, dryRun: Boolean) : this(setOf(recoveryPeer), timeWindow, false, TransactionRole.ALL, dryRun, false) + + constructor(timeWindow: RecoveryTimeWindow, dryRun: Boolean) : this(emptySet(), timeWindow, false, TransactionRole.ALL, dryRun, false) + constructor(timeWindow: RecoveryTimeWindow, dryRun: Boolean, optimisticInitiatorRecovery: Boolean) : this(emptySet(), timeWindow, false, TransactionRole.ALL, dryRun, optimisticInitiatorRecovery) + constructor(recoveryPeers: Collection, timeWindow: RecoveryTimeWindow, dryRun: Boolean) : this(recoveryPeers, timeWindow, false, TransactionRole.ALL, dryRun, false) + constructor(recoveryPeers: Collection, timeWindow: RecoveryTimeWindow, dryRun: Boolean, optimisticInitiatorRecovery: Boolean) : this(recoveryPeers, timeWindow, false, TransactionRole.ALL, dryRun, optimisticInitiatorRecovery) + + @Suspendable + @Throws(LedgerRecoveryException::class) + override fun call(): Map { + throw NotImplementedError("Enterprise only feature") + } +} + +@InitiatedBy(LedgerRecoveryFlow::class) +class ReceiveLedgerRecoveryFlow constructor(private val otherSideSession: FlowSession) : FlowLogic() { + @Suspendable + override fun call() { + throw NotImplementedError("Enterprise only feature") + } +} + +@CordaSerializable +class LedgerRecoveryException(message: String) : FlowException("Ledger recovery failed: $message") + +/** + * This specifies which type of transactions to recover based on the transaction role of the recovering node + */ +@CordaSerializable +enum class TransactionRole { + ALL, + INITIATOR, // only recover transactions that I initiated + PEER, // only recover transactions where I am a participant on a transaction + OBSERVER, // only recover transactions where I am an observer (but not participant) to a transaction + PEER_AND_OBSERVER // recovery transactions where I am either participant or observer +} + +@CordaSerializable +data class RecoveryResult( + val transactionId: SecureHash, + val recoveryPeer: CordaX500Name, + val transactionRole: TransactionRole, // what role did I play in this transaction + val synchronised: Boolean, // whether the transaction was successfully synchronised (will always be false when dryRun option specified) + val synchronisedInitiated: Boolean = false, // only attempted if [optimisticInitiatorRecovery] option set to true and [TransactionRecoveryType.INITIATOR] + val failureCause: String? = null // reason why a transaction failed to synchronise +) + + + From 492373d1802e6b02a19b7b3171670d4db8a85565 Mon Sep 17 00:00:00 2001 From: Jose Coll Date: Wed, 16 Aug 2023 17:02:58 +0100 Subject: [PATCH 055/126] Introduction of Sender and Receiver Distribution Lists to support receiver self-recovery mode. --- .../net/corda/core/flows/FlowTransaction.kt | 22 ++++++--- .../corda/core/flows/SendTransactionFlow.kt | 7 +-- .../DBTransactionStorageLedgerRecovery.kt | 10 ++-- ...DBTransactionStorageLedgerRecoveryTests.kt | 48 +++++++++---------- 4 files changed, 50 insertions(+), 37 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/flows/FlowTransaction.kt b/core/src/main/kotlin/net/corda/core/flows/FlowTransaction.kt index 05539f7480..b213c6dbd0 100644 --- a/core/src/main/kotlin/net/corda/core/flows/FlowTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/flows/FlowTransaction.kt @@ -23,15 +23,25 @@ data class FlowTransactionInfo( @CordaSerializable data class TransactionMetadata( - val initiator: CordaX500Name, - val distributionList: DistributionList + val initiator: CordaX500Name, + val distributionList: DistributionList ) @CordaSerializable -data class DistributionList( - val senderStatesToRecord: StatesToRecord, - val peersToStatesToRecord: Map -) +sealed class DistributionList { + + @CordaSerializable + data class SenderDistributionList( + val senderStatesToRecord: StatesToRecord, + val peersToStatesToRecord: Map + ) : DistributionList() + + @CordaSerializable + data class ReceiverDistributionList( + val opaqueData: ByteArray, // decipherable only by sender + val receiverStatesToRecord: StatesToRecord // inferred or actual + ) : DistributionList() +} @CordaSerializable enum class TransactionStatus { diff --git a/core/src/main/kotlin/net/corda/core/flows/SendTransactionFlow.kt b/core/src/main/kotlin/net/corda/core/flows/SendTransactionFlow.kt index 93ed7d0c97..07247b3b46 100644 --- a/core/src/main/kotlin/net/corda/core/flows/SendTransactionFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/SendTransactionFlow.kt @@ -23,6 +23,7 @@ import kotlin.collections.map import kotlin.collections.mutableSetOf import kotlin.collections.plus import kotlin.collections.toSet +import net.corda.core.flows.DistributionList.SenderDistributionList /** * In the words of Matt working code is more important then pretty code. This class that contains code that may @@ -98,9 +99,9 @@ open class SendTransactionFlow(val stx: SignedTransaction, fun makeMetaData(stx: SignedTransaction, recordMetaDataEvenIfNotFullySigned: Boolean, senderStatesToRecord: StatesToRecord, participantSessions: Set, observerSessions: Set): TransactionMetadata? { return if (recordMetaDataEvenIfNotFullySigned || isFullySigned(stx)) TransactionMetadata(DUMMY_PARTICIPANT_NAME, - DistributionList(senderStatesToRecord, - (participantSessions.map { it.counterparty.name to StatesToRecord.ONLY_RELEVANT}).toMap() + - (observerSessions.map { it.counterparty.name to StatesToRecord.ALL_VISIBLE}).toMap())) + SenderDistributionList(senderStatesToRecord, + (participantSessions.map { it.counterparty.name to StatesToRecord.ONLY_RELEVANT }).toMap() + + (observerSessions.map { it.counterparty.name to StatesToRecord.ALL_VISIBLE }).toMap())) else null } diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecovery.kt b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecovery.kt index 6c101a8401..4492c27724 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecovery.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecovery.kt @@ -1,6 +1,7 @@ package net.corda.node.services.persistence import net.corda.core.crypto.SecureHash +import net.corda.core.flows.DistributionList.SenderDistributionList import net.corda.core.flows.RecoveryTimeWindow import net.corda.core.flows.TransactionMetadata import net.corda.core.identity.CordaX500Name @@ -139,21 +140,22 @@ class DBTransactionStorageLedgerRecovery(private val database: CordaPersistence, override fun addSenderTransactionRecoveryMetadata(txId: SecureHash, metadata: TransactionMetadata): ByteArray { return database.transaction { val senderRecordingTimestamp = clock.instant() - for (peer in metadata.distributionList.peersToStatesToRecord.keys) { + val distributionList = metadata.distributionList as? SenderDistributionList ?: throw IllegalStateException("Expecting SenderDistributionList") + for (peer in distributionList.peersToStatesToRecord.keys) { val senderDistributionRecord = DBSenderDistributionRecord( PersistentKey(Key(senderRecordingTimestamp)), txId.toString(), partyInfoCache.getPartyIdByCordaX500Name(peer), - metadata.distributionList.senderStatesToRecord + distributionList.senderStatesToRecord ) session.save(senderDistributionRecord) } - val hashedPeersToStatesToRecord = metadata.distributionList.peersToStatesToRecord.mapKeys { (peer) -> + val hashedPeersToStatesToRecord = distributionList.peersToStatesToRecord.mapKeys { (peer) -> partyInfoCache.getPartyIdByCordaX500Name(peer) } val hashedDistributionList = HashedDistributionList( - metadata.distributionList.senderStatesToRecord, + distributionList.senderStatesToRecord, hashedPeersToStatesToRecord, HashedDistributionList.PublicHeader(senderRecordingTimestamp) ) diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecoveryTests.kt b/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecoveryTests.kt index e38079536d..9a0a8b194d 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecoveryTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecoveryTests.kt @@ -6,7 +6,7 @@ import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SignableData import net.corda.core.crypto.SignatureMetadata import net.corda.core.crypto.sign -import net.corda.core.flows.DistributionList +import net.corda.core.flows.DistributionList.SenderDistributionList import net.corda.core.flows.RecoveryTimeWindow import net.corda.core.flows.TransactionMetadata import net.corda.core.node.NodeInfo @@ -86,7 +86,7 @@ class DBTransactionStorageLedgerRecoveryTests { val beforeFirstTxn = now() val txn = newTransaction() transactionRecovery.addUnnotarisedTransaction(txn) - transactionRecovery.addSenderTransactionRecoveryMetadata(txn.id, TransactionMetadata(ALICE_NAME, DistributionList(ALL_VISIBLE, mapOf(BOB_NAME to ONLY_RELEVANT)))) + transactionRecovery.addSenderTransactionRecoveryMetadata(txn.id, TransactionMetadata(ALICE_NAME, SenderDistributionList(ALL_VISIBLE, mapOf(BOB_NAME to ONLY_RELEVANT)))) val timeWindow = RecoveryTimeWindow(fromTime = beforeFirstTxn, untilTime = beforeFirstTxn.plus(1, ChronoUnit.MINUTES)) val results = transactionRecovery.querySenderDistributionRecords(timeWindow) @@ -95,7 +95,7 @@ class DBTransactionStorageLedgerRecoveryTests { val afterFirstTxn = now() val txn2 = newTransaction() transactionRecovery.addUnnotarisedTransaction(txn2) - transactionRecovery.addSenderTransactionRecoveryMetadata(txn2.id, TransactionMetadata(ALICE_NAME, DistributionList(ONLY_RELEVANT, mapOf(CHARLIE_NAME to ONLY_RELEVANT)))) + transactionRecovery.addSenderTransactionRecoveryMetadata(txn2.id, TransactionMetadata(ALICE_NAME, SenderDistributionList(ONLY_RELEVANT, mapOf(CHARLIE_NAME to ONLY_RELEVANT)))) assertEquals(2, transactionRecovery.querySenderDistributionRecords(timeWindow).size) assertEquals(1, transactionRecovery.querySenderDistributionRecords(RecoveryTimeWindow(fromTime = afterFirstTxn)).size) } @@ -104,10 +104,10 @@ class DBTransactionStorageLedgerRecoveryTests { fun `query local ledger for transactions within timeWindow and excluding remoteTransactionIds`() { val transaction1 = newTransaction() transactionRecovery.addUnnotarisedTransaction(transaction1) - transactionRecovery.addSenderTransactionRecoveryMetadata(transaction1.id, TransactionMetadata(ALICE_NAME, DistributionList(ALL_VISIBLE, mapOf(BOB_NAME to ONLY_RELEVANT)))) + transactionRecovery.addSenderTransactionRecoveryMetadata(transaction1.id, TransactionMetadata(ALICE_NAME, SenderDistributionList(ALL_VISIBLE, mapOf(BOB_NAME to ONLY_RELEVANT)))) val transaction2 = newTransaction() transactionRecovery.addUnnotarisedTransaction(transaction2) - transactionRecovery.addSenderTransactionRecoveryMetadata(transaction2.id, TransactionMetadata(ALICE_NAME, DistributionList(ALL_VISIBLE, mapOf(BOB_NAME to ONLY_RELEVANT)))) + transactionRecovery.addSenderTransactionRecoveryMetadata(transaction2.id, TransactionMetadata(ALICE_NAME, SenderDistributionList(ALL_VISIBLE, mapOf(BOB_NAME to ONLY_RELEVANT)))) val timeWindow = RecoveryTimeWindow(fromTime = now().minus(1, ChronoUnit.DAYS)) val results = transactionRecovery.querySenderDistributionRecords(timeWindow, excludingTxnIds = setOf(transaction1.id)) assertEquals(1, results.size) @@ -118,12 +118,12 @@ class DBTransactionStorageLedgerRecoveryTests { val transaction1 = newTransaction() // sender txn transactionRecovery.addUnnotarisedTransaction(transaction1) - transactionRecovery.addSenderTransactionRecoveryMetadata(transaction1.id, TransactionMetadata(ALICE_NAME, DistributionList(ALL_VISIBLE, mapOf(BOB_NAME to ALL_VISIBLE)))) + transactionRecovery.addSenderTransactionRecoveryMetadata(transaction1.id, TransactionMetadata(ALICE_NAME, SenderDistributionList(ALL_VISIBLE, mapOf(BOB_NAME to ALL_VISIBLE)))) val transaction2 = newTransaction() // receiver txn transactionRecovery.addUnnotarisedTransaction(transaction2) transactionRecovery.addReceiverTransactionRecoveryMetadata(transaction2.id, BOB_NAME, ALICE_NAME, ALL_VISIBLE, - DistributionList(ONLY_RELEVANT, mapOf(ALICE_NAME to ALL_VISIBLE)).toWire()) + SenderDistributionList(ONLY_RELEVANT, mapOf(ALICE_NAME to ALL_VISIBLE)).toWire()) val timeWindow = RecoveryTimeWindow(fromTime = now().minus(1, ChronoUnit.DAYS)) transactionRecovery.queryDistributionRecords(timeWindow, recordType = DistributionRecordType.SENDER).let { assertEquals(1, it.size) @@ -143,19 +143,19 @@ class DBTransactionStorageLedgerRecoveryTests { fun `query for sender distribution records by peers`() { val txn1 = newTransaction() transactionRecovery.addUnnotarisedTransaction(txn1) - transactionRecovery.addSenderTransactionRecoveryMetadata(txn1.id, TransactionMetadata(ALICE_NAME, DistributionList(ALL_VISIBLE, mapOf(BOB_NAME to ALL_VISIBLE)))) + transactionRecovery.addSenderTransactionRecoveryMetadata(txn1.id, TransactionMetadata(ALICE_NAME, SenderDistributionList(ALL_VISIBLE, mapOf(BOB_NAME to ALL_VISIBLE)))) val txn2 = newTransaction() transactionRecovery.addUnnotarisedTransaction(txn2) - transactionRecovery.addSenderTransactionRecoveryMetadata(txn2.id, TransactionMetadata(ALICE_NAME, DistributionList(ONLY_RELEVANT, mapOf(CHARLIE_NAME to ONLY_RELEVANT)))) + transactionRecovery.addSenderTransactionRecoveryMetadata(txn2.id, TransactionMetadata(ALICE_NAME, SenderDistributionList(ONLY_RELEVANT, mapOf(CHARLIE_NAME to ONLY_RELEVANT)))) val txn3 = newTransaction() transactionRecovery.addUnnotarisedTransaction(txn3) - transactionRecovery.addSenderTransactionRecoveryMetadata(txn3.id, TransactionMetadata(ALICE_NAME, DistributionList(ONLY_RELEVANT, mapOf(BOB_NAME to ONLY_RELEVANT, CHARLIE_NAME to ALL_VISIBLE)))) + transactionRecovery.addSenderTransactionRecoveryMetadata(txn3.id, TransactionMetadata(ALICE_NAME, SenderDistributionList(ONLY_RELEVANT, mapOf(BOB_NAME to ONLY_RELEVANT, CHARLIE_NAME to ALL_VISIBLE)))) val txn4 = newTransaction() transactionRecovery.addUnnotarisedTransaction(txn4) - transactionRecovery.addSenderTransactionRecoveryMetadata(txn4.id, TransactionMetadata(BOB_NAME, DistributionList(ONLY_RELEVANT, mapOf(ALICE_NAME to ONLY_RELEVANT)))) + transactionRecovery.addSenderTransactionRecoveryMetadata(txn4.id, TransactionMetadata(BOB_NAME, SenderDistributionList(ONLY_RELEVANT, mapOf(ALICE_NAME to ONLY_RELEVANT)))) val txn5 = newTransaction() transactionRecovery.addUnnotarisedTransaction(txn5) - transactionRecovery.addSenderTransactionRecoveryMetadata(txn5.id, TransactionMetadata(CHARLIE_NAME, DistributionList(ONLY_RELEVANT, emptyMap()))) + transactionRecovery.addSenderTransactionRecoveryMetadata(txn5.id, TransactionMetadata(CHARLIE_NAME, SenderDistributionList(ONLY_RELEVANT, emptyMap()))) assertEquals(5, readSenderDistributionRecordFromDB().size) val timeWindow = RecoveryTimeWindow(fromTime = now().minus(1, ChronoUnit.DAYS)) @@ -173,23 +173,23 @@ class DBTransactionStorageLedgerRecoveryTests { val txn1 = newTransaction() transactionRecovery.addUnnotarisedTransaction(txn1) transactionRecovery.addReceiverTransactionRecoveryMetadata(txn1.id, ALICE_NAME, BOB_NAME, ALL_VISIBLE, - DistributionList(ONLY_RELEVANT, mapOf(BOB_NAME to ALL_VISIBLE, CHARLIE_NAME to ALL_VISIBLE)).toWire()) + SenderDistributionList(ONLY_RELEVANT, mapOf(BOB_NAME to ALL_VISIBLE, CHARLIE_NAME to ALL_VISIBLE)).toWire()) val txn2 = newTransaction() transactionRecovery.addUnnotarisedTransaction(txn2) transactionRecovery.addReceiverTransactionRecoveryMetadata(txn2.id, ALICE_NAME, BOB_NAME, ONLY_RELEVANT, - DistributionList(ONLY_RELEVANT, mapOf(BOB_NAME to ONLY_RELEVANT)).toWire()) + SenderDistributionList(ONLY_RELEVANT, mapOf(BOB_NAME to ONLY_RELEVANT)).toWire()) val txn3 = newTransaction() transactionRecovery.addUnnotarisedTransaction(txn3) transactionRecovery.addReceiverTransactionRecoveryMetadata(txn3.id, ALICE_NAME, CHARLIE_NAME, NONE, - DistributionList(ONLY_RELEVANT, mapOf(CHARLIE_NAME to NONE)).toWire()) + SenderDistributionList(ONLY_RELEVANT, mapOf(CHARLIE_NAME to NONE)).toWire()) val txn4 = newTransaction() transactionRecovery.addUnnotarisedTransaction(txn4) transactionRecovery.addReceiverTransactionRecoveryMetadata(txn4.id, BOB_NAME, ALICE_NAME, ONLY_RELEVANT, - DistributionList(ONLY_RELEVANT, mapOf(ALICE_NAME to ALL_VISIBLE)).toWire()) + SenderDistributionList(ONLY_RELEVANT, mapOf(ALICE_NAME to ALL_VISIBLE)).toWire()) val txn5 = newTransaction() transactionRecovery.addUnnotarisedTransaction(txn5) transactionRecovery.addReceiverTransactionRecoveryMetadata(txn5.id, CHARLIE_NAME, BOB_NAME, ONLY_RELEVANT, - DistributionList(ONLY_RELEVANT, mapOf(BOB_NAME to ONLY_RELEVANT)).toWire()) + SenderDistributionList(ONLY_RELEVANT, mapOf(BOB_NAME to ONLY_RELEVANT)).toWire()) val timeWindow = RecoveryTimeWindow(fromTime = now().minus(1, ChronoUnit.DAYS)) transactionRecovery.queryReceiverDistributionRecords(timeWindow, initiators = setOf(ALICE_NAME)).let { @@ -207,7 +207,7 @@ class DBTransactionStorageLedgerRecoveryTests { fun `transaction without peers does not store recovery metadata in database`() { val senderTransaction = newTransaction() transactionRecovery.addUnnotarisedTransaction(senderTransaction) - transactionRecovery.addSenderTransactionRecoveryMetadata(senderTransaction.id, TransactionMetadata(ALICE_NAME, DistributionList(ONLY_RELEVANT, emptyMap()))) + transactionRecovery.addSenderTransactionRecoveryMetadata(senderTransaction.id, TransactionMetadata(ALICE_NAME, SenderDistributionList(ONLY_RELEVANT, emptyMap()))) assertEquals(IN_FLIGHT, readTransactionFromDB(senderTransaction.id).status) assertEquals(0, readSenderDistributionRecordFromDB(senderTransaction.id).size) } @@ -217,7 +217,7 @@ class DBTransactionStorageLedgerRecoveryTests { val senderTransaction = newTransaction() transactionRecovery.addUnnotarisedTransaction(senderTransaction) transactionRecovery.addSenderTransactionRecoveryMetadata(senderTransaction.id, - TransactionMetadata(ALICE_NAME, DistributionList(ALL_VISIBLE, mapOf(BOB_NAME to ALL_VISIBLE)))) + TransactionMetadata(ALICE_NAME, SenderDistributionList(ALL_VISIBLE, mapOf(BOB_NAME to ALL_VISIBLE)))) assertEquals(IN_FLIGHT, readTransactionFromDB(senderTransaction.id).status) readSenderDistributionRecordFromDB(senderTransaction.id).let { assertEquals(1, it.size) @@ -228,7 +228,7 @@ class DBTransactionStorageLedgerRecoveryTests { val receiverTransaction = newTransaction() transactionRecovery.addUnnotarisedTransaction(receiverTransaction) transactionRecovery.addReceiverTransactionRecoveryMetadata(receiverTransaction.id, ALICE_NAME, BOB_NAME, ALL_VISIBLE, - DistributionList(ONLY_RELEVANT, mapOf(BOB_NAME to ALL_VISIBLE)).toWire()) + SenderDistributionList(ONLY_RELEVANT, mapOf(BOB_NAME to ALL_VISIBLE)).toWire()) assertEquals(IN_FLIGHT, readTransactionFromDB(receiverTransaction.id).status) readReceiverDistributionRecordFromDB(receiverTransaction.id).let { assertEquals(ALL_VISIBLE, it.statesToRecord) @@ -243,7 +243,7 @@ class DBTransactionStorageLedgerRecoveryTests { val transaction = newTransaction(notarySig = false) transactionRecovery.finalizeTransaction(transaction) transactionRecovery.addSenderTransactionRecoveryMetadata(transaction.id, - TransactionMetadata(ALICE_NAME, DistributionList(ONLY_RELEVANT, mapOf(CHARLIE_NAME to ALL_VISIBLE)))) + TransactionMetadata(ALICE_NAME, SenderDistributionList(ONLY_RELEVANT, mapOf(CHARLIE_NAME to ALL_VISIBLE)))) assertEquals(VERIFIED, readTransactionFromDB(transaction.id).status) readSenderDistributionRecordFromDB(transaction.id).apply { assertEquals(1, this.size) @@ -256,7 +256,7 @@ class DBTransactionStorageLedgerRecoveryTests { val senderTransaction = newTransaction(notarySig = false) transactionRecovery.addUnnotarisedTransaction(senderTransaction) transactionRecovery.addReceiverTransactionRecoveryMetadata(senderTransaction.id, ALICE.name, BOB.name, ONLY_RELEVANT, - DistributionList(ONLY_RELEVANT, mapOf(BOB.name to ONLY_RELEVANT, CHARLIE_NAME to ONLY_RELEVANT)).toWire()) + SenderDistributionList(ONLY_RELEVANT, mapOf(BOB.name to ONLY_RELEVANT, CHARLIE_NAME to ONLY_RELEVANT)).toWire()) assertNull(transactionRecovery.getTransaction(senderTransaction.id)) assertEquals(IN_FLIGHT, readTransactionFromDB(senderTransaction.id).status) @@ -268,7 +268,7 @@ class DBTransactionStorageLedgerRecoveryTests { val receiverTransaction = newTransaction(notarySig = false) transactionRecovery.addUnnotarisedTransaction(receiverTransaction) transactionRecovery.addReceiverTransactionRecoveryMetadata(receiverTransaction.id, ALICE.name, BOB.name, ONLY_RELEVANT, - DistributionList(ONLY_RELEVANT, mapOf(BOB.name to ONLY_RELEVANT)).toWire()) + SenderDistributionList(ONLY_RELEVANT, mapOf(BOB.name to ONLY_RELEVANT)).toWire()) assertNull(transactionRecovery.getTransaction(receiverTransaction.id)) assertEquals(IN_FLIGHT, readTransactionFromDB(receiverTransaction.id).status) @@ -379,7 +379,7 @@ class DBTransactionStorageLedgerRecoveryTests { private fun notarySig(txId: SecureHash) = DUMMY_NOTARY.keyPair.sign(SignableData(txId, SignatureMetadata(1, Crypto.findSignatureScheme(DUMMY_NOTARY.publicKey).schemeNumberID))) - private fun DistributionList.toWire(): ByteArray { + private fun SenderDistributionList.toWire(): ByteArray { val hashedPeersToStatesToRecord = this.peersToStatesToRecord.mapKeys { (peer) -> partyInfoCache.getPartyIdByCordaX500Name(peer) } val hashedDistributionList = HashedDistributionList( this.senderStatesToRecord, From 9b7affa6b303a71376ff1a416df9e9f80d8d8fa2 Mon Sep 17 00:00:00 2001 From: Jose Coll Date: Wed, 16 Aug 2023 17:40:33 +0100 Subject: [PATCH 056/126] Fix compilation errors following merge. --- .../coretests/flows/FinalityFlowTests.kt | 3 -- .../DBTransactionStorageLedgerRecovery.kt | 51 +------------------ 2 files changed, 2 insertions(+), 52 deletions(-) diff --git a/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt b/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt index 277061deae..9d82164ccc 100644 --- a/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt +++ b/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt @@ -359,7 +359,6 @@ class FinalityFlowTests : WithFinality { } getReceiverRecoveryData(stx.id, bobNode, aliceNode).apply { assertEquals(StatesToRecord.ALL_VISIBLE, this?.statesToRecord) - assertEquals(StatesToRecord.ONLY_RELEVANT, this?.senderStatesToRecord) assertEquals(aliceNode.info.singleIdentity().name.hashCode().toLong(), this?.initiatorPartyId) assertEquals(mapOf(BOB_NAME.hashCode().toLong() to StatesToRecord.ALL_VISIBLE), this?.peersToStatesToRecord) } @@ -392,7 +391,6 @@ class FinalityFlowTests : WithFinality { } getReceiverRecoveryData(stx.id, bobNode, aliceNode).apply { assertEquals(StatesToRecord.ONLY_RELEVANT, this?.statesToRecord) - assertEquals(StatesToRecord.ONLY_RELEVANT, this?.senderStatesToRecord) assertEquals(aliceNode.info.singleIdentity().name.hashCode().toLong(), this?.initiatorPartyId) // note: Charlie assertion here is using the hinted StatesToRecord value passed to it from Alice assertEquals(mapOf(BOB_NAME.hashCode().toLong() to StatesToRecord.ONLY_RELEVANT, @@ -435,7 +433,6 @@ class FinalityFlowTests : WithFinality { } getReceiverRecoveryData(stx.id, bobNode, aliceNode).apply { assertEquals(StatesToRecord.ONLY_RELEVANT, this?.statesToRecord) - assertEquals(StatesToRecord.ONLY_RELEVANT, this?.senderStatesToRecord) assertEquals(aliceNode.info.singleIdentity().name.hashCode().toLong(), this?.initiatorPartyId) assertEquals(mapOf(BOB_NAME.hashCode().toLong() to StatesToRecord.ONLY_RELEVANT), this?.peersToStatesToRecord) } diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecovery.kt b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecovery.kt index 86cfdb66f5..ad234a6500 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecovery.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecovery.kt @@ -6,7 +6,6 @@ import net.corda.core.flows.RecoveryTimeWindow import net.corda.core.flows.TransactionMetadata import net.corda.core.identity.CordaX500Name import net.corda.core.internal.NamedCacheFactory -import net.corda.core.internal.VisibleForTesting import net.corda.core.node.StatesToRecord import net.corda.core.node.services.vault.Sort import net.corda.core.serialization.CordaSerializable @@ -164,31 +163,6 @@ class DBTransactionStorageLedgerRecovery(private val database: CordaPersistence, } } - fun createReceiverTransactionRecoverMetadata(txId: SecureHash, - senderPartyId: Long, - senderStatesToRecord: StatesToRecord, - senderRecords: List): List { - val senderRecordsByTimestampKey = senderRecords.groupBy { TimestampKey(it.compositeKey.timestamp, it.compositeKey.timestampDiscriminator) } - return senderRecordsByTimestampKey.map { - val hashedDistributionList = HashedDistributionList( - senderStatesToRecord = senderStatesToRecord, - peerHashToStatesToRecord = senderRecords.map { it.compositeKey.peerPartyId to it.statesToRecord }.toMap(), - senderRecordedTimestamp = it.key.timestamp - ) - DBReceiverDistributionRecord( - compositeKey = PersistentKey(Key(TimestampKey(it.key.timestamp, it.key.timestampDiscriminator), senderPartyId)), - txId = txId.toString(), - distributionList = cryptoService.encrypt(hashedDistributionList.serialize()) - ) - } - } - - fun addSenderTransactionRecoveryMetadata(record: DBSenderDistributionRecord) { - return database.transaction { - session.save(record) - } - } - override fun addReceiverTransactionRecoveryMetadata(txId: SecureHash, sender: CordaX500Name, receiver: CordaX500Name, @@ -206,12 +180,6 @@ class DBTransactionStorageLedgerRecovery(private val database: CordaPersistence, } } - fun addReceiverTransactionRecoveryMetadata(record: DBReceiverDistributionRecord) { - return database.transaction { - session.save(record) - } - } - override fun removeUnnotarisedTransaction(id: SecureHash): Boolean { return database.transaction { super.removeUnnotarisedTransaction(id) @@ -281,20 +249,7 @@ class DBTransactionStorageLedgerRecovery(private val database: CordaPersistence, } criteriaQuery.orderBy(orderCriteria) } - session.createQuery(criteriaQuery).stream().use { results -> - results.map { it.toSenderDistributionRecord() }.toList() - } - } - } - - fun querySenderDistributionRecordsByTxId(txId: SecureHash): List { - return database.transaction { - val criteriaBuilder = session.criteriaBuilder - val criteriaQuery = criteriaBuilder.createQuery(DBSenderDistributionRecord::class.java) - val txnMetadata = criteriaQuery.from(DBSenderDistributionRecord::class.java) - criteriaQuery.where(criteriaBuilder.equal(txnMetadata.get(DBSenderDistributionRecord::txId.name), txId.toString())) - val results = session.createQuery(criteriaQuery).stream() - results.toList() + session.createQuery(criteriaQuery).stream().toList() } } @@ -331,9 +286,7 @@ class DBTransactionStorageLedgerRecovery(private val database: CordaPersistence, } criteriaQuery.orderBy(orderCriteria) } - session.createQuery(criteriaQuery).stream().use { results -> - results.map { it.toReceiverDistributionRecord(encryptionService) }.toList() - } + session.createQuery(criteriaQuery).stream().toList() } } } From f565232f36b2f37755890ee4557470c048c23b93 Mon Sep 17 00:00:00 2001 From: Jose Coll Date: Wed, 16 Aug 2023 18:05:18 +0100 Subject: [PATCH 057/126] Fix compilation errors following merge. --- .../DBTransactionStorageLedgerRecoveryTests.kt | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecoveryTests.kt b/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecoveryTests.kt index e0f628acc7..8f759838cb 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecoveryTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecoveryTests.kt @@ -118,10 +118,10 @@ class DBTransactionStorageLedgerRecoveryTests { fun `query local ledger for transactions within timeWindow and for given peers`() { val transaction1 = newTransaction() transactionRecovery.addUnnotarisedTransaction(transaction1) - transactionRecovery.addSenderTransactionRecoveryMetadata(transaction1.id, TransactionMetadata(ALICE_NAME, DistributionList(ALL_VISIBLE, mapOf(BOB_NAME to ONLY_RELEVANT)))) + transactionRecovery.addSenderTransactionRecoveryMetadata(transaction1.id, TransactionMetadata(ALICE_NAME, SenderDistributionList(ALL_VISIBLE, mapOf(BOB_NAME to ONLY_RELEVANT)))) val transaction2 = newTransaction() transactionRecovery.addUnnotarisedTransaction(transaction2) - transactionRecovery.addSenderTransactionRecoveryMetadata(transaction2.id, TransactionMetadata(ALICE_NAME, DistributionList(ALL_VISIBLE, mapOf(CHARLIE_NAME to ONLY_RELEVANT)))) + transactionRecovery.addSenderTransactionRecoveryMetadata(transaction2.id, TransactionMetadata(ALICE_NAME, SenderDistributionList(ALL_VISIBLE, mapOf(CHARLIE_NAME to ONLY_RELEVANT)))) val timeWindow = RecoveryTimeWindow(fromTime = now().minus(1, ChronoUnit.DAYS)) val results = transactionRecovery.querySenderDistributionRecords(timeWindow, peers = setOf(CHARLIE_NAME)) assertEquals(1, results.size) @@ -148,7 +148,7 @@ class DBTransactionStorageLedgerRecoveryTests { transactionRecovery.queryDistributionRecords(timeWindow, recordType = DistributionRecordType.RECEIVER).let { assertEquals(1, it.size) assertEquals(BOB_NAME.hashCode().toLong(), it.receiverRecords[0].compositeKey.peerPartyId) - assertEquals(ALL_VISIBLE, (transactionRecovery.decrypt(it.receiverRecords[0].distributionList).peerHashToStatesToRecord.map { it.value }[0])) + assertEquals(ALL_VISIBLE, (HashedDistributionList.decrypt(it.receiverRecords[0].distributionList, encryptionService)).peerHashToStatesToRecord.map { it.value }[0]) } val resultsAll = transactionRecovery.queryDistributionRecords(timeWindow, recordType = DistributionRecordType.ALL) assertEquals(2, resultsAll.size) @@ -209,9 +209,9 @@ class DBTransactionStorageLedgerRecoveryTests { val timeWindow = RecoveryTimeWindow(fromTime = now().minus(1, ChronoUnit.DAYS)) transactionRecovery.queryReceiverDistributionRecords(timeWindow, initiators = setOf(ALICE_NAME)).let { assertEquals(3, it.size) - assertEquals(transactionRecovery.decrypt(it[0].distributionList).peerHashToStatesToRecord.map { it.value }[0], ALL_VISIBLE) - assertEquals(transactionRecovery.decrypt(it[1].distributionList).peerHashToStatesToRecord.map { it.value }[0], ONLY_RELEVANT) - assertEquals(transactionRecovery.decrypt(it[2].distributionList).peerHashToStatesToRecord.map { it.value }[0], NONE) + assertEquals(HashedDistributionList.decrypt(it[0].distributionList, encryptionService).peerHashToStatesToRecord.map { it.value }[0], ALL_VISIBLE) + assertEquals(HashedDistributionList.decrypt(it[1].distributionList, encryptionService).peerHashToStatesToRecord.map { it.value }[0], ONLY_RELEVANT) + assertEquals(HashedDistributionList.decrypt(it[2].distributionList, encryptionService).peerHashToStatesToRecord.map { it.value }[0], NONE) } assertEquals(1, transactionRecovery.queryReceiverDistributionRecords(timeWindow, initiators = setOf(BOB_NAME)).size) assertEquals(1, transactionRecovery.queryReceiverDistributionRecords(timeWindow, initiators = setOf(CHARLIE_NAME)).size) @@ -405,6 +405,3 @@ class DBTransactionStorageLedgerRecoveryTests { } } -internal fun DBTransactionStorageLedgerRecovery.decrypt(distributionList: ByteArray): HashedDistributionList { - return HashedDistributionList.deserialize(this.cryptoService.decrypt(distributionList)) -} From 06e43eb9e2cb2e3cbed26bc3e21d3b5c8d2618bc Mon Sep 17 00:00:00 2001 From: Jose Coll Date: Thu, 17 Aug 2023 08:47:58 +0100 Subject: [PATCH 058/126] Fixes following merge. --- .ci/api-current.txt | 26 +++++++++++++++++-- .../DBTransactionStorageLedgerRecovery.kt | 8 +++--- .../migration/node-core.changelog-v25.xml | 3 +++ 3 files changed, 30 insertions(+), 7 deletions(-) diff --git a/.ci/api-current.txt b/.ci/api-current.txt index 200b056099..1233ed1555 100644 --- a/.ci/api-current.txt +++ b/.ci/api-current.txt @@ -2542,14 +2542,36 @@ public class net.corda.core.flows.DataVendingFlow extends net.corda.core.flows.F public interface net.corda.core.flows.Destination ## @CordaSerializable -public final class net.corda.core.flows.DistributionList extends java.lang.Object +public abstract class net.corda.core.flows.DistributionList extends java.lang.Object + public (kotlin.jvm.internal.DefaultConstructorMarker) +## +@CordaSerializable +public static final class net.corda.core.flows.DistributionList$ReceiverDistributionList extends net.corda.core.flows.DistributionList + public (byte[], net.corda.core.node.StatesToRecord) + @NotNull + public final byte[] component1() + @NotNull + public final net.corda.core.node.StatesToRecord component2() + @NotNull + public final net.corda.core.flows.DistributionList$ReceiverDistributionList copy(byte[], net.corda.core.node.StatesToRecord) + public boolean equals(Object) + @NotNull + public final byte[] getOpaqueData() + @NotNull + public final net.corda.core.node.StatesToRecord getReceiverStatesToRecord() + public int hashCode() + @NotNull + public String toString() +## +@CordaSerializable +public static final class net.corda.core.flows.DistributionList$SenderDistributionList extends net.corda.core.flows.DistributionList public (net.corda.core.node.StatesToRecord, java.util.Map) @NotNull public final net.corda.core.node.StatesToRecord component1() @NotNull public final java.util.Map component2() @NotNull - public final net.corda.core.flows.DistributionList copy(net.corda.core.node.StatesToRecord, java.util.Map) + public final net.corda.core.flows.DistributionList$SenderDistributionList copy(net.corda.core.node.StatesToRecord, java.util.Map) public boolean equals(Object) @NotNull public final java.util.Map getPeersToStatesToRecord() diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecovery.kt b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecovery.kt index ad234a6500..8f92995e56 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecovery.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecovery.kt @@ -142,15 +142,13 @@ class DBTransactionStorageLedgerRecovery(private val database: CordaPersistence, val senderRecordingTimestamp = clock.instant() val timeDiscriminator = Key.nextDiscriminatorNumber.andIncrement val distributionList = metadata.distributionList as? SenderDistributionList ?: throw IllegalStateException("Expecting SenderDistributionList") - for (peer in distributionList.peersToStatesToRecord.keys) { + distributionList.peersToStatesToRecord.map { (peerCordaX500Name, peerStatesToRecord) -> val senderDistributionRecord = DBSenderDistributionRecord( - PersistentKey(Key(TimestampKey(senderRecordingTimestamp, timeDiscriminator), partyInfoCache.getPartyIdByCordaX500Name(peer))), + PersistentKey(Key(TimestampKey(senderRecordingTimestamp, timeDiscriminator), partyInfoCache.getPartyIdByCordaX500Name(peerCordaX500Name))), txId.toString(), - distributionList.senderStatesToRecord - ) + peerStatesToRecord) session.save(senderDistributionRecord) } - val hashedPeersToStatesToRecord = distributionList.peersToStatesToRecord.mapKeys { (peer) -> partyInfoCache.getPartyIdByCordaX500Name(peer) } diff --git a/node/src/main/resources/migration/node-core.changelog-v25.xml b/node/src/main/resources/migration/node-core.changelog-v25.xml index a199a65df8..9ea40bada9 100644 --- a/node/src/main/resources/migration/node-core.changelog-v25.xml +++ b/node/src/main/resources/migration/node-core.changelog-v25.xml @@ -52,6 +52,9 @@ + + + From 4a6e99556bbe08061330df897321391ddde07235 Mon Sep 17 00:00:00 2001 From: Jose Coll Date: Fri, 18 Aug 2023 17:22:42 +0100 Subject: [PATCH 059/126] Incorporating PR review feedback. --- .../coretests/flows/FinalityFlowTests.kt | 2 +- .../core/flows/ReceiveTransactionFlow.kt | 3 +- .../core/internal/ServiceHubCoreInternal.kt | 8 +-- .../node/services/api/ServiceHubInternal.kt | 12 ++-- .../persistence/DBTransactionStorage.kt | 5 +- .../DBTransactionStorageLedgerRecovery.kt | 37 +++++++----- .../node/messaging/TwoPartyTradeFlowTests.kt | 6 +- ...DBTransactionStorageLedgerRecoveryTests.kt | 59 ++++++++++++------- .../test/flows/CashIssueWithObserversFlow.kt | 4 +- .../node/internal/MockTransactionStorage.kt | 5 +- .../kotlin/net/corda/testing/dsl/TestDSL.kt | 2 +- 11 files changed, 77 insertions(+), 66 deletions(-) diff --git a/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt b/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt index 9d82164ccc..f0e8ad93b9 100644 --- a/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt +++ b/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt @@ -358,7 +358,7 @@ class FinalityFlowTests : WithFinality { assertEquals(BOB_NAME.hashCode().toLong(), this[0].peerPartyId) } getReceiverRecoveryData(stx.id, bobNode, aliceNode).apply { - assertEquals(StatesToRecord.ALL_VISIBLE, this?.statesToRecord) + assertEquals(StatesToRecord.ONLY_RELEVANT, this?.statesToRecord) assertEquals(aliceNode.info.singleIdentity().name.hashCode().toLong(), this?.initiatorPartyId) assertEquals(mapOf(BOB_NAME.hashCode().toLong() to StatesToRecord.ALL_VISIBLE), this?.peersToStatesToRecord) } diff --git a/core/src/main/kotlin/net/corda/core/flows/ReceiveTransactionFlow.kt b/core/src/main/kotlin/net/corda/core/flows/ReceiveTransactionFlow.kt index a4f2defa3a..6c1431748d 100644 --- a/core/src/main/kotlin/net/corda/core/flows/ReceiveTransactionFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/ReceiveTransactionFlow.kt @@ -86,7 +86,8 @@ open class ReceiveTransactionFlow constructor(private val otherSideSession: Flow open fun resolvePayload(payload: Any): SignedTransaction { return if (payload is SignedTransactionWithDistributionList) { if (checkSufficientSignatures || deferredAck) { - (serviceHub as ServiceHubCoreInternal).recordReceiverTransactionRecoveryMetadata(payload.stx.id, otherSideSession.counterparty.name, ourIdentity.name, statesToRecord, payload.distributionList) + (serviceHub as ServiceHubCoreInternal).recordReceiverTransactionRecoveryMetadata(payload.stx.id, otherSideSession.counterparty.name, + TransactionMetadata(otherSideSession.counterparty.name, DistributionList.ReceiverDistributionList(payload.distributionList, statesToRecord))) payload.stx } else payload.stx } else payload as SignedTransaction diff --git a/core/src/main/kotlin/net/corda/core/internal/ServiceHubCoreInternal.kt b/core/src/main/kotlin/net/corda/core/internal/ServiceHubCoreInternal.kt index 27f05c9f2f..d752eb3b15 100644 --- a/core/src/main/kotlin/net/corda/core/internal/ServiceHubCoreInternal.kt +++ b/core/src/main/kotlin/net/corda/core/internal/ServiceHubCoreInternal.kt @@ -76,15 +76,11 @@ interface ServiceHubCoreInternal : ServiceHub { * * @param txnId The SecureHash of a transaction. * @param sender The sender of the transaction. - * @param receiver The receiver of the transaction. - * @param receiverStatesToRecord The StatesToRecord value of the receiver. - * @param encryptedDistributionList encrypted distribution list (hashed peers -> StatesToRecord values) + * @param txnMetadata The recovery metadata associated with a transaction. */ fun recordReceiverTransactionRecoveryMetadata(txnId: SecureHash, sender: CordaX500Name, - receiver: CordaX500Name, - receiverStatesToRecord: StatesToRecord, - encryptedDistributionList: ByteArray) + txnMetadata: TransactionMetadata) } interface TransactionsResolver { diff --git a/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt b/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt index 962f7a0664..9ed76b15c8 100644 --- a/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt +++ b/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt @@ -198,8 +198,8 @@ interface ServiceHubInternal : ServiceHubCoreInternal { override fun recordSenderTransactionRecoveryMetadata(txnId: SecureHash, txnMetadata: TransactionMetadata) = validatedTransactions.addSenderTransactionRecoveryMetadata(txnId, txnMetadata) - override fun recordReceiverTransactionRecoveryMetadata(txnId: SecureHash, sender: CordaX500Name, receiver: CordaX500Name, receiverStatesToRecord: StatesToRecord, encryptedDistributionList: ByteArray) = - validatedTransactions.addReceiverTransactionRecoveryMetadata(txnId, sender, receiver, receiverStatesToRecord, encryptedDistributionList) + override fun recordReceiverTransactionRecoveryMetadata(txnId: SecureHash, sender: CordaX500Name, txnMetadata: TransactionMetadata) = + validatedTransactions.addReceiverTransactionRecoveryMetadata(txnId, sender, txnMetadata) @Suppress("NestedBlockDepth") @VisibleForTesting @@ -383,15 +383,11 @@ interface WritableTransactionStorage : TransactionStorage { * * @param txId The SecureHash of a transaction. * @param sender The sender of the transaction. - * @param receiver The receiver of the transaction. - * @param receiverStatesToRecord The StatesToRecord value of the receiver. - * @param encryptedDistributionList encrypted distribution list (hashed peers -> StatesToRecord values) + * @param metadata The recovery metadata associated with a transaction. */ fun addReceiverTransactionRecoveryMetadata(txId: SecureHash, sender: CordaX500Name, - receiver: CordaX500Name, - receiverStatesToRecord: StatesToRecord, - encryptedDistributionList: ByteArray) + metadata: TransactionMetadata) /** * Removes an un-notarised transaction (with a status of *MISSING_TRANSACTION_SIG*) from the data store. diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt index 1973f9e7c1..c43834993c 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt @@ -219,9 +219,8 @@ open class DBTransactionStorage(private val database: CordaPersistence, cacheFac override fun addReceiverTransactionRecoveryMetadata(txId: SecureHash, sender: CordaX500Name, - receiver: CordaX500Name, - receiverStatesToRecord: StatesToRecord, - encryptedDistributionList: ByteArray) { } + metadata: TransactionMetadata + ) { } override fun finalizeTransaction(transaction: SignedTransaction) = addTransaction(transaction) { diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecovery.kt b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecovery.kt index 8f92995e56..527c0e4e9a 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecovery.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecovery.kt @@ -1,11 +1,13 @@ package net.corda.node.services.persistence import net.corda.core.crypto.SecureHash +import net.corda.core.flows.DistributionList.ReceiverDistributionList import net.corda.core.flows.DistributionList.SenderDistributionList import net.corda.core.flows.RecoveryTimeWindow import net.corda.core.flows.TransactionMetadata import net.corda.core.identity.CordaX500Name import net.corda.core.internal.NamedCacheFactory +import net.corda.core.internal.VisibleForTesting import net.corda.core.node.StatesToRecord import net.corda.core.node.services.vault.Sort import net.corda.core.serialization.CordaSerializable @@ -26,7 +28,6 @@ import javax.persistence.Id import javax.persistence.Lob import javax.persistence.Table import javax.persistence.criteria.Predicate -import kotlin.streams.toList class DBTransactionStorageLedgerRecovery(private val database: CordaPersistence, cacheFactory: NamedCacheFactory, @@ -98,7 +99,7 @@ class DBTransactionStorageLedgerRecovery(private val database: CordaPersistence, distributionList = encryptedDistributionList, receiverStatesToRecord = receiverStatesToRecord ) - + @VisibleForTesting fun toReceiverDistributionRecord(encryptionService: EncryptionService): ReceiverDistributionRecord { val hashedDL = HashedDistributionList.decrypt(this.distributionList, encryptionService) return ReceiverDistributionRecord( @@ -163,18 +164,22 @@ class DBTransactionStorageLedgerRecovery(private val database: CordaPersistence, override fun addReceiverTransactionRecoveryMetadata(txId: SecureHash, sender: CordaX500Name, - receiver: CordaX500Name, - receiverStatesToRecord: StatesToRecord, - encryptedDistributionList: ByteArray) { - val publicHeader = HashedDistributionList.PublicHeader.unauthenticatedDeserialise(encryptedDistributionList, encryptionService) - database.transaction { - val receiverDistributionRecord = DBReceiverDistributionRecord( - Key(partyInfoCache.getPartyIdByCordaX500Name(sender), publicHeader.senderRecordedTimestamp), - txId, - encryptedDistributionList, - receiverStatesToRecord - ) - session.save(receiverDistributionRecord) + metadata: TransactionMetadata) { + when (metadata.distributionList) { + is ReceiverDistributionList -> { + val distributionList = metadata.distributionList as ReceiverDistributionList + val publicHeader = HashedDistributionList.PublicHeader.unauthenticatedDeserialise(distributionList.opaqueData, encryptionService) + database.transaction { + val receiverDistributionRecord = DBReceiverDistributionRecord( + Key(partyInfoCache.getPartyIdByCordaX500Name(sender), publicHeader.senderRecordedTimestamp), + txId, + distributionList.opaqueData, + distributionList.receiverStatesToRecord + ) + session.save(receiverDistributionRecord) + } + } + else -> throw IllegalStateException("Expecting ReceiverDistributionList") } } @@ -247,7 +252,7 @@ class DBTransactionStorageLedgerRecovery(private val database: CordaPersistence, } criteriaQuery.orderBy(orderCriteria) } - session.createQuery(criteriaQuery).stream().toList() + session.createQuery(criteriaQuery).resultList } } @@ -284,7 +289,7 @@ class DBTransactionStorageLedgerRecovery(private val database: CordaPersistence, } criteriaQuery.orderBy(orderCriteria) } - session.createQuery(criteriaQuery).stream().toList() + session.createQuery(criteriaQuery).resultList } } } diff --git a/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt b/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt index d0f0de3d60..68cfd544d5 100644 --- a/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt +++ b/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt @@ -818,11 +818,9 @@ class TwoPartyTradeFlowTests(private val anonymous: Boolean) { override fun addReceiverTransactionRecoveryMetadata(txId: SecureHash, sender: CordaX500Name, - receiver: CordaX500Name, - receiverStatesToRecord: StatesToRecord, - encryptedDistributionList: ByteArray) { + metadata: TransactionMetadata) { database.transaction { - delegate.addReceiverTransactionRecoveryMetadata(txId, sender, receiver, receiverStatesToRecord, encryptedDistributionList) + delegate.addReceiverTransactionRecoveryMetadata(txId, sender, metadata) } } diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecoveryTests.kt b/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecoveryTests.kt index 8f759838cb..3e32fef074 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecoveryTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecoveryTests.kt @@ -6,6 +6,7 @@ import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SignableData import net.corda.core.crypto.SignatureMetadata import net.corda.core.crypto.sign +import net.corda.core.flows.DistributionList.ReceiverDistributionList import net.corda.core.flows.DistributionList.SenderDistributionList import net.corda.core.flows.RecoveryTimeWindow import net.corda.core.flows.TransactionMetadata @@ -137,11 +138,13 @@ class DBTransactionStorageLedgerRecoveryTests { val transaction2 = newTransaction() // receiver txn transactionRecovery.addUnnotarisedTransaction(transaction2) - transactionRecovery.addReceiverTransactionRecoveryMetadata(transaction2.id, BOB_NAME, ALICE_NAME, ALL_VISIBLE, - SenderDistributionList(ONLY_RELEVANT, mapOf(ALICE_NAME to ALL_VISIBLE)).toWire()) + val encryptedDL = transactionRecovery.addSenderTransactionRecoveryMetadata(transaction2.id, + TransactionMetadata(BOB_NAME, SenderDistributionList(ONLY_RELEVANT, mapOf(ALICE_NAME to ALL_VISIBLE)))) + transactionRecovery.addReceiverTransactionRecoveryMetadata(transaction2.id, BOB_NAME, + TransactionMetadata(BOB_NAME, ReceiverDistributionList(encryptedDL, ALL_VISIBLE))) val timeWindow = RecoveryTimeWindow(fromTime = now().minus(1, ChronoUnit.DAYS)) transactionRecovery.queryDistributionRecords(timeWindow, recordType = DistributionRecordType.SENDER).let { - assertEquals(1, it.size) + assertEquals(2, it.size) assertEquals(BOB_NAME.hashCode().toLong(), it.senderRecords[0].compositeKey.peerPartyId) assertEquals(ALL_VISIBLE, it.senderRecords[0].statesToRecord) } @@ -151,7 +154,7 @@ class DBTransactionStorageLedgerRecoveryTests { assertEquals(ALL_VISIBLE, (HashedDistributionList.decrypt(it.receiverRecords[0].distributionList, encryptionService)).peerHashToStatesToRecord.map { it.value }[0]) } val resultsAll = transactionRecovery.queryDistributionRecords(timeWindow, recordType = DistributionRecordType.ALL) - assertEquals(2, resultsAll.size) + assertEquals(3, resultsAll.size) } @Test(timeout = 300_000) @@ -187,24 +190,34 @@ class DBTransactionStorageLedgerRecoveryTests { fun `query for receiver distribution records by initiator`() { val txn1 = newTransaction() transactionRecovery.addUnnotarisedTransaction(txn1) - transactionRecovery.addReceiverTransactionRecoveryMetadata(txn1.id, ALICE_NAME, BOB_NAME, ALL_VISIBLE, - SenderDistributionList(ONLY_RELEVANT, mapOf(BOB_NAME to ALL_VISIBLE, CHARLIE_NAME to ALL_VISIBLE)).toWire()) + val encryptedDL1 = transactionRecovery.addSenderTransactionRecoveryMetadata(txn1.id, + TransactionMetadata(ALICE_NAME, SenderDistributionList(ONLY_RELEVANT, mapOf(BOB_NAME to ALL_VISIBLE, CHARLIE_NAME to ALL_VISIBLE)))) + transactionRecovery.addReceiverTransactionRecoveryMetadata(txn1.id, ALICE_NAME, + TransactionMetadata(ALICE_NAME, ReceiverDistributionList(encryptedDL1, ALL_VISIBLE))) val txn2 = newTransaction() transactionRecovery.addUnnotarisedTransaction(txn2) - transactionRecovery.addReceiverTransactionRecoveryMetadata(txn2.id, ALICE_NAME, BOB_NAME, ONLY_RELEVANT, - SenderDistributionList(ONLY_RELEVANT, mapOf(BOB_NAME to ONLY_RELEVANT)).toWire()) + val encryptedDL2 = transactionRecovery.addSenderTransactionRecoveryMetadata(txn2.id, + TransactionMetadata(ALICE_NAME, SenderDistributionList(ONLY_RELEVANT, mapOf(BOB_NAME to ONLY_RELEVANT)))) + transactionRecovery.addReceiverTransactionRecoveryMetadata(txn2.id, ALICE_NAME, + TransactionMetadata(ALICE_NAME, ReceiverDistributionList(encryptedDL2, ONLY_RELEVANT))) val txn3 = newTransaction() transactionRecovery.addUnnotarisedTransaction(txn3) - transactionRecovery.addReceiverTransactionRecoveryMetadata(txn3.id, ALICE_NAME, CHARLIE_NAME, NONE, - SenderDistributionList(ONLY_RELEVANT, mapOf(CHARLIE_NAME to NONE)).toWire()) + val encryptedDL3 = transactionRecovery.addSenderTransactionRecoveryMetadata(txn3.id, + TransactionMetadata(ALICE_NAME, SenderDistributionList(ONLY_RELEVANT, mapOf(CHARLIE_NAME to NONE)))) + transactionRecovery.addReceiverTransactionRecoveryMetadata(txn3.id, ALICE_NAME, + TransactionMetadata(ALICE_NAME, ReceiverDistributionList(encryptedDL3, NONE))) val txn4 = newTransaction() transactionRecovery.addUnnotarisedTransaction(txn4) - transactionRecovery.addReceiverTransactionRecoveryMetadata(txn4.id, BOB_NAME, ALICE_NAME, ONLY_RELEVANT, - SenderDistributionList(ONLY_RELEVANT, mapOf(ALICE_NAME to ALL_VISIBLE)).toWire()) + val encryptedDL4 = transactionRecovery.addSenderTransactionRecoveryMetadata(txn4.id, + TransactionMetadata(BOB_NAME, SenderDistributionList(ONLY_RELEVANT, mapOf(ALICE_NAME to ALL_VISIBLE)))) + transactionRecovery.addReceiverTransactionRecoveryMetadata(txn4.id, BOB_NAME, + TransactionMetadata(BOB_NAME, ReceiverDistributionList(encryptedDL4, ALL_VISIBLE))) val txn5 = newTransaction() transactionRecovery.addUnnotarisedTransaction(txn5) - transactionRecovery.addReceiverTransactionRecoveryMetadata(txn5.id, CHARLIE_NAME, BOB_NAME, ONLY_RELEVANT, - SenderDistributionList(ONLY_RELEVANT, mapOf(BOB_NAME to ONLY_RELEVANT)).toWire()) + val encryptedDL5 = transactionRecovery.addSenderTransactionRecoveryMetadata(txn5.id, + TransactionMetadata(CHARLIE_NAME, SenderDistributionList(ONLY_RELEVANT, mapOf(BOB_NAME to ONLY_RELEVANT)))) + transactionRecovery.addReceiverTransactionRecoveryMetadata(txn5.id, CHARLIE_NAME, + TransactionMetadata(CHARLIE_NAME, ReceiverDistributionList(encryptedDL5, ONLY_RELEVANT))) val timeWindow = RecoveryTimeWindow(fromTime = now().minus(1, ChronoUnit.DAYS)) transactionRecovery.queryReceiverDistributionRecords(timeWindow, initiators = setOf(ALICE_NAME)).let { @@ -242,8 +255,10 @@ class DBTransactionStorageLedgerRecoveryTests { val receiverTransaction = newTransaction() transactionRecovery.addUnnotarisedTransaction(receiverTransaction) - transactionRecovery.addReceiverTransactionRecoveryMetadata(receiverTransaction.id, ALICE_NAME, BOB_NAME, ALL_VISIBLE, - SenderDistributionList(ONLY_RELEVANT, mapOf(BOB_NAME to ALL_VISIBLE)).toWire()) + val encryptedDL = transactionRecovery.addSenderTransactionRecoveryMetadata(receiverTransaction.id, + TransactionMetadata(ALICE_NAME, SenderDistributionList(ONLY_RELEVANT, mapOf(BOB_NAME to ALL_VISIBLE)))) + transactionRecovery.addReceiverTransactionRecoveryMetadata(receiverTransaction.id, ALICE_NAME, + TransactionMetadata(ALICE_NAME, ReceiverDistributionList(encryptedDL, ALL_VISIBLE))) assertEquals(IN_FLIGHT, readTransactionFromDB(receiverTransaction.id).status) readReceiverDistributionRecordFromDB(receiverTransaction.id).let { assertEquals(ONLY_RELEVANT, it.statesToRecord) @@ -270,8 +285,10 @@ class DBTransactionStorageLedgerRecoveryTests { fun `remove un-notarised transaction and associated recovery metadata`() { val senderTransaction = newTransaction(notarySig = false) transactionRecovery.addUnnotarisedTransaction(senderTransaction) - transactionRecovery.addReceiverTransactionRecoveryMetadata(senderTransaction.id, ALICE.name, BOB.name, ONLY_RELEVANT, - SenderDistributionList(ONLY_RELEVANT, mapOf(BOB.name to ONLY_RELEVANT, CHARLIE_NAME to ONLY_RELEVANT)).toWire()) + val encryptedDL1 = transactionRecovery.addSenderTransactionRecoveryMetadata(senderTransaction.id, + TransactionMetadata(ALICE.name, SenderDistributionList(ONLY_RELEVANT, mapOf(BOB.name to ONLY_RELEVANT, CHARLIE_NAME to ONLY_RELEVANT)))) + transactionRecovery.addReceiverTransactionRecoveryMetadata(senderTransaction.id, BOB.name, + TransactionMetadata(ALICE.name, ReceiverDistributionList(encryptedDL1, ONLY_RELEVANT))) assertNull(transactionRecovery.getTransaction(senderTransaction.id)) assertEquals(IN_FLIGHT, readTransactionFromDB(senderTransaction.id).status) @@ -282,8 +299,10 @@ class DBTransactionStorageLedgerRecoveryTests { val receiverTransaction = newTransaction(notarySig = false) transactionRecovery.addUnnotarisedTransaction(receiverTransaction) - transactionRecovery.addReceiverTransactionRecoveryMetadata(receiverTransaction.id, ALICE.name, BOB.name, ONLY_RELEVANT, - SenderDistributionList(ONLY_RELEVANT, mapOf(BOB.name to ONLY_RELEVANT)).toWire()) + val encryptedDL2 = transactionRecovery.addSenderTransactionRecoveryMetadata(receiverTransaction.id, + TransactionMetadata(ALICE.name, SenderDistributionList(ONLY_RELEVANT, mapOf(BOB.name to ONLY_RELEVANT)))) + transactionRecovery.addReceiverTransactionRecoveryMetadata(receiverTransaction.id, BOB.name, + TransactionMetadata(ALICE.name, ReceiverDistributionList(encryptedDL2, ONLY_RELEVANT))) assertNull(transactionRecovery.getTransaction(receiverTransaction.id)) assertEquals(IN_FLIGHT, readTransactionFromDB(receiverTransaction.id).status) diff --git a/testing/cordapps/cashobservers/src/main/kotlin/net/corda/finance/test/flows/CashIssueWithObserversFlow.kt b/testing/cordapps/cashobservers/src/main/kotlin/net/corda/finance/test/flows/CashIssueWithObserversFlow.kt index c860617078..282b49c3cb 100644 --- a/testing/cordapps/cashobservers/src/main/kotlin/net/corda/finance/test/flows/CashIssueWithObserversFlow.kt +++ b/testing/cordapps/cashobservers/src/main/kotlin/net/corda/finance/test/flows/CashIssueWithObserversFlow.kt @@ -42,9 +42,9 @@ class CashIssueWithObserversFlow(private val amount: Amount, } @Suspendable - private fun finalise(tx: SignedTransaction, sessions: Collection, message: String): SignedTransaction { + private fun finalise(tx: SignedTransaction, observerSessions: Collection, message: String): SignedTransaction { try { - return subFlow(FinalityFlow(tx, sessions)) + return subFlow(FinalityFlow(tx, sessions = emptySet(), observerSessions = observerSessions)) } catch (e: NotaryException) { throw CashException(message, e) } diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/MockTransactionStorage.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/MockTransactionStorage.kt index 9f23bf6beb..0a52c09f56 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/MockTransactionStorage.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/MockTransactionStorage.kt @@ -12,7 +12,6 @@ import net.corda.node.services.api.WritableTransactionStorage import net.corda.core.flows.TransactionMetadata import net.corda.core.flows.TransactionStatus import net.corda.core.identity.CordaX500Name -import net.corda.core.node.StatesToRecord import net.corda.testing.node.MockServices import rx.Observable import rx.subjects.PublishSubject @@ -65,9 +64,7 @@ open class MockTransactionStorage : WritableTransactionStorage, SingletonSeriali override fun addReceiverTransactionRecoveryMetadata(txId: SecureHash, sender: CordaX500Name, - receiver: CordaX500Name, - receiverStatesToRecord: StatesToRecord, - encryptedDistributionList: ByteArray) { } + metadata: TransactionMetadata) { } override fun removeUnnotarisedTransaction(id: SecureHash): Boolean { return txns.remove(id) != null diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TestDSL.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TestDSL.kt index fd55d3645d..b460d02f30 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TestDSL.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TestDSL.kt @@ -150,7 +150,7 @@ data class TestTransactionDSLInterpreter private constructor( override fun recordSenderTransactionRecoveryMetadata(txnId: SecureHash, txnMetadata: TransactionMetadata): ByteArray? { return null } - override fun recordReceiverTransactionRecoveryMetadata(txnId: SecureHash, sender: CordaX500Name, receiver: CordaX500Name, receiverStatesToRecord: StatesToRecord, encryptedDistributionList: ByteArray) {} + override fun recordReceiverTransactionRecoveryMetadata(txnId: SecureHash, sender: CordaX500Name, txnMetadata: TransactionMetadata) {} } private fun copy(): TestTransactionDSLInterpreter = From 6a7e9000a4fac043f54bc2e2a8195bb4bc16368f Mon Sep 17 00:00:00 2001 From: Jose Coll Date: Fri, 18 Aug 2023 17:26:22 +0100 Subject: [PATCH 060/126] Detekt --- .../net/corda/node/services/persistence/DBTransactionStorage.kt | 1 - .../kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt | 1 - 2 files changed, 2 deletions(-) diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt index c43834993c..6905d6f7c1 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt @@ -11,7 +11,6 @@ import net.corda.core.internal.VisibleForTesting import net.corda.core.internal.bufferUntilSubscribed import net.corda.core.internal.concurrent.doneFuture import net.corda.core.messaging.DataFeed -import net.corda.core.node.StatesToRecord import net.corda.core.serialization.SerializationContext import net.corda.core.serialization.SerializationDefaults import net.corda.core.serialization.SerializedBytes diff --git a/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt b/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt index 68cfd544d5..b53d9daeee 100644 --- a/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt +++ b/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt @@ -31,7 +31,6 @@ import net.corda.core.internal.concurrent.map import net.corda.core.internal.rootCause import net.corda.core.messaging.DataFeed import net.corda.core.messaging.StateMachineTransactionMapping -import net.corda.core.node.StatesToRecord import net.corda.core.node.services.Vault import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.SingletonSerializeAsToken From a6786769e54503969681122e3a4693616d4139d0 Mon Sep 17 00:00:00 2001 From: Jose Coll Date: Mon, 21 Aug 2023 09:11:00 +0100 Subject: [PATCH 061/126] ENT-10306 Determine whether to use 2PF based on the execution CorDapp TPV. (#7447) --- .../flows/ContractUpgradeFlowTest.kt | 2 +- .../coretests/flows/FinalityFlowTests.kt | 8 +++----- .../net/corda/coretests/flows/WithFinality.kt | 17 +++++++++++++++- .../net/corda/core/flows/FinalityFlow.kt | 5 +++++ .../node/messaging/TwoPartyTradeFlowTests.kt | 5 +++-- .../services/ServiceHubConcurrentUsageTest.kt | 4 +++- .../net/corda/node/services/TimedFlowTests.kt | 4 +++- .../statemachine/FlowFrameworkTests.kt | 3 ++- testing/cordapps/cashobservers/build.gradle | 20 ++++++++++++++++--- .../dbfailure/dbfworkflows/build.gradle | 20 ++++++++++++++++--- 10 files changed, 70 insertions(+), 18 deletions(-) diff --git a/core-tests/src/test/kotlin/net/corda/coretests/flows/ContractUpgradeFlowTest.kt b/core-tests/src/test/kotlin/net/corda/coretests/flows/ContractUpgradeFlowTest.kt index 54678ff3c6..e6994316a8 100644 --- a/core-tests/src/test/kotlin/net/corda/coretests/flows/ContractUpgradeFlowTest.kt +++ b/core-tests/src/test/kotlin/net/corda/coretests/flows/ContractUpgradeFlowTest.kt @@ -30,7 +30,7 @@ import java.util.* class ContractUpgradeFlowTest : WithContracts, WithFinality { companion object { - private val classMockNet = InternalMockNetwork(cordappsForAllNodes = listOf(FINANCE_CONTRACTS_CORDAPP, DUMMY_CONTRACTS_CORDAPP, enclosedCordapp())) + private val classMockNet = InternalMockNetwork(cordappsForAllNodes = listOf(FINANCE_CONTRACTS_CORDAPP, FINANCE_WORKFLOWS_CORDAPP, DUMMY_CONTRACTS_CORDAPP, enclosedCordapp())) @JvmStatic @AfterClass diff --git a/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt b/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt index e34b0a4c2b..a2c7124b74 100644 --- a/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt +++ b/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt @@ -40,6 +40,7 @@ import net.corda.core.utilities.unwrap import net.corda.coretesting.internal.matchers.flow.willReturn import net.corda.coretesting.internal.matchers.flow.willThrow import net.corda.coretests.flows.WithFinality.FinalityInvoker +import net.corda.coretests.flows.WithFinality.OldFinalityInvoker import net.corda.finance.GBP import net.corda.finance.POUNDS import net.corda.finance.contracts.asset.Cash @@ -59,7 +60,6 @@ import net.corda.testing.core.BOB_NAME import net.corda.testing.core.CHARLIE_NAME import net.corda.testing.core.TestIdentity import net.corda.testing.core.singleIdentity -import net.corda.testing.node.internal.CustomCordapp import net.corda.testing.node.internal.DUMMY_CONTRACTS_CORDAPP import net.corda.testing.node.internal.FINANCE_CONTRACTS_CORDAPP import net.corda.testing.node.internal.FINANCE_WORKFLOWS_CORDAPP @@ -87,9 +87,7 @@ class FinalityFlowTests : WithFinality { } override val mockNet = InternalMockNetwork(cordappsForAllNodes = setOf(FINANCE_CONTRACTS_CORDAPP, FINANCE_WORKFLOWS_CORDAPP, DUMMY_CONTRACTS_CORDAPP, enclosedCordapp(), - findCordapp("net.corda.finance.test.flows"), - CustomCordapp(targetPlatformVersion = 3, classes = setOf(FinalityFlow::class.java)))) - + findCordapp("net.corda.finance.test.flows"))) private val aliceNode = makeNode(ALICE_NAME) private val notary = mockNet.defaultNotaryIdentity @@ -124,7 +122,7 @@ class FinalityFlowTests : WithFinality { val oldBob = createBob(cordapps = listOf(tokenOldCordapp())) val stx = aliceNode.issuesCashTo(oldBob) @Suppress("DEPRECATION") - aliceNode.startFlowAndRunNetwork(FinalityFlow(stx)).resultFuture.getOrThrow() + aliceNode.startFlowAndRunNetwork(OldFinalityInvoker(stx)).resultFuture.getOrThrow() assertThat(oldBob.services.validatedTransactions.getTransaction(stx.id)).isNotNull } diff --git a/core-tests/src/test/kotlin/net/corda/coretests/flows/WithFinality.kt b/core-tests/src/test/kotlin/net/corda/coretests/flows/WithFinality.kt index 9ed9b04679..714e8b85fa 100644 --- a/core-tests/src/test/kotlin/net/corda/coretests/flows/WithFinality.kt +++ b/core-tests/src/test/kotlin/net/corda/coretests/flows/WithFinality.kt @@ -4,7 +4,13 @@ import co.paralleluniverse.fibers.Suspendable import com.natpryce.hamkrest.MatchResult import com.natpryce.hamkrest.Matcher import com.natpryce.hamkrest.equalTo -import net.corda.core.flows.* +import net.corda.core.flows.FinalityFlow +import net.corda.core.flows.FlowLogic +import net.corda.core.flows.FlowSession +import net.corda.core.flows.InitiatedBy +import net.corda.core.flows.InitiatingFlow +import net.corda.core.flows.ReceiveFinalityFlow +import net.corda.core.flows.StartableByRPC import net.corda.core.identity.Party import net.corda.core.internal.FlowStateMachineHandle import net.corda.core.messaging.CordaRPCOps @@ -58,4 +64,13 @@ interface WithFinality : WithMockNet { subFlow(ReceiveFinalityFlow(otherSide)) } } + + @StartableByRPC + class OldFinalityInvoker(private val transaction: SignedTransaction) : FlowLogic() { + @Suspendable + override fun call(): SignedTransaction { + @Suppress("DEPRECATION") + return subFlow(FinalityFlow(transaction)) + } + } } diff --git a/core/src/main/kotlin/net/corda/core/flows/FinalityFlow.kt b/core/src/main/kotlin/net/corda/core/flows/FinalityFlow.kt index 36b5174ad8..62d85b9fff 100644 --- a/core/src/main/kotlin/net/corda/core/flows/FinalityFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/FinalityFlow.kt @@ -219,6 +219,7 @@ class FinalityFlow private constructor(val transaction: SignedTransaction, val requiresNotarisation = needsNotarySignature(transaction) val useTwoPhaseFinality = serviceHub.myInfo.platformVersion >= PlatformVersionSwitches.TWO_PHASE_FINALITY + && serviceHub.getAppContext().cordapp.targetPlatformVersion >= PlatformVersionSwitches.TWO_PHASE_FINALITY if (useTwoPhaseFinality) { val stxn = if (requiresNotarisation) { @@ -250,6 +251,7 @@ class FinalityFlow private constructor(val transaction: SignedTransaction, return stxn } else { + logger.warnOnce("The current usage of FinalityFlow is not using Two Phase Finality. Please consider upgrading your CorDapp (refer to Corda 4.11 release notes).") val stxn = if (requiresNotarisation) { notarise().first } else transaction @@ -501,6 +503,8 @@ class ReceiveFinalityFlow(private val otherSideSession: FlowSession, val requiresNotarisation = needsNotarySignature(stx) val fromTwoPhaseFinalityNode = serviceHub.networkMapCache.getNodeByLegalIdentity(otherSideSession.counterparty)?.platformVersion!! >= PlatformVersionSwitches.TWO_PHASE_FINALITY + && serviceHub.getAppContext().cordapp.targetPlatformVersion >= PlatformVersionSwitches.TWO_PHASE_FINALITY + if (fromTwoPhaseFinalityNode) { if (requiresNotarisation) { serviceHub.telemetryServiceInternal.span("${this::class.java.name}#recordUnnotarisedTransaction", flowLogic = this) { @@ -537,6 +541,7 @@ class ReceiveFinalityFlow(private val otherSideSession: FlowSession, otherSideSession.send(FetchDataFlow.Request.End) // Finish fetching data (deferredAck) } } else { + logger.warnOnce("The current usage of ReceiveFinalityFlow is not using Two Phase Finality. Please consider upgrading your CorDapp (refer to Corda 4.11 release notes).") serviceHub.telemetryServiceInternal.span("${this::class.java.name}#recordTransactions", flowLogic = this) { serviceHub.recordTransactions(statesToRecord, setOf(stx)) } diff --git a/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt b/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt index fa516073fb..e85061a8f8 100644 --- a/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt +++ b/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt @@ -72,6 +72,7 @@ import net.corda.testing.internal.IS_OPENJ9 import net.corda.testing.internal.LogHelper import net.corda.testing.internal.vault.VaultFiller import net.corda.testing.node.internal.FINANCE_CONTRACTS_CORDAPP +import net.corda.testing.node.internal.FINANCE_WORKFLOWS_CORDAPP import net.corda.testing.node.internal.InternalMockNetwork import net.corda.testing.node.internal.InternalMockNodeParameters import net.corda.testing.node.internal.TestStartedNode @@ -141,7 +142,7 @@ class TwoPartyTradeFlowTests(private val anonymous: Boolean) { // We run this in parallel threads to help catch any race conditions that may exist. The other tests // we run in the unit test thread exclusively to speed things up, ensure deterministic results and // allow interruption half way through. - mockNet = InternalMockNetwork(cordappsForAllNodes = listOf(FINANCE_CONTRACTS_CORDAPP), threadPerNode = true) + mockNet = InternalMockNetwork(cordappsForAllNodes = listOf(FINANCE_CONTRACTS_CORDAPP, FINANCE_WORKFLOWS_CORDAPP), threadPerNode = true) val notaryNode = mockNet.defaultNotaryNode val notary = mockNet.defaultNotaryIdentity notaryNode.services.ledger(notary) { @@ -248,7 +249,7 @@ class TwoPartyTradeFlowTests(private val anonymous: Boolean) { @Test(timeout=300_000) fun `shutdown and restore`() { Assume.assumeTrue(!IS_OPENJ9) - mockNet = InternalMockNetwork(cordappsForAllNodes = listOf(FINANCE_CONTRACTS_CORDAPP)) + mockNet = InternalMockNetwork(cordappsForAllNodes = listOf(FINANCE_CONTRACTS_CORDAPP, FINANCE_WORKFLOWS_CORDAPP)) val notaryNode = mockNet.defaultNotaryNode val notary = mockNet.defaultNotaryIdentity notaryNode.services.ledger(notary) { diff --git a/node/src/test/kotlin/net/corda/node/services/ServiceHubConcurrentUsageTest.kt b/node/src/test/kotlin/net/corda/node/services/ServiceHubConcurrentUsageTest.kt index ecba9d248d..9e235cebb1 100644 --- a/node/src/test/kotlin/net/corda/node/services/ServiceHubConcurrentUsageTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/ServiceHubConcurrentUsageTest.kt @@ -13,6 +13,7 @@ import net.corda.core.utilities.getOrThrow import net.corda.finance.DOLLARS import net.corda.finance.contracts.asset.Cash import net.corda.finance.issuedBy +import net.corda.testing.node.internal.CustomCordapp import net.corda.testing.node.internal.FINANCE_CONTRACTS_CORDAPP import net.corda.testing.node.internal.InternalMockNetwork import net.corda.testing.node.internal.startFlow @@ -23,7 +24,8 @@ import rx.schedulers.Schedulers import java.util.concurrent.CountDownLatch class ServiceHubConcurrentUsageTest { - private val mockNet = InternalMockNetwork(cordappsForAllNodes = listOf(FINANCE_CONTRACTS_CORDAPP)) + private val mockNet = InternalMockNetwork(cordappsForAllNodes = listOf(FINANCE_CONTRACTS_CORDAPP, + CustomCordapp(classes = setOf(TestFlow::class.java)))) @After fun stopNodes() { diff --git a/node/src/test/kotlin/net/corda/node/services/TimedFlowTests.kt b/node/src/test/kotlin/net/corda/node/services/TimedFlowTests.kt index b0b4d8313d..df1fa5aa64 100644 --- a/node/src/test/kotlin/net/corda/node/services/TimedFlowTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/TimedFlowTests.kt @@ -75,7 +75,9 @@ class TimedFlowTests { @JvmStatic fun setup() { mockNet = InternalMockNetwork( - cordappsForAllNodes = listOf(DUMMY_CONTRACTS_CORDAPP, enclosedCordapp()), + cordappsForAllNodes = listOf(DUMMY_CONTRACTS_CORDAPP, + CustomCordapp(classes = setOf(FinalityFlow::class.java)), + enclosedCordapp()), defaultParameters = MockNetworkParameters().withServicePeerAllocationStrategy(InMemoryMessagingNetwork.ServicePeerAllocationStrategy.RoundRobin()), threadPerNode = true ) diff --git a/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt b/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt index def6530e5e..9c2628ff45 100644 --- a/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt @@ -58,6 +58,7 @@ import net.corda.testing.internal.IS_OPENJ9 import net.corda.testing.internal.LogHelper import net.corda.testing.node.InMemoryMessagingNetwork.MessageTransfer import net.corda.testing.node.InMemoryMessagingNetwork.ServicePeerAllocationStrategy.RoundRobin +import net.corda.testing.node.internal.CustomCordapp import net.corda.testing.node.internal.DUMMY_CONTRACTS_CORDAPP import net.corda.testing.node.internal.FINANCE_CONTRACTS_CORDAPP import net.corda.testing.node.internal.InternalMockNetwork @@ -124,7 +125,7 @@ class FlowFrameworkTests { @Before fun setUpMockNet() { mockNet = InternalMockNetwork( - cordappsForAllNodes = listOf(DUMMY_CONTRACTS_CORDAPP, FINANCE_CONTRACTS_CORDAPP), + cordappsForAllNodes = listOf(DUMMY_CONTRACTS_CORDAPP, FINANCE_CONTRACTS_CORDAPP, CustomCordapp(setOf("net.corda.node.services.statemachine"))), servicePeerAllocationStrategy = RoundRobin() ) diff --git a/testing/cordapps/cashobservers/build.gradle b/testing/cordapps/cashobservers/build.gradle index e8f0d47d5f..8222caae16 100644 --- a/testing/cordapps/cashobservers/build.gradle +++ b/testing/cordapps/cashobservers/build.gradle @@ -1,10 +1,10 @@ apply plugin: 'kotlin' -//apply plugin: 'net.corda.plugins.cordapp' +apply plugin: 'net.corda.plugins.cordapp' //apply plugin: 'net.corda.plugins.quasar-utils' dependencies { - compile project(":core") - compile project(':finance:workflows') + cordaCompile project(":core") + cordapp project(':finance:workflows') } jar { @@ -14,4 +14,18 @@ jar { // Driver will not include it as part of an out-of-process node. attributes('Corda-Testing': true) } +} + +cordapp { + targetPlatformVersion corda_platform_version.toInteger() + minimumPlatformVersion 1 + workflow { + name "Corda Cash Observers Test CorDapp" + versionId 1 + vendor "R3" + licence "Open Source (Apache 2)" + } + signing { + enabled false + } } \ No newline at end of file diff --git a/testing/cordapps/dbfailure/dbfworkflows/build.gradle b/testing/cordapps/dbfailure/dbfworkflows/build.gradle index 26e0058cc5..221b063236 100644 --- a/testing/cordapps/dbfailure/dbfworkflows/build.gradle +++ b/testing/cordapps/dbfailure/dbfworkflows/build.gradle @@ -1,10 +1,10 @@ apply plugin: 'kotlin' -//apply plugin: 'net.corda.plugins.cordapp' +apply plugin: 'net.corda.plugins.cordapp' //apply plugin: 'net.corda.plugins.quasar-utils' dependencies { - compile project(":core") - compile project(":testing:cordapps:dbfailure:dbfcontracts") + cordaCompile project(":core") + cordapp project(":testing:cordapps:dbfailure:dbfcontracts") } jar { @@ -14,4 +14,18 @@ jar { // Driver will not include it as part of an out-of-process node. attributes('Corda-Testing': true) } +} + +cordapp { + targetPlatformVersion corda_platform_version.toInteger() + minimumPlatformVersion 1 + workflow { + name "Corda DB Failure Test CorDapp" + versionId 1 + vendor "R3" + licence "Open Source (Apache 2)" + } + signing { + enabled false + } } \ No newline at end of file From 825a970b92840082f2479a694a1ea1c10f63863f Mon Sep 17 00:00:00 2001 From: Shams Asari Date: Mon, 21 Aug 2023 09:49:39 +0100 Subject: [PATCH 062/126] ENT-6750: Checkpoint serialisation to support primitive void.class (#7448) --- .../nodeapi/internal/serialization/kryo/Kryo.kt | 2 +- .../internal/serialization/kryo/KryoTests.kt | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/kryo/Kryo.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/kryo/Kryo.kt index 166a10fdf3..d747c23b97 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/kryo/Kryo.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/kryo/Kryo.kt @@ -432,7 +432,7 @@ object LoggerSerializer : Serializer() { object ClassSerializer : Serializer>() { override fun read(kryo: Kryo, input: Input, type: Class>): Class<*> { val className = input.readString() - return Class.forName(className, true, kryo.classLoader) + return if (className == "void") Void.TYPE else Class.forName(className, true, kryo.classLoader) } override fun write(kryo: Kryo, output: Output, clazz: Class<*>) { diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/kryo/KryoTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/kryo/KryoTests.kt index 15a98d1552..2f070a4d24 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/kryo/KryoTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/kryo/KryoTests.kt @@ -351,6 +351,20 @@ class KryoTests(private val compression: CordaSerializationEncoding?) { assertEquals(randomHash, exception2.requested) } + @Test(timeout=300_000) + fun `serialize - deserialize primative void`() { + val original = JavaVoidHolder() + val roundtrip = original.checkpointSerialize(context).checkpointDeserialize(context) + assertThat(roundtrip.voidClass).isEqualTo(original.voidClass) + } + + class JavaVoidHolder { + val voidClass: Class = Void.TYPE + init { + check(voidClass.name == "void") // Sanity check to make sure we're dealing with the primitive void + } + } + @Test(timeout=300_000) fun `compression has the desired effect`() { compression ?: return @@ -373,6 +387,7 @@ class KryoTests(private val compression: CordaSerializationEncoding?) { @Test(timeout=300_000) fun `compression reduces number of bytes significantly`() { + @Suppress("unused") class Holder(val holder: ByteArray) val obj = Holder(ByteArray(20000)) From 3f067d90020d6a49b355534afbac64be49a28cfd Mon Sep 17 00:00:00 2001 From: Jose Coll Date: Mon, 21 Aug 2023 12:28:23 +0100 Subject: [PATCH 063/126] Check sender and receiver timestamps are same. --- .../coretests/flows/FinalityFlowTests.kt | 36 ++++++++++++++----- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt b/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt index f0e8ad93b9..013fa07d53 100644 --- a/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt +++ b/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt @@ -352,16 +352,17 @@ class FinalityFlowTests : WithFinality { assertThat(aliceNode.services.validatedTransactions.getTransaction(stx.id)).isNotNull assertThat(bobNode.services.validatedTransactions.getTransaction(stx.id)).isNotNull - getSenderRecoveryData(stx.id, aliceNode.database).apply { + val sdrs = getSenderRecoveryData(stx.id, aliceNode.database).apply { assertEquals(1, this.size) assertEquals(StatesToRecord.ALL_VISIBLE, this[0].statesToRecord) assertEquals(BOB_NAME.hashCode().toLong(), this[0].peerPartyId) } - getReceiverRecoveryData(stx.id, bobNode, aliceNode).apply { + val rdr = getReceiverRecoveryData(stx.id, bobNode, aliceNode).apply { assertEquals(StatesToRecord.ONLY_RELEVANT, this?.statesToRecord) assertEquals(aliceNode.info.singleIdentity().name.hashCode().toLong(), this?.initiatorPartyId) assertEquals(mapOf(BOB_NAME.hashCode().toLong() to StatesToRecord.ALL_VISIBLE), this?.peersToStatesToRecord) } + validateSenderAndReceiverTimestamps(sdrs, rdr!!) } @Test(timeout=300_000) @@ -382,20 +383,21 @@ class FinalityFlowTests : WithFinality { assertThat(bobNode.services.validatedTransactions.getTransaction(stx.id)).isNotNull assertThat(charlieNode.services.validatedTransactions.getTransaction(stx.id)).isNotNull - getSenderRecoveryData(stx.id, aliceNode.database).apply { + val sdrs = getSenderRecoveryData(stx.id, aliceNode.database).apply { assertEquals(2, this.size) assertEquals(StatesToRecord.ONLY_RELEVANT, this[0].statesToRecord) assertEquals(BOB_NAME.hashCode().toLong(), this[0].peerPartyId) assertEquals(StatesToRecord.ALL_VISIBLE, this[1].statesToRecord) assertEquals(CHARLIE_NAME.hashCode().toLong(), this[1].peerPartyId) } - getReceiverRecoveryData(stx.id, bobNode, aliceNode).apply { + val rdr = getReceiverRecoveryData(stx.id, bobNode, aliceNode).apply { assertEquals(StatesToRecord.ONLY_RELEVANT, this?.statesToRecord) assertEquals(aliceNode.info.singleIdentity().name.hashCode().toLong(), this?.initiatorPartyId) // note: Charlie assertion here is using the hinted StatesToRecord value passed to it from Alice assertEquals(mapOf(BOB_NAME.hashCode().toLong() to StatesToRecord.ONLY_RELEVANT, CHARLIE_NAME.hashCode().toLong() to StatesToRecord.ALL_VISIBLE), this?.peersToStatesToRecord) } + validateSenderAndReceiverTimestamps(sdrs, rdr!!) // exercise the new FinalityFlow observerSessions constructor parameter val stx3 = aliceNode.startFlowAndRunNetwork(CashPaymentWithObserversFlow( @@ -408,9 +410,24 @@ class FinalityFlowTests : WithFinality { assertThat(bobNode.services.validatedTransactions.getTransaction(stx3.id)).isNotNull assertThat(charlieNode.services.validatedTransactions.getTransaction(stx3.id)).isNotNull - assertEquals(2, getSenderRecoveryData(stx3.id, aliceNode.database).size) - assertThat(getReceiverRecoveryData(stx3.id, bobNode, aliceNode)).isNotNull - assertThat(getReceiverRecoveryData(stx3.id, charlieNode, aliceNode)).isNotNull + val senderDistributionRecords = getSenderRecoveryData(stx3.id, aliceNode.database).apply { + assertEquals(2, this.size) + assertEquals(this[0].timestamp, this[1].timestamp) + } + getReceiverRecoveryData(stx3.id, bobNode, aliceNode).apply { + assertThat(getReceiverRecoveryData(stx3.id, bobNode, aliceNode)).isNotNull + assertEquals(senderDistributionRecords[0].timestamp, this!!.timestamp) + } + getReceiverRecoveryData(stx3.id, charlieNode, aliceNode).apply { + assertThat(getReceiverRecoveryData(stx3.id, charlieNode, aliceNode)).isNotNull + assertEquals(senderDistributionRecords[0].timestamp, this!!.timestamp) + } + } + + private fun validateSenderAndReceiverTimestamps(sdrs: List, rdr: ReceiverDistributionRecord) { + sdrs.map { + assertEquals(it.timestamp, rdr.timestamp) + } } @Test(timeout=300_000) @@ -426,16 +443,17 @@ class FinalityFlowTests : WithFinality { assertThat(aliceNode.services.validatedTransactions.getTransaction(stx.id)).isNotNull assertThat(bobNode.services.validatedTransactions.getTransaction(stx.id)).isNotNull - getSenderRecoveryData(stx.id, aliceNode.database).apply { + val sdr = getSenderRecoveryData(stx.id, aliceNode.database).apply { assertEquals(1, this.size) assertEquals(StatesToRecord.ONLY_RELEVANT, this[0].statesToRecord) assertEquals(BOB_NAME.hashCode().toLong(), this[0].peerPartyId) } - getReceiverRecoveryData(stx.id, bobNode, aliceNode).apply { + val rdr = getReceiverRecoveryData(stx.id, bobNode, aliceNode).apply { assertEquals(StatesToRecord.ONLY_RELEVANT, this?.statesToRecord) assertEquals(aliceNode.info.singleIdentity().name.hashCode().toLong(), this?.initiatorPartyId) assertEquals(mapOf(BOB_NAME.hashCode().toLong() to StatesToRecord.ONLY_RELEVANT), this?.peersToStatesToRecord) } + validateSenderAndReceiverTimestamps(sdr, rdr!!) } private fun getSenderRecoveryData(id: SecureHash, database: CordaPersistence): List { From 4a7a4eb5bb0c5e4ae469534760ff6a0f073de33b Mon Sep 17 00:00:00 2001 From: Shams Asari Date: Tue, 22 Aug 2023 11:14:37 +0100 Subject: [PATCH 064/126] ENT-9876: Encrypting the ledger recovery participant distribution list (#7423) --- .../coretests/flows/FinalityFlowTests.kt | 72 ++++--- .../core/internal/ServiceHubCoreInternal.kt | 6 +- .../nodeapi/internal/crypto/AesEncryption.kt | 65 ++++++ .../internal/crypto/AesEncryptionTest.kt | 73 +++++++ .../net/corda/node/internal/AbstractNode.kt | 5 +- .../corda/node/services/EncryptionService.kt | 42 ++++ .../node/services/api/ServiceHubInternal.kt | 12 +- .../persistence/AesDbEncryptionService.kt | 158 ++++++++++++++ .../persistence/DBTransactionStorage.kt | 8 +- .../DBTransactionStorageLedgerRecovery.kt | 204 +++++++----------- .../persistence/HashedDistributionList.kt | 104 +++++++++ .../node/services/schema/NodeSchemaService.kt | 6 +- .../migration/node-core.changelog-master.xml | 1 + .../migration/node-core.changelog-v26.xml | 28 +++ .../node/messaging/TwoPartyTradeFlowTests.kt | 12 +- .../persistence/AesDbEncryptionServiceTest.kt | 134 ++++++++++++ ...DBTransactionStorageLedgerRecoveryTests.kt | 113 +++++----- .../node/internal/MockEncryptionService.kt | 39 ++++ .../node/internal/MockTransactionStorage.kt | 8 +- 19 files changed, 873 insertions(+), 217 deletions(-) create mode 100644 node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/AesEncryption.kt create mode 100644 node-api/src/test/kotlin/net/corda/nodeapi/internal/crypto/AesEncryptionTest.kt create mode 100644 node/src/main/kotlin/net/corda/node/services/EncryptionService.kt create mode 100644 node/src/main/kotlin/net/corda/node/services/persistence/AesDbEncryptionService.kt create mode 100644 node/src/main/kotlin/net/corda/node/services/persistence/HashedDistributionList.kt create mode 100644 node/src/main/resources/migration/node-core.changelog-v26.xml create mode 100644 node/src/test/kotlin/net/corda/node/services/persistence/AesDbEncryptionServiceTest.kt create mode 100644 testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/MockEncryptionService.kt diff --git a/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt b/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt index a2c7124b74..49628a8307 100644 --- a/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt +++ b/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt @@ -51,6 +51,9 @@ import net.corda.finance.test.flows.CashIssueWithObserversFlow import net.corda.finance.test.flows.CashPaymentWithObserversFlow import net.corda.node.services.persistence.DBTransactionStorage import net.corda.node.services.persistence.DBTransactionStorageLedgerRecovery +import net.corda.node.services.persistence.DBTransactionStorageLedgerRecovery.DBReceiverDistributionRecord +import net.corda.node.services.persistence.DBTransactionStorageLedgerRecovery.DBSenderDistributionRecord +import net.corda.node.services.persistence.HashedDistributionList import net.corda.node.services.persistence.ReceiverDistributionRecord import net.corda.node.services.persistence.SenderDistributionRecord import net.corda.nodeapi.internal.persistence.CordaPersistence @@ -66,7 +69,6 @@ import net.corda.testing.node.internal.FINANCE_WORKFLOWS_CORDAPP import net.corda.testing.node.internal.InternalMockNetwork import net.corda.testing.node.internal.InternalMockNodeParameters import net.corda.testing.node.internal.MOCK_VERSION_INFO -import net.corda.testing.node.internal.MockCryptoService import net.corda.testing.node.internal.TestCordappInternal import net.corda.testing.node.internal.TestStartedNode import net.corda.testing.node.internal.cordappWithPackages @@ -75,6 +77,7 @@ import net.corda.testing.node.internal.findCordapp import org.assertj.core.api.Assertions.assertThat import org.junit.After import org.junit.Test +import org.junit.jupiter.api.assertThrows import java.sql.SQLException import java.util.Random import kotlin.test.assertEquals @@ -239,7 +242,7 @@ class FinalityFlowTests : WithFinality { session.createQuery( "from ${DBTransactionStorage.DBTransaction::class.java.name} where txId = :transactionId", DBTransactionStorage.DBTransaction::class.java - ).setParameter("transactionId", stxId.toString()).resultList.map { it } + ).setParameter("transactionId", stxId.toString()).resultList } assertEquals(0, fromDb.size) } @@ -355,10 +358,10 @@ class FinalityFlowTests : WithFinality { assertEquals(StatesToRecord.ALL_VISIBLE, this[0].statesToRecord) assertEquals(BOB_NAME.hashCode().toLong(), this[0].peerPartyId) } - getReceiverRecoveryData(stx.id, bobNode.database).apply { - assertEquals(StatesToRecord.ONLY_RELEVANT, this?.statesToRecord) - assertEquals(aliceNode.info.singleIdentity().name.hashCode().toLong(), this?.initiatorPartyId) - assertEquals(mapOf(BOB_NAME.hashCode().toLong() to StatesToRecord.ALL_VISIBLE), this?.peersToStatesToRecord) + getReceiverRecoveryData(stx.id, bobNode, aliceNode).let { (record, distList) -> + assertEquals(StatesToRecord.ONLY_RELEVANT, distList.senderStatesToRecord) + assertEquals(aliceNode.info.singleIdentity().name.hashCode().toLong(), record.initiatorPartyId) + assertEquals(mapOf(BOB_NAME.hashCode().toLong() to StatesToRecord.ALL_VISIBLE), distList.peerHashToStatesToRecord) } } @@ -387,11 +390,13 @@ class FinalityFlowTests : WithFinality { assertEquals(StatesToRecord.ALL_VISIBLE, this[1].statesToRecord) assertEquals(CHARLIE_NAME.hashCode().toLong(), this[1].peerPartyId) } - getReceiverRecoveryData(stx.id, bobNode.database).apply { - assertEquals(aliceNode.info.singleIdentity().name.hashCode().toLong(), this?.initiatorPartyId) + getReceiverRecoveryData(stx.id, bobNode, aliceNode).let { (record, distList) -> + assertEquals(aliceNode.info.singleIdentity().name.hashCode().toLong(), record.initiatorPartyId) // note: Charlie assertion here is using the hinted StatesToRecord value passed to it from Alice - assertEquals(mapOf(BOB_NAME.hashCode().toLong() to StatesToRecord.ONLY_RELEVANT, - CHARLIE_NAME.hashCode().toLong() to StatesToRecord.ALL_VISIBLE), this?.peersToStatesToRecord) + assertEquals(mapOf( + BOB_NAME.hashCode().toLong() to StatesToRecord.ONLY_RELEVANT, + CHARLIE_NAME.hashCode().toLong() to StatesToRecord.ALL_VISIBLE + ), distList.peerHashToStatesToRecord) } // exercise the new FinalityFlow observerSessions constructor parameter @@ -406,8 +411,8 @@ class FinalityFlowTests : WithFinality { assertThat(charlieNode.services.validatedTransactions.getTransaction(stx3.id)).isNotNull assertEquals(2, getSenderRecoveryData(stx3.id, aliceNode.database).size) - assertThat(getReceiverRecoveryData(stx3.id, bobNode.database)).isNotNull - assertThat(getReceiverRecoveryData(stx3.id, charlieNode.database)).isNotNull + assertThat(getReceiverRecoveryData(stx3.id, bobNode, aliceNode)).isNotNull + assertThat(getReceiverRecoveryData(stx3.id, charlieNode, aliceNode)).isNotNull } @Test(timeout=300_000) @@ -428,30 +433,44 @@ class FinalityFlowTests : WithFinality { assertEquals(StatesToRecord.ONLY_RELEVANT, this[0].statesToRecord) assertEquals(BOB_NAME.hashCode().toLong(), this[0].peerPartyId) } - getReceiverRecoveryData(stx.id, bobNode.database).apply { - assertEquals(aliceNode.info.singleIdentity().name.hashCode().toLong(), this?.initiatorPartyId) - assertEquals(mapOf(BOB_NAME.hashCode().toLong() to StatesToRecord.ONLY_RELEVANT), this?.peersToStatesToRecord) + getReceiverRecoveryData(stx.id, bobNode, aliceNode).let { (record, distList) -> + assertEquals(aliceNode.info.singleIdentity().name.hashCode().toLong(), record.initiatorPartyId) + assertEquals(mapOf(BOB_NAME.hashCode().toLong() to StatesToRecord.ONLY_RELEVANT), distList.peerHashToStatesToRecord) } } private fun getSenderRecoveryData(id: SecureHash, database: CordaPersistence): List { val fromDb = database.transaction { session.createQuery( - "from ${DBTransactionStorageLedgerRecovery.DBSenderDistributionRecord::class.java.name} where txId = :transactionId", - DBTransactionStorageLedgerRecovery.DBSenderDistributionRecord::class.java - ).setParameter("transactionId", id.toString()).resultList.map { it } + "from ${DBSenderDistributionRecord::class.java.name} where txId = :transactionId", + DBSenderDistributionRecord::class.java + ).setParameter("transactionId", id.toString()).resultList } return fromDb.map { it.toSenderDistributionRecord() }.also { println("SenderDistributionRecord\n$it") } } - private fun getReceiverRecoveryData(id: SecureHash, database: CordaPersistence): ReceiverDistributionRecord? { - val fromDb = database.transaction { + private fun getReceiverRecoveryData(txId: SecureHash, + receiver: TestStartedNode, + sender: TestStartedNode): Pair { + val fromDb = receiver.database.transaction { session.createQuery( - "from ${DBTransactionStorageLedgerRecovery.DBReceiverDistributionRecord::class.java.name} where txId = :transactionId", - DBTransactionStorageLedgerRecovery.DBReceiverDistributionRecord::class.java - ).setParameter("transactionId", id.toString()).resultList.map { it } + "from ${DBReceiverDistributionRecord::class.java.name} where txId = :transactionId", + DBReceiverDistributionRecord::class.java + ).setParameter("transactionId", txId.toString()).singleResult } - return fromDb.singleOrNull()?.toReceiverDistributionRecord(MockCryptoService(emptyMap())).also { println("ReceiverDistributionRecord\n$it") } + + // The receiver should not be able to decrypt the distribution list + assertThrows { + receiver.decryptReceiverDistributionRecord(fromDb) + } + + // Only the sender can + return sender.decryptReceiverDistributionRecord(fromDb) + } + + private fun TestStartedNode.decryptReceiverDistributionRecord(dbRecord: DBReceiverDistributionRecord): Pair { + val hashedDistList = (internals.transactionStorage as DBTransactionStorageLedgerRecovery).decryptHashedDistributionList(dbRecord.distributionList) + return Pair(dbRecord.toReceiverDistributionRecord(), hashedDistList) } @StartableByRPC @@ -482,6 +501,7 @@ class FinalityFlowTests : WithFinality { } } + @Suppress("unused") @InitiatedBy(SpendFlow::class) class AcceptSpendFlow(private val otherSide: FlowSession) : FlowLogic() { @@ -518,6 +538,7 @@ class FinalityFlowTests : WithFinality { } } + @Suppress("unused") @InitiatedBy(SpeedySpendFlow::class) class AcceptSpeedySpendFlow(private val otherSideSession: FlowSession) : FlowLogic() { @@ -551,7 +572,7 @@ class FinalityFlowTests : WithFinality { } } - class FinaliseSpeedySpendFlow(val id: SecureHash, val sigs: List) : FlowLogic() { + class FinaliseSpeedySpendFlow(val id: SecureHash, private val sigs: List) : FlowLogic() { @Suspendable override fun call(): SignedTransaction { @@ -577,6 +598,7 @@ class FinalityFlowTests : WithFinality { } } + @Suppress("unused") @InitiatedBy(MimicFinalityFailureFlow::class) class TriggerReceiveFinalityFlow(private val otherSide: FlowSession) : FlowLogic() { @Suspendable diff --git a/core/src/main/kotlin/net/corda/core/internal/ServiceHubCoreInternal.kt b/core/src/main/kotlin/net/corda/core/internal/ServiceHubCoreInternal.kt index 803b52d300..27f05c9f2f 100644 --- a/core/src/main/kotlin/net/corda/core/internal/ServiceHubCoreInternal.kt +++ b/core/src/main/kotlin/net/corda/core/internal/ServiceHubCoreInternal.kt @@ -80,7 +80,11 @@ interface ServiceHubCoreInternal : ServiceHub { * @param receiverStatesToRecord The StatesToRecord value of the receiver. * @param encryptedDistributionList encrypted distribution list (hashed peers -> StatesToRecord values) */ - fun recordReceiverTransactionRecoveryMetadata(txnId: SecureHash, sender: CordaX500Name, receiver: CordaX500Name, receiverStatesToRecord: StatesToRecord, encryptedDistributionList: ByteArray) + fun recordReceiverTransactionRecoveryMetadata(txnId: SecureHash, + sender: CordaX500Name, + receiver: CordaX500Name, + receiverStatesToRecord: StatesToRecord, + encryptedDistributionList: ByteArray) } interface TransactionsResolver { diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/AesEncryption.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/AesEncryption.kt new file mode 100644 index 0000000000..f9b36ffd07 --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/AesEncryption.kt @@ -0,0 +1,65 @@ +package net.corda.nodeapi.internal.crypto + +import net.corda.core.crypto.secureRandomBytes +import java.nio.ByteBuffer +import javax.crypto.Cipher +import javax.crypto.SecretKey +import javax.crypto.spec.GCMParameterSpec +import javax.crypto.spec.SecretKeySpec + +object AesEncryption { + const val KEY_SIZE_BYTES = 16 + internal const val IV_SIZE_BYTES = 12 + private const val TAG_SIZE_BYTES = 16 + private const val TAG_SIZE_BITS = TAG_SIZE_BYTES * 8 + + /** + * Generates a random 128-bit AES key. + */ + fun randomKey(): SecretKey { + return SecretKeySpec(secureRandomBytes(KEY_SIZE_BYTES), "AES") + } + + /** + * Encrypt the given [plaintext] with AES using the given [aesKey]. + * + * An optional public [additionalData] bytes can also be provided which will be authenticated alongside the ciphertext but not encrypted. + * This may be metadata for example. The same authenticated data bytes must be provided to [decrypt] to be able to decrypt the + * ciphertext. Typically these bytes are serialised alongside the ciphertext. Since it's authenticated in the ciphertext, it cannot be + * modified undetected. + */ + fun encrypt(aesKey: SecretKey, plaintext: ByteArray, additionalData: ByteArray? = null): ByteArray { + val cipher = Cipher.getInstance("AES/GCM/NoPadding") + val iv = secureRandomBytes(IV_SIZE_BYTES) // Never use the same IV with the same key! + cipher.init(Cipher.ENCRYPT_MODE, aesKey, GCMParameterSpec(TAG_SIZE_BITS, iv)) + val buffer = ByteBuffer.allocate(IV_SIZE_BYTES + plaintext.size + TAG_SIZE_BYTES) + buffer.put(iv) + if (additionalData != null) { + cipher.updateAAD(additionalData) + } + cipher.doFinal(ByteBuffer.wrap(plaintext), buffer) + return buffer.array() + } + + fun encrypt(aesKey: ByteArray, plaintext: ByteArray, additionalData: ByteArray? = null): ByteArray { + return encrypt(SecretKeySpec(aesKey, "AES"), plaintext, additionalData) + } + + /** + * Decrypt ciphertext that was encrypted with the same key using [encrypt]. + * + * If additional data was used for the encryption then it must also be provided. If doesn't match then the decryption will fail. + */ + fun decrypt(aesKey: SecretKey, ciphertext: ByteArray, additionalData: ByteArray? = null): ByteArray { + val cipher = Cipher.getInstance("AES/GCM/NoPadding") + cipher.init(Cipher.DECRYPT_MODE, aesKey, GCMParameterSpec(TAG_SIZE_BITS, ciphertext, 0, IV_SIZE_BYTES)) + if (additionalData != null) { + cipher.updateAAD(additionalData) + } + return cipher.doFinal(ciphertext, IV_SIZE_BYTES, ciphertext.size - IV_SIZE_BYTES) + } + + fun decrypt(aesKey: ByteArray, ciphertext: ByteArray, additionalData: ByteArray? = null): ByteArray { + return decrypt(SecretKeySpec(aesKey, "AES"), ciphertext, additionalData) + } +} diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/crypto/AesEncryptionTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/crypto/AesEncryptionTest.kt new file mode 100644 index 0000000000..d3b1ded638 --- /dev/null +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/crypto/AesEncryptionTest.kt @@ -0,0 +1,73 @@ +package net.corda.nodeapi.internal.crypto + +import net.corda.core.crypto.secureRandomBytes +import net.corda.nodeapi.internal.crypto.AesEncryption.IV_SIZE_BYTES +import net.corda.nodeapi.internal.crypto.AesEncryption.KEY_SIZE_BYTES +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatExceptionOfType +import org.junit.Test +import java.security.GeneralSecurityException + +class AesEncryptionTest { + private val aesKey = secureRandomBytes(KEY_SIZE_BYTES) + private val plaintext = secureRandomBytes(257) // Intentionally not a power of 2 + + @Test(timeout = 300_000) + fun `ciphertext can be decrypted using the same key`() { + val ciphertext = AesEncryption.encrypt(aesKey, plaintext) + assertThat(String(ciphertext)).doesNotContain(String(plaintext)) + val decrypted = AesEncryption.decrypt(aesKey, ciphertext) + assertThat(decrypted).isEqualTo(plaintext) + } + + @Test(timeout = 300_000) + fun `ciphertext with authenticated data can be decrypted using the same key`() { + val ciphertext = AesEncryption.encrypt(aesKey, plaintext, "Extra public data".toByteArray()) + assertThat(String(ciphertext)).doesNotContain(String(plaintext)) + val decrypted = AesEncryption.decrypt(aesKey, ciphertext, "Extra public data".toByteArray()) + assertThat(decrypted).isEqualTo(plaintext) + } + + @Test(timeout = 300_000) + fun `ciphertext cannot be decrypted with different authenticated data`() { + val ciphertext = AesEncryption.encrypt(aesKey, plaintext, "Extra public data".toByteArray()) + assertThat(String(ciphertext)).doesNotContain(String(plaintext)) + assertThatExceptionOfType(GeneralSecurityException::class.java).isThrownBy { + AesEncryption.decrypt(aesKey, ciphertext, "Different public data".toByteArray()) + } + } + + @Test(timeout = 300_000) + fun `ciphertext cannot be decrypted with different key`() { + val ciphertext = AesEncryption.encrypt(aesKey, plaintext) + for (index in aesKey.indices) { + aesKey[index]-- + assertThatExceptionOfType(GeneralSecurityException::class.java).isThrownBy { + AesEncryption.decrypt(aesKey, ciphertext) + } + aesKey[index]++ + } + } + + @Test(timeout = 300_000) + fun `corrupted ciphertext cannot be decrypted`() { + val ciphertext = AesEncryption.encrypt(aesKey, plaintext) + for (index in ciphertext.indices) { + ciphertext[index]-- + assertThatExceptionOfType(GeneralSecurityException::class.java).isThrownBy { + AesEncryption.decrypt(aesKey, ciphertext) + } + ciphertext[index]++ + } + } + + @Test(timeout = 300_000) + fun `encrypting same plainttext twice with same key does not produce same ciphertext`() { + val first = AesEncryption.encrypt(aesKey, plaintext) + val second = AesEncryption.encrypt(aesKey, plaintext) + // The IV should be different + assertThat(first.take(IV_SIZE_BYTES)).isNotEqualTo(second.take(IV_SIZE_BYTES)) + // Which should cause the encrypted bytes to be different as well + assertThat(first.drop(IV_SIZE_BYTES)).isNotEqualTo(second.drop(IV_SIZE_BYTES)) + } +} diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index 51da915a64..72c31fc33c 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -125,6 +125,7 @@ import net.corda.node.services.persistence.AbstractPartyToX500NameAsStringConver import net.corda.node.services.persistence.AttachmentStorageInternal import net.corda.node.services.persistence.DBCheckpointPerformanceRecorder import net.corda.node.services.persistence.DBCheckpointStorage +import net.corda.node.services.persistence.AesDbEncryptionService import net.corda.node.services.persistence.DBTransactionMappingStorage import net.corda.node.services.persistence.DBTransactionStorageLedgerRecovery import net.corda.node.services.persistence.NodeAttachmentService @@ -278,6 +279,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, val networkMapCache = PersistentNetworkMapCache(cacheFactory, database, identityService).tokenize() val partyInfoCache = PersistentPartyInfoCache(networkMapCache, cacheFactory, database) + val encryptionService = AesDbEncryptionService(database) @Suppress("LeakingThis") val cryptoService = makeCryptoService() @Suppress("LeakingThis") @@ -638,6 +640,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, verifyCheckpointsCompatible(frozenTokenizableServices) partyInfoCache.start() + encryptionService.start(nodeInfo.legalIdentities[0]) /* Note the .get() at the end of the distributeEvent call, below. This will block until all Corda Services have returned from processing the event, allowing a service to prevent the @@ -1060,7 +1063,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, } protected open fun makeTransactionStorage(transactionCacheSizeBytes: Long): WritableTransactionStorage { - return DBTransactionStorageLedgerRecovery(database, cacheFactory, platformClock, cryptoService, partyInfoCache) + return DBTransactionStorageLedgerRecovery(database, cacheFactory, platformClock, encryptionService, partyInfoCache) } protected open fun makeNetworkParametersStorage(): NetworkParametersStorage { diff --git a/node/src/main/kotlin/net/corda/node/services/EncryptionService.kt b/node/src/main/kotlin/net/corda/node/services/EncryptionService.kt new file mode 100644 index 0000000000..85dea166e0 --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/services/EncryptionService.kt @@ -0,0 +1,42 @@ +package net.corda.node.services + +/** + * A service for encrypting data. This abstraction does not mandate any security properties except the same service instance will be + * able to decrypt ciphertext encrypted by it. Further security properties are defined by the implementations. This includes the encryption + * protocol used. + */ +interface EncryptionService { + /** + * Encrypt the given [plaintext]. The encryption key used is dependent on the implementation. The returned ciphertext can be decrypted + * using [decrypt]. + * + * An optional public [additionalData] bytes can also be provided which will be authenticated (thus tamperproof) alongside the + * ciphertext but not encrypted. It will be incorporated into the returned bytes in an implementation dependent fashion. + */ + fun encrypt(plaintext: ByteArray, additionalData: ByteArray? = null): ByteArray + + /** + * Decrypt ciphertext that was encrypted using [encrypt] and return the original plaintext plus the additional data authenticated (if + * present). The service will select the correct encryption key to use. + */ + fun decrypt(ciphertext: ByteArray): PlaintextAndAAD + + /** + * Extracts the (unauthenticated) additional data, if present, from the given [ciphertext]. This is the public data that would have been + * given at encryption time. + * + * Note, this method does not verify if the data was tampered with, and hence is unauthenticated. To have it authenticated requires + * calling [decrypt]. This is still useful however, as it doesn't require the encryption key, and so a third-party can view the + * additional data without needing access to the key. + */ + fun extractUnauthenticatedAdditionalData(ciphertext: ByteArray): ByteArray? + + + /** + * Represents the decrypted plaintext and the optional authenticated additional data bytes. + */ + class PlaintextAndAAD(val plaintext: ByteArray, val authenticatedAdditionalData: ByteArray?) { + operator fun component1() = plaintext + operator fun component2() = authenticatedAdditionalData + } +} diff --git a/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt b/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt index a5ef5f054a..962f7a0664 100644 --- a/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt +++ b/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt @@ -372,22 +372,26 @@ interface WritableTransactionStorage : TransactionStorage { /** * Records Sender [TransactionMetadata] for a given txnId. * - * @param id The SecureHash of a transaction. + * @param txId The SecureHash of a transaction. * @param metadata The recovery metadata associated with a transaction. * @return encrypted distribution list (hashed peers -> StatesToRecord values). */ - fun addSenderTransactionRecoveryMetadata(id: SecureHash, metadata: TransactionMetadata): ByteArray? + fun addSenderTransactionRecoveryMetadata(txId: SecureHash, metadata: TransactionMetadata): ByteArray? /** * Records Received [TransactionMetadata] for a given txnId. * - * @param id The SecureHash of a transaction. + * @param txId The SecureHash of a transaction. * @param sender The sender of the transaction. * @param receiver The receiver of the transaction. * @param receiverStatesToRecord The StatesToRecord value of the receiver. * @param encryptedDistributionList encrypted distribution list (hashed peers -> StatesToRecord values) */ - fun addReceiverTransactionRecoveryMetadata(id: SecureHash, sender: CordaX500Name, receiver: CordaX500Name, receiverStatesToRecord: StatesToRecord, encryptedDistributionList: ByteArray) + fun addReceiverTransactionRecoveryMetadata(txId: SecureHash, + sender: CordaX500Name, + receiver: CordaX500Name, + receiverStatesToRecord: StatesToRecord, + encryptedDistributionList: ByteArray) /** * Removes an un-notarised transaction (with a status of *MISSING_TRANSACTION_SIG*) from the data store. diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/AesDbEncryptionService.kt b/node/src/main/kotlin/net/corda/node/services/persistence/AesDbEncryptionService.kt new file mode 100644 index 0000000000..0b4c14638b --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/services/persistence/AesDbEncryptionService.kt @@ -0,0 +1,158 @@ +package net.corda.node.services.persistence + +import net.corda.core.crypto.newSecureRandom +import net.corda.core.identity.Party +import net.corda.core.internal.copyBytes +import net.corda.core.serialization.SingletonSerializeAsToken +import net.corda.node.services.EncryptionService +import net.corda.nodeapi.internal.crypto.AesEncryption +import net.corda.nodeapi.internal.persistence.CordaPersistence +import net.corda.nodeapi.internal.persistence.NODE_DATABASE_PREFIX +import org.hibernate.annotations.Type +import java.nio.ByteBuffer +import java.security.Key +import java.security.MessageDigest +import java.util.UUID +import javax.crypto.Cipher +import javax.crypto.SecretKey +import javax.crypto.spec.SecretKeySpec +import javax.persistence.Column +import javax.persistence.Entity +import javax.persistence.Id +import javax.persistence.Table + +/** + * [EncryptionService] which uses AES keys stored in the node database. A random key is chosen for encryption, and the resultant ciphertext + * encodes the key used so that it can be decrypted without needing further information. + * + * **Storing encryption keys in a database is not secure, and so only use this service if the data being encrypted is also stored + * unencrypted in the same database.** + * + * To obfuscate the keys, they are stored wrapped using another AES key (called the wrapping key or key-encryption-key) derived from the + * node's legal identity. This is not a security measure; it's only meant to reduce the impact of accidental leakage. + */ +// TODO Add support for key expiry +class AesDbEncryptionService(private val database: CordaPersistence) : EncryptionService, SingletonSerializeAsToken() { + companion object { + private const val INITIAL_KEY_COUNT = 10 + private const val UUID_BYTES = 16 + private const val VERSION_TAG = 1 + } + + private val aesKeys = ArrayList>() + + fun start(ourIdentity: Party) { + database.transaction { + val criteria = session.criteriaBuilder.createQuery(EncryptionKeyRecord::class.java) + criteria.select(criteria.from(EncryptionKeyRecord::class.java)) + val dbKeyRecords = session.createQuery(criteria).resultList + val keyWrapper = Cipher.getInstance("AESWrap") + if (dbKeyRecords.isEmpty()) { + repeat(INITIAL_KEY_COUNT) { + val keyId = UUID.randomUUID() + val aesKey = AesEncryption.randomKey() + aesKeys += Pair(keyId, aesKey) + val wrappedKey = with(keyWrapper) { + init(Cipher.WRAP_MODE, createKEK(ourIdentity, keyId)) + wrap(aesKey) + } + session.save(EncryptionKeyRecord(keyId = keyId, keyMaterial = wrappedKey)) + } + } else { + for (dbKeyRecord in dbKeyRecords) { + val aesKey = with(keyWrapper) { + init(Cipher.UNWRAP_MODE, createKEK(ourIdentity, dbKeyRecord.keyId)) + unwrap(dbKeyRecord.keyMaterial, "AES", Cipher.SECRET_KEY) as SecretKey + } + aesKeys += Pair(dbKeyRecord.keyId, aesKey) + } + } + } + } + + override fun encrypt(plaintext: ByteArray, additionalData: ByteArray?): ByteArray { + val (keyId, aesKey) = aesKeys[newSecureRandom().nextInt(aesKeys.size)] + val ciphertext = AesEncryption.encrypt(aesKey, plaintext, additionalData) + val buffer = ByteBuffer.allocate(1 + UUID_BYTES + Integer.BYTES + (additionalData?.size ?: 0) + ciphertext.size) + buffer.put(VERSION_TAG.toByte()) + // Prepend the key ID to the returned ciphertext. It's OK that this is not included in the authenticated additional data because + // changing this value will lead to either an non-existent key or an another key which will not be able decrypt the ciphertext. + buffer.putUUID(keyId) + if (additionalData != null) { + buffer.putInt(additionalData.size) + buffer.put(additionalData) + } else { + buffer.putInt(0) + } + buffer.put(ciphertext) + return buffer.array() + } + + override fun decrypt(ciphertext: ByteArray): EncryptionService.PlaintextAndAAD { + val buffer = wrap(ciphertext) + val keyId = buffer.getUUID() + val aesKey = requireNotNull(aesKeys.find { it.first == keyId }?.second) { "Unable to decrypt" } + val additionalData = buffer.getAdditionaData() + val plaintext = AesEncryption.decrypt(aesKey, buffer.copyBytes(), additionalData) + // Only now is the additional data authenticated + return EncryptionService.PlaintextAndAAD(plaintext, additionalData) + } + + override fun extractUnauthenticatedAdditionalData(ciphertext: ByteArray): ByteArray? { + val buffer = wrap(ciphertext) + buffer.position(buffer.position() + UUID_BYTES) + return buffer.getAdditionaData() + } + + private fun wrap(ciphertext: ByteArray): ByteBuffer { + val buffer = ByteBuffer.wrap(ciphertext) + val version = buffer.get().toInt() + require(version == VERSION_TAG) { "Unknown version $version" } + return buffer + } + + private fun ByteBuffer.getAdditionaData(): ByteArray? { + val additionalDataSize = getInt() + return if (additionalDataSize > 0) ByteArray(additionalDataSize).also { get(it) } else null + } + + private fun UUID.toByteArray(): ByteArray { + val buffer = ByteBuffer.allocate(UUID_BYTES) + buffer.putUUID(this) + return buffer.array() + } + + /** + * Derive the key-encryption-key (KEK) from the the node's identity and the persisted key's ID. + */ + private fun createKEK(ourIdentity: Party, keyId: UUID): Key { + val digest = MessageDigest.getInstance("SHA-256") + digest.update(ourIdentity.name.x500Principal.encoded) + digest.update(keyId.toByteArray()) + return SecretKeySpec(digest.digest(), 0, AesEncryption.KEY_SIZE_BYTES, "AES") + } + + + @Entity + @Table(name = "${NODE_DATABASE_PREFIX}aes_encryption_keys") + class EncryptionKeyRecord( + @Id + @Type(type = "uuid-char") + @Column(name = "key_id", nullable = false) + val keyId: UUID, + + @Column(name = "key_material", nullable = false) + val keyMaterial: ByteArray + ) +} + +internal fun ByteBuffer.putUUID(uuid: UUID) { + putLong(uuid.mostSignificantBits) + putLong(uuid.leastSignificantBits) +} + +internal fun ByteBuffer.getUUID(): UUID { + val mostSigBits = getLong() + val leastSigBits = getLong() + return UUID(mostSigBits, leastSigBits) +} diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt index 56acdfd61b..1973f9e7c1 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt @@ -215,9 +215,13 @@ open class DBTransactionStorage(private val database: CordaPersistence, cacheFac false } - override fun addSenderTransactionRecoveryMetadata(id: SecureHash, metadata: TransactionMetadata): ByteArray? { return null } + override fun addSenderTransactionRecoveryMetadata(txId: SecureHash, metadata: TransactionMetadata): ByteArray? { return null } - override fun addReceiverTransactionRecoveryMetadata(id: SecureHash, sender: CordaX500Name, receiver: CordaX500Name, receiverStatesToRecord: StatesToRecord, encryptedDistributionList: ByteArray) { } + override fun addReceiverTransactionRecoveryMetadata(txId: SecureHash, + sender: CordaX500Name, + receiver: CordaX500Name, + receiverStatesToRecord: StatesToRecord, + encryptedDistributionList: ByteArray) { } override fun finalizeTransaction(transaction: SignedTransaction) = addTransaction(transaction) { diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecovery.kt b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecovery.kt index 6a064f6bc8..f828537ea9 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecovery.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecovery.kt @@ -5,20 +5,16 @@ import net.corda.core.flows.RecoveryTimeWindow import net.corda.core.flows.TransactionMetadata import net.corda.core.identity.CordaX500Name import net.corda.core.internal.NamedCacheFactory -import net.corda.core.internal.VisibleForTesting import net.corda.core.node.StatesToRecord import net.corda.core.node.services.vault.Sort import net.corda.core.serialization.CordaSerializable +import net.corda.core.utilities.OpaqueBytes import net.corda.node.CordaClock +import net.corda.node.services.EncryptionService import net.corda.node.services.network.PersistentPartyInfoCache -import net.corda.nodeapi.internal.cryptoservice.CryptoService import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.NODE_DATABASE_PREFIX import org.hibernate.annotations.Immutable -import java.io.ByteArrayInputStream -import java.io.ByteArrayOutputStream -import java.io.DataInputStream -import java.io.DataOutputStream import java.io.Serializable import java.time.Instant import java.util.concurrent.atomic.AtomicInteger @@ -30,11 +26,11 @@ import javax.persistence.Id import javax.persistence.Lob import javax.persistence.Table import javax.persistence.criteria.Predicate -import kotlin.streams.toList -class DBTransactionStorageLedgerRecovery(private val database: CordaPersistence, cacheFactory: NamedCacheFactory, +class DBTransactionStorageLedgerRecovery(private val database: CordaPersistence, + cacheFactory: NamedCacheFactory, val clock: CordaClock, - val cryptoService: CryptoService, + private val encryptionService: EncryptionService, private val partyInfoCache: PersistentPartyInfoCache) : DBTransactionStorage(database, cacheFactory, clock) { @Embeddable @Immutable @@ -66,7 +62,6 @@ class DBTransactionStorageLedgerRecovery(private val database: CordaPersistence, /** states to record: NONE, ALL_VISIBLE, ONLY_RELEVANT */ @Column(name = "states_to_record", nullable = false) var statesToRecord: StatesToRecord - ) { fun toSenderDistributionRecord() = SenderDistributionRecord( @@ -80,7 +75,7 @@ class DBTransactionStorageLedgerRecovery(private val database: CordaPersistence, @CordaSerializable @Entity @Table(name = "${NODE_DATABASE_PREFIX}receiver_distribution_records") - data class DBReceiverDistributionRecord( + class DBReceiverDistributionRecord( @EmbeddedId var compositeKey: PersistentKey, @@ -91,20 +86,18 @@ class DBTransactionStorageLedgerRecovery(private val database: CordaPersistence, @Lob @Column(name = "distribution_list", nullable = false) val distributionList: ByteArray -) { - constructor(key: Key, txId: SecureHash, encryptedDistributionList: ByteArray) : - this(PersistentKey(key), - txId = txId.toString(), - distributionList = encryptedDistributionList - ) + ) { + constructor(key: Key, txId: SecureHash, encryptedDistributionList: ByteArray) : this( + PersistentKey(key), + txId.toString(), + encryptedDistributionList + ) - fun toReceiverDistributionRecord(cryptoService: CryptoService): ReceiverDistributionRecord { - val hashedDL = HashedDistributionList.deserialize(cryptoService.decrypt(this.distributionList)) + fun toReceiverDistributionRecord(): ReceiverDistributionRecord { return ReceiverDistributionRecord( SecureHash.parse(this.txId), this.compositeKey.peerPartyId, - hashedDL.peerHashToStatesToRecord, - hashedDL.senderStatesToRecord, + OpaqueBytes(this.distributionList), this.compositeKey.timestamp ) } @@ -130,28 +123,38 @@ class DBTransactionStorageLedgerRecovery(private val database: CordaPersistence, val timestamp: Instant, val timestampDiscriminator: Int = nextDiscriminatorNumber.andIncrement ) { - constructor(key: TimestampKey, partyId: Long): this(partyId = partyId, timestamp = key.timestamp, timestampDiscriminator = key.timestampDiscriminator) + constructor(key: TimestampKey, partyId: Long): this(partyId, key.timestamp, key.timestampDiscriminator) companion object { val nextDiscriminatorNumber = AtomicInteger() } } - override fun addSenderTransactionRecoveryMetadata(id: SecureHash, metadata: TransactionMetadata): ByteArray { + override fun addSenderTransactionRecoveryMetadata(txId: SecureHash, metadata: TransactionMetadata): ByteArray { val senderRecordingTimestamp = clock.instant() return database.transaction { // sender distribution records must be unique per txnId and timestamp val timeDiscriminator = Key.nextDiscriminatorNumber.andIncrement - metadata.distributionList.peersToStatesToRecord.map { (peerCordaX500Name, peerStatesToRecord) -> + metadata.distributionList.peersToStatesToRecord.forEach { peerCordaX500Name, peerStatesToRecord -> val senderDistributionRecord = DBSenderDistributionRecord( - PersistentKey(Key(TimestampKey(senderRecordingTimestamp, timeDiscriminator), partyInfoCache.getPartyIdByCordaX500Name(peerCordaX500Name))), - id.toString(), - peerStatesToRecord) + PersistentKey(Key( + TimestampKey(senderRecordingTimestamp, timeDiscriminator), + partyInfoCache.getPartyIdByCordaX500Name(peerCordaX500Name) + )), + txId.toString(), + peerStatesToRecord + ) session.save(senderDistributionRecord) } - val hashedPeersToStatesToRecord = metadata.distributionList.peersToStatesToRecord.map { (peer, statesToRecord) -> - partyInfoCache.getPartyIdByCordaX500Name(peer) to statesToRecord }.toMap() - val hashedDistributionList = HashedDistributionList(metadata.distributionList.senderStatesToRecord, hashedPeersToStatesToRecord, senderRecordingTimestamp) - cryptoService.encrypt(hashedDistributionList.serialize()) + + val hashedPeersToStatesToRecord = metadata.distributionList.peersToStatesToRecord.mapKeys { (peer) -> + partyInfoCache.getPartyIdByCordaX500Name(peer) + } + val hashedDistributionList = HashedDistributionList( + metadata.distributionList.senderStatesToRecord, + hashedPeersToStatesToRecord, + HashedDistributionList.PublicHeader(senderRecordingTimestamp) + ) + hashedDistributionList.encrypt(encryptionService) } } @@ -160,16 +163,16 @@ class DBTransactionStorageLedgerRecovery(private val database: CordaPersistence, senderStatesToRecord: StatesToRecord, senderRecords: List): List { val senderRecordsByTimestampKey = senderRecords.groupBy { TimestampKey(it.compositeKey.timestamp, it.compositeKey.timestampDiscriminator) } - return senderRecordsByTimestampKey.map { + return senderRecordsByTimestampKey.map { (key) -> val hashedDistributionList = HashedDistributionList( - senderStatesToRecord = senderStatesToRecord, - peerHashToStatesToRecord = senderRecords.map { it.compositeKey.peerPartyId to it.statesToRecord }.toMap(), - senderRecordedTimestamp = it.key.timestamp + senderStatesToRecord, + senderRecords.associate { it.compositeKey.peerPartyId to it.statesToRecord }, + HashedDistributionList.PublicHeader(key.timestamp) ) DBReceiverDistributionRecord( - compositeKey = PersistentKey(Key(TimestampKey(it.key.timestamp, it.key.timestampDiscriminator), senderPartyId)), - txId = txId.toString(), - distributionList = cryptoService.encrypt(hashedDistributionList.serialize()) + PersistentKey(Key(TimestampKey(key.timestamp, key.timestampDiscriminator), senderPartyId)), + txId.toString(), + hashedDistributionList.encrypt(encryptionService) ) } } @@ -180,13 +183,18 @@ class DBTransactionStorageLedgerRecovery(private val database: CordaPersistence, } } - override fun addReceiverTransactionRecoveryMetadata(id: SecureHash, sender: CordaX500Name, receiver: CordaX500Name, receiverStatesToRecord: StatesToRecord, encryptedDistributionList: ByteArray) { - val senderRecordedTimestamp = HashedDistributionList.deserialize(cryptoService.decrypt(encryptedDistributionList)).senderRecordedTimestamp + override fun addReceiverTransactionRecoveryMetadata(txId: SecureHash, + sender: CordaX500Name, + receiver: CordaX500Name, + receiverStatesToRecord: StatesToRecord, + encryptedDistributionList: ByteArray) { + val publicHeader = HashedDistributionList.PublicHeader.unauthenticatedDeserialise(encryptedDistributionList, encryptionService) database.transaction { - val receiverDistributionRecord = - DBReceiverDistributionRecord(Key(partyInfoCache.getPartyIdByCordaX500Name(sender), senderRecordedTimestamp), - id, - encryptedDistributionList) + val receiverDistributionRecord = DBReceiverDistributionRecord( + Key(partyInfoCache.getPartyIdByCordaX500Name(sender), publicHeader.senderRecordedTimestamp), + txId, + encryptedDistributionList + ) session.save(receiverDistributionRecord) } } @@ -266,8 +274,7 @@ class DBTransactionStorageLedgerRecovery(private val database: CordaPersistence, } criteriaQuery.orderBy(orderCriteria) } - val results = session.createQuery(criteriaQuery).stream() - results.toList() + session.createQuery(criteriaQuery).resultList } } @@ -277,8 +284,7 @@ class DBTransactionStorageLedgerRecovery(private val database: CordaPersistence, val criteriaQuery = criteriaBuilder.createQuery(DBSenderDistributionRecord::class.java) val txnMetadata = criteriaQuery.from(DBSenderDistributionRecord::class.java) criteriaQuery.where(criteriaBuilder.equal(txnMetadata.get(DBSenderDistributionRecord::txId.name), txId.toString())) - val results = session.createQuery(criteriaQuery).stream() - results.toList() + session.createQuery(criteriaQuery).resultList } } @@ -294,43 +300,36 @@ class DBTransactionStorageLedgerRecovery(private val database: CordaPersistence, val txnMetadata = criteriaQuery.from(DBReceiverDistributionRecord::class.java) val predicates = mutableListOf() val compositeKey = txnMetadata.get("compositeKey") - predicates.add(criteriaBuilder.greaterThanOrEqualTo(compositeKey.get(PersistentKey::timestamp.name), timeWindow.fromTime)) - predicates.add(criteriaBuilder.and(criteriaBuilder.lessThanOrEqualTo(compositeKey.get(PersistentKey::timestamp.name), timeWindow.untilTime))) + val timestamp = compositeKey.get(PersistentKey::timestamp.name) + predicates.add(criteriaBuilder.greaterThanOrEqualTo(timestamp, timeWindow.fromTime)) + predicates.add(criteriaBuilder.and(criteriaBuilder.lessThanOrEqualTo(timestamp, timeWindow.untilTime))) if (excludingTxnIds.isNotEmpty()) { - predicates.add(criteriaBuilder.and(criteriaBuilder.not(txnMetadata.get(DBSenderDistributionRecord::txId.name).`in`( - excludingTxnIds.map { it.toString() })))) + val txId = txnMetadata.get(DBSenderDistributionRecord::txId.name) + predicates.add(criteriaBuilder.and(criteriaBuilder.not(txId.`in`(excludingTxnIds.map { it.toString() })))) } if (initiators.isNotEmpty()) { - val initiatorPartyIds = initiators.map { partyInfoCache.getPartyIdByCordaX500Name(it) } + val initiatorPartyIds = initiators.map(partyInfoCache::getPartyIdByCordaX500Name) predicates.add(criteriaBuilder.and(compositeKey.get(PersistentKey::peerPartyId.name).`in`(initiatorPartyIds))) } criteriaQuery.where(*predicates.toTypedArray()) // optionally order by timestamp orderByTimestamp?.let { - val orderCriteria = - when (orderByTimestamp) { - // when adding column position of 'group by' shift in case columns were removed - Sort.Direction.ASC -> criteriaBuilder.asc(compositeKey.get(PersistentKey::timestamp.name)) - Sort.Direction.DESC -> criteriaBuilder.desc(compositeKey.get(PersistentKey::timestamp.name)) - } + val orderCriteria = when (orderByTimestamp) { + // when adding column position of 'group by' shift in case columns were removed + Sort.Direction.ASC -> criteriaBuilder.asc(timestamp) + Sort.Direction.DESC -> criteriaBuilder.desc(timestamp) + } criteriaQuery.orderBy(orderCriteria) } - val results = session.createQuery(criteriaQuery).stream() - results.toList() + session.createQuery(criteriaQuery).resultList } } + + fun decryptHashedDistributionList(encryptedBytes: ByteArray): HashedDistributionList { + return HashedDistributionList.decrypt(encryptedBytes, encryptionService) + } } -// TO DO: https://r3-cev.atlassian.net/browse/ENT-9876 -@VisibleForTesting -fun CryptoService.decrypt(bytes: ByteArray): ByteArray { - return bytes -} - -// TO DO: https://r3-cev.atlassian.net/browse/ENT-9876 -fun CryptoService.encrypt(bytes: ByteArray): ByteArray { - return bytes -} @CordaSerializable class DistributionRecords( @@ -338,80 +337,35 @@ class DistributionRecords( val receiverRecords: List = emptyList() ) { init { - assert(senderRecords.isNotEmpty() || receiverRecords.isNotEmpty()) { "Must set senderRecords or receiverRecords or both." } + require(senderRecords.isNotEmpty() || receiverRecords.isNotEmpty()) { "Must set senderRecords or receiverRecords or both." } } val size = senderRecords.size + receiverRecords.size } @CordaSerializable -open class DistributionRecord( - open val txId: SecureHash, - open val statesToRecord: StatesToRecord, - open val timestamp: Instant -) +abstract class DistributionRecord { + abstract val txId: SecureHash + abstract val timestamp: Instant +} @CordaSerializable data class SenderDistributionRecord( override val txId: SecureHash, val peerPartyId: Long, // CordaX500Name hashCode() - override val statesToRecord: StatesToRecord, + val statesToRecord: StatesToRecord, override val timestamp: Instant -) : DistributionRecord(txId, statesToRecord, timestamp) +) : DistributionRecord() @CordaSerializable data class ReceiverDistributionRecord( override val txId: SecureHash, val initiatorPartyId: Long, // CordaX500Name hashCode() - val peersToStatesToRecord: Map, // CordaX500Name hashCode() -> StatesToRecord - override val statesToRecord: StatesToRecord, + val encryptedDistributionList: OpaqueBytes, override val timestamp: Instant -) : DistributionRecord(txId, statesToRecord, timestamp) +) : DistributionRecord() @CordaSerializable enum class DistributionRecordType { SENDER, RECEIVER, ALL } - -@CordaSerializable -data class HashedDistributionList( - val senderStatesToRecord: StatesToRecord, - val peerHashToStatesToRecord: Map, - val senderRecordedTimestamp: Instant -) { - fun serialize(): ByteArray { - val baos = ByteArrayOutputStream() - val out = DataOutputStream(baos) - out.use { - out.writeByte(SERIALIZER_VERSION_ID) - out.writeByte(senderStatesToRecord.ordinal) - out.writeInt(peerHashToStatesToRecord.size) - for(entry in peerHashToStatesToRecord) { - out.writeLong(entry.key) - out.writeByte(entry.value.ordinal) - } - out.writeLong(senderRecordedTimestamp.toEpochMilli()) - out.flush() - return baos.toByteArray() - } - } - companion object { - const val SERIALIZER_VERSION_ID = 1 - fun deserialize(bytes: ByteArray): HashedDistributionList { - val input = DataInputStream(ByteArrayInputStream(bytes)) - input.use { - assert(input.readByte().toInt() == SERIALIZER_VERSION_ID) { "Serialization version conflict." } - val senderStatesToRecord = StatesToRecord.values()[input.readByte().toInt()] - val numPeerHashToStatesToRecords = input.readInt() - val peerHashToStatesToRecord = mutableMapOf() - repeat (numPeerHashToStatesToRecords) { - peerHashToStatesToRecord[input.readLong()] = StatesToRecord.values()[input.readByte().toInt()] - } - val senderRecordedTimestamp = Instant.ofEpochMilli(input.readLong()) - return HashedDistributionList(senderStatesToRecord, peerHashToStatesToRecord, senderRecordedTimestamp) - } - } - } -} - - diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/HashedDistributionList.kt b/node/src/main/kotlin/net/corda/node/services/persistence/HashedDistributionList.kt new file mode 100644 index 0000000000..910a00ce74 --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/services/persistence/HashedDistributionList.kt @@ -0,0 +1,104 @@ +package net.corda.node.services.persistence + +import net.corda.core.node.StatesToRecord +import net.corda.core.serialization.CordaSerializable +import net.corda.node.services.EncryptionService +import java.io.ByteArrayOutputStream +import java.io.DataInputStream +import java.io.DataOutputStream +import java.nio.ByteBuffer +import java.time.Instant + +@Suppress("TooGenericExceptionCaught") +@CordaSerializable +data class HashedDistributionList( + val senderStatesToRecord: StatesToRecord, + val peerHashToStatesToRecord: Map, + val publicHeader: PublicHeader +) { + /** + * Encrypt this hashed distribution list using the given [EncryptionService]. The [publicHeader] is not encrypted but is instead + * authenticated so that it is tamperproof. + * + * The same [EncryptionService] instance needs to be used with [decrypt] for decryption. + */ + fun encrypt(encryptionService: EncryptionService): ByteArray { + val baos = ByteArrayOutputStream() + val out = DataOutputStream(baos) + out.writeByte(senderStatesToRecord.ordinal) + out.writeInt(peerHashToStatesToRecord.size) + for (entry in peerHashToStatesToRecord) { + out.writeLong(entry.key) + out.writeByte(entry.value.ordinal) + } + return encryptionService.encrypt(baos.toByteArray(), publicHeader.serialise()) + } + + + @CordaSerializable + data class PublicHeader( + val senderRecordedTimestamp: Instant + ) { + fun serialise(): ByteArray { + val buffer = ByteBuffer.allocate(1 + java.lang.Long.BYTES) + buffer.put(VERSION_TAG.toByte()) + buffer.putLong(senderRecordedTimestamp.toEpochMilli()) + return buffer.array() + } + + companion object { + /** + * Deserialise a [PublicHeader] from the given [encryptedBytes]. The bytes is expected is to be a valid encrypted blob that can + * be decrypted by [HashedDistributionList.decrypt] using the same [EncryptionService]. + * + * Because this method does not actually decrypt the bytes, the header returned is not authenticated and any modifications to it + * will not be detected. That can only be done by the encrypting party with [HashedDistributionList.decrypt]. + */ + fun unauthenticatedDeserialise(encryptedBytes: ByteArray, encryptionService: EncryptionService): PublicHeader { + val additionalData = encryptionService.extractUnauthenticatedAdditionalData(encryptedBytes) + requireNotNull(additionalData) { "Missing additional data field" } + return deserialise(additionalData!!) + } + + fun deserialise(bytes: ByteArray): PublicHeader { + val buffer = ByteBuffer.wrap(bytes) + try { + val version = buffer.get().toInt() + require(version == VERSION_TAG) { "Unknown distribution list format $version" } + val senderRecordedTimestamp = Instant.ofEpochMilli(buffer.getLong()) + return PublicHeader(senderRecordedTimestamp) + } catch (e: Exception) { + throw IllegalArgumentException("Corrupt or not a distribution list header", e) + } + } + } + } + + companion object { + // The version tag is serialised in the header, even though it is separate from the encrypted main body of the distribution list. + // This is because the header and the dist list are cryptographically coupled and we want to avoid declaring the version field twice. + private const val VERSION_TAG = 1 + private val statesToRecordValues = StatesToRecord.values() // Cache the enum values since .values() returns a new array each time. + + /** + * Decrypt a [HashedDistributionList] from the given [encryptedBytes] using the same [EncryptionService] that was used in [encrypt]. + */ + fun decrypt(encryptedBytes: ByteArray, encryptionService: EncryptionService): HashedDistributionList { + val (plaintext, authenticatedAdditionalData) = encryptionService.decrypt(encryptedBytes) + requireNotNull(authenticatedAdditionalData) { "Missing authenticated header" } + val publicHeader = PublicHeader.deserialise(authenticatedAdditionalData!!) + val input = DataInputStream(plaintext.inputStream()) + try { + val senderStatesToRecord = statesToRecordValues[input.readByte().toInt()] + val numPeerHashToStatesToRecords = input.readInt() + val peerHashToStatesToRecord = mutableMapOf() + repeat(numPeerHashToStatesToRecords) { + peerHashToStatesToRecord[input.readLong()] = statesToRecordValues[input.readByte().toInt()] + } + return HashedDistributionList(senderStatesToRecord, peerHashToStatesToRecord, publicHeader) + } catch (e: Exception) { + throw IllegalArgumentException("Corrupt or not a distribution list", e) + } + } + } +} diff --git a/node/src/main/kotlin/net/corda/node/services/schema/NodeSchemaService.kt b/node/src/main/kotlin/net/corda/node/services/schema/NodeSchemaService.kt index 760544758d..68dc445e29 100644 --- a/node/src/main/kotlin/net/corda/node/services/schema/NodeSchemaService.kt +++ b/node/src/main/kotlin/net/corda/node/services/schema/NodeSchemaService.kt @@ -16,6 +16,7 @@ import net.corda.node.services.keys.BasicHSMKeyManagementService import net.corda.node.services.messaging.P2PMessageDeduplicator import net.corda.node.services.network.PersistentNetworkMapCache import net.corda.node.services.persistence.DBCheckpointStorage +import net.corda.node.services.persistence.AesDbEncryptionService import net.corda.node.services.persistence.DBTransactionStorageLedgerRecovery import net.corda.node.services.persistence.DBTransactionStorage import net.corda.node.services.persistence.NodeAttachmentService @@ -30,7 +31,7 @@ import net.corda.node.services.vault.VaultSchemaV1 * TODO: support plugins for schema version upgrading or custom mapping not supported by original [QueryableState]. * TODO: create whitelisted tables when a CorDapp is first installed */ -class NodeSchemaService(private val extraSchemas: Set = emptySet()) : SchemaService, SingletonSerializeAsToken() { +class NodeSchemaService(extraSchemas: Set = emptySet()) : SchemaService, SingletonSerializeAsToken() { // Core Entities used by a Node object NodeCore @@ -55,7 +56,8 @@ class NodeSchemaService(private val extraSchemas: Set = emptySet() PersistentNetworkMapCache.PersistentPartyToPublicKeyHash::class.java, DBTransactionStorageLedgerRecovery.DBSenderDistributionRecord::class.java, DBTransactionStorageLedgerRecovery.DBReceiverDistributionRecord::class.java, - DBTransactionStorageLedgerRecovery.DBRecoveryPartyInfo::class.java + DBTransactionStorageLedgerRecovery.DBRecoveryPartyInfo::class.java, + AesDbEncryptionService.EncryptionKeyRecord::class.java )) { override val migrationResource = "node-core.changelog-master" } diff --git a/node/src/main/resources/migration/node-core.changelog-master.xml b/node/src/main/resources/migration/node-core.changelog-master.xml index 0ebf26bdc1..ef9116aade 100644 --- a/node/src/main/resources/migration/node-core.changelog-master.xml +++ b/node/src/main/resources/migration/node-core.changelog-master.xml @@ -31,6 +31,7 @@ + diff --git a/node/src/main/resources/migration/node-core.changelog-v26.xml b/node/src/main/resources/migration/node-core.changelog-v26.xml new file mode 100644 index 0000000000..b0d4925c7a --- /dev/null +++ b/node/src/main/resources/migration/node-core.changelog-v26.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt b/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt index e85061a8f8..2cf36d35f9 100644 --- a/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt +++ b/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt @@ -811,15 +811,19 @@ class TwoPartyTradeFlowTests(private val anonymous: Boolean) { return true } - override fun addSenderTransactionRecoveryMetadata(id: SecureHash, metadata: TransactionMetadata): ByteArray? { + override fun addSenderTransactionRecoveryMetadata(txId: SecureHash, metadata: TransactionMetadata): ByteArray? { return database.transaction { - delegate.addSenderTransactionRecoveryMetadata(id, metadata) + delegate.addSenderTransactionRecoveryMetadata(txId, metadata) } } - override fun addReceiverTransactionRecoveryMetadata(id: SecureHash, sender: CordaX500Name, receiver: CordaX500Name, receiverStatesToRecord: StatesToRecord, encryptedDistributionList: ByteArray) { + override fun addReceiverTransactionRecoveryMetadata(txId: SecureHash, + sender: CordaX500Name, + receiver: CordaX500Name, + receiverStatesToRecord: StatesToRecord, + encryptedDistributionList: ByteArray) { database.transaction { - delegate.addReceiverTransactionRecoveryMetadata(id, sender, receiver, receiverStatesToRecord, encryptedDistributionList) + delegate.addReceiverTransactionRecoveryMetadata(txId, sender, receiver, receiverStatesToRecord, encryptedDistributionList) } } diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/AesDbEncryptionServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/persistence/AesDbEncryptionServiceTest.kt new file mode 100644 index 0000000000..806357627a --- /dev/null +++ b/node/src/test/kotlin/net/corda/node/services/persistence/AesDbEncryptionServiceTest.kt @@ -0,0 +1,134 @@ +package net.corda.node.services.persistence + +import net.corda.node.services.persistence.AesDbEncryptionService.EncryptionKeyRecord +import net.corda.nodeapi.internal.persistence.CordaPersistence +import net.corda.nodeapi.internal.persistence.DatabaseConfig +import net.corda.testing.core.TestIdentity +import net.corda.testing.internal.configureDatabase +import net.corda.testing.node.MockServices +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatExceptionOfType +import org.assertj.core.api.Assertions.assertThatIllegalArgumentException +import org.junit.After +import org.junit.Before +import org.junit.Test +import java.nio.ByteBuffer +import java.security.GeneralSecurityException +import java.util.UUID + +class AesDbEncryptionServiceTest { + private val identity = TestIdentity.fresh("me").party + private lateinit var database: CordaPersistence + private lateinit var encryptionService: AesDbEncryptionService + + @Before + fun setUp() { + val dataSourceProps = MockServices.makeTestDataSourceProperties() + database = configureDatabase(dataSourceProps, DatabaseConfig(), { null }, { null }) + encryptionService = AesDbEncryptionService(database) + encryptionService.start(identity) + } + + @After + fun cleanUp() { + database.close() + } + + @Test(timeout = 300_000) + fun `same instance can decrypt ciphertext`() { + val ciphertext = encryptionService.encrypt("Hello World".toByteArray()) + val (plaintext, authenticatedData) = encryptionService.decrypt(ciphertext) + assertThat(String(plaintext)).isEqualTo("Hello World") + assertThat(authenticatedData).isNull() + } + + @Test(timeout = 300_000) + fun `encypting twice produces different ciphertext`() { + val plaintext = "Hello".toByteArray() + assertThat(encryptionService.encrypt(plaintext)).isNotEqualTo(encryptionService.encrypt(plaintext)) + } + + @Test(timeout = 300_000) + fun `ciphertext can be decrypted after restart`() { + val ciphertext = encryptionService.encrypt("Hello World".toByteArray()) + encryptionService = AesDbEncryptionService(database) + encryptionService.start(identity) + val plaintext = encryptionService.decrypt(ciphertext).plaintext + assertThat(String(plaintext)).isEqualTo("Hello World") + } + + @Test(timeout = 300_000) + fun `encrypting with authenticated data`() { + val ciphertext = encryptionService.encrypt("Hello World".toByteArray(), "Additional data".toByteArray()) + val (plaintext, authenticatedData) = encryptionService.decrypt(ciphertext) + assertThat(String(plaintext)).isEqualTo("Hello World") + assertThat(authenticatedData?.let { String(it) }).isEqualTo("Additional data") + } + + @Test(timeout = 300_000) + fun extractUnauthenticatedAdditionalData() { + val ciphertext = encryptionService.encrypt("Hello World".toByteArray(), "Additional data".toByteArray()) + val additionalData = encryptionService.extractUnauthenticatedAdditionalData(ciphertext) + assertThat(additionalData?.let { String(it) }).isEqualTo("Additional data") + } + + @Test(timeout = 300_000) + fun `ciphertext cannot be decrypted if the authenticated data is modified`() { + val ciphertext = ByteBuffer.wrap(encryptionService.encrypt("Hello World".toByteArray(), "1234".toByteArray())) + + ciphertext.position(21) + ciphertext.put("4321".toByteArray()) // Use same length for the modified AAD + + assertThatExceptionOfType(GeneralSecurityException::class.java).isThrownBy { + encryptionService.decrypt(ciphertext.array()) + } + } + + @Test(timeout = 300_000) + fun `ciphertext cannot be decrypted if the key used is deleted`() { + val ciphertext = encryptionService.encrypt("Hello World".toByteArray()) + val keyId = ByteBuffer.wrap(ciphertext).getKeyId() + val deletedCount = database.transaction { + session.createQuery("DELETE FROM ${EncryptionKeyRecord::class.java.name} k WHERE k.keyId = :keyId") + .setParameter("keyId", keyId) + .executeUpdate() + } + assertThat(deletedCount).isEqualTo(1) + + encryptionService = AesDbEncryptionService(database) + encryptionService.start(identity) + assertThatIllegalArgumentException().isThrownBy { + encryptionService.decrypt(ciphertext) + } + } + + @Test(timeout = 300_000) + fun `ciphertext cannot be decrypted if forced to use a different key`() { + val ciphertext = ByteBuffer.wrap(encryptionService.encrypt("Hello World".toByteArray())) + val keyId = ciphertext.getKeyId() + val anotherKeyId = database.transaction { + session.createQuery("SELECT keyId FROM ${EncryptionKeyRecord::class.java.name} k WHERE k.keyId != :keyId", UUID::class.java) + .setParameter("keyId", keyId) + .setMaxResults(1) + .singleResult + } + + ciphertext.putKeyId(anotherKeyId) + + encryptionService = AesDbEncryptionService(database) + encryptionService.start(identity) + assertThatExceptionOfType(GeneralSecurityException::class.java).isThrownBy { + encryptionService.decrypt(ciphertext.array()) + } + } + + private fun ByteBuffer.getKeyId(): UUID { + position(1) + return getUUID() + } + + private fun ByteBuffer.putKeyId(keyId: UUID) { + position(1) + putUUID(keyId) + } +} diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecoveryTests.kt b/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecoveryTests.kt index 39836dfdd1..352398e2ab 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecoveryTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecoveryTests.kt @@ -23,8 +23,9 @@ import net.corda.node.services.network.PersistentNetworkMapCache import net.corda.node.services.network.PersistentPartyInfoCache import net.corda.node.services.persistence.DBTransactionStorage.TransactionStatus.IN_FLIGHT import net.corda.node.services.persistence.DBTransactionStorage.TransactionStatus.VERIFIED +import net.corda.node.services.persistence.DBTransactionStorageLedgerRecovery.DBReceiverDistributionRecord +import net.corda.node.services.persistence.DBTransactionStorageLedgerRecovery.DBSenderDistributionRecord import net.corda.nodeapi.internal.DEV_ROOT_CA -import net.corda.nodeapi.internal.cryptoservice.CryptoService import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.testing.core.ALICE_NAME @@ -38,7 +39,8 @@ import net.corda.testing.internal.TestingNamedCacheFactory import net.corda.testing.internal.configureDatabase import net.corda.testing.internal.createWireTransaction import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties -import net.corda.testing.node.internal.MockCryptoService +import net.corda.testing.node.internal.MockEncryptionService +import org.assertj.core.api.Assertions.assertThat import org.junit.After import org.junit.Before import org.junit.Rule @@ -67,6 +69,8 @@ class DBTransactionStorageLedgerRecoveryTests { private lateinit var transactionRecovery: DBTransactionStorageLedgerRecovery private lateinit var partyInfoCache: PersistentPartyInfoCache + private val encryptionService = MockEncryptionService() + @Before fun setUp() { val dataSourceProps = makeTestDataSourceProperties() @@ -136,7 +140,7 @@ class DBTransactionStorageLedgerRecoveryTests { // receiver txn transactionRecovery.addUnnotarisedTransaction(transaction2) transactionRecovery.addReceiverTransactionRecoveryMetadata(transaction2.id, BOB_NAME, ALICE_NAME, ALL_VISIBLE, - DistributionList(ONLY_RELEVANT, mapOf(ALICE_NAME to ALL_VISIBLE)).toWire()) + DistributionList(ONLY_RELEVANT, mapOf(ALICE_NAME to ALL_VISIBLE)).encrypt()) val timeWindow = RecoveryTimeWindow(fromTime = now().minus(1, ChronoUnit.DAYS)) transactionRecovery.queryDistributionRecords(timeWindow, recordType = DistributionRecordType.SENDER).let { assertEquals(1, it.size) @@ -146,7 +150,7 @@ class DBTransactionStorageLedgerRecoveryTests { transactionRecovery.queryDistributionRecords(timeWindow, recordType = DistributionRecordType.RECEIVER).let { assertEquals(1, it.size) assertEquals(BOB_NAME.hashCode().toLong(), it.receiverRecords[0].compositeKey.peerPartyId) - assertEquals(ALL_VISIBLE, (transactionRecovery.decrypt(it.receiverRecords[0].distributionList).peerHashToStatesToRecord.map { it.value }[0])) + assertEquals(ALL_VISIBLE, transactionRecovery.decryptHashedDistributionList(it.receiverRecords[0].distributionList).peerHashToStatesToRecord.values.first()) } val resultsAll = transactionRecovery.queryDistributionRecords(timeWindow, recordType = DistributionRecordType.ALL) assertEquals(2, resultsAll.size) @@ -186,30 +190,30 @@ class DBTransactionStorageLedgerRecoveryTests { val txn1 = newTransaction() transactionRecovery.addUnnotarisedTransaction(txn1) transactionRecovery.addReceiverTransactionRecoveryMetadata(txn1.id, ALICE_NAME, BOB_NAME, ALL_VISIBLE, - DistributionList(ONLY_RELEVANT, mapOf(BOB_NAME to ALL_VISIBLE, CHARLIE_NAME to ALL_VISIBLE)).toWire()) + DistributionList(ONLY_RELEVANT, mapOf(BOB_NAME to ALL_VISIBLE, CHARLIE_NAME to ALL_VISIBLE)).encrypt()) val txn2 = newTransaction() transactionRecovery.addUnnotarisedTransaction(txn2) transactionRecovery.addReceiverTransactionRecoveryMetadata(txn2.id, ALICE_NAME, BOB_NAME, ONLY_RELEVANT, - DistributionList(ONLY_RELEVANT, mapOf(BOB_NAME to ONLY_RELEVANT)).toWire()) + DistributionList(ONLY_RELEVANT, mapOf(BOB_NAME to ONLY_RELEVANT)).encrypt()) val txn3 = newTransaction() transactionRecovery.addUnnotarisedTransaction(txn3) transactionRecovery.addReceiverTransactionRecoveryMetadata(txn3.id, ALICE_NAME, CHARLIE_NAME, NONE, - DistributionList(ONLY_RELEVANT, mapOf(CHARLIE_NAME to NONE)).toWire()) + DistributionList(ONLY_RELEVANT, mapOf(CHARLIE_NAME to NONE)).encrypt()) val txn4 = newTransaction() transactionRecovery.addUnnotarisedTransaction(txn4) transactionRecovery.addReceiverTransactionRecoveryMetadata(txn4.id, BOB_NAME, ALICE_NAME, ONLY_RELEVANT, - DistributionList(ONLY_RELEVANT, mapOf(ALICE_NAME to ALL_VISIBLE)).toWire()) + DistributionList(ONLY_RELEVANT, mapOf(ALICE_NAME to ALL_VISIBLE)).encrypt()) val txn5 = newTransaction() transactionRecovery.addUnnotarisedTransaction(txn5) transactionRecovery.addReceiverTransactionRecoveryMetadata(txn5.id, CHARLIE_NAME, BOB_NAME, ONLY_RELEVANT, - DistributionList(ONLY_RELEVANT, mapOf(BOB_NAME to ONLY_RELEVANT)).toWire()) + DistributionList(ONLY_RELEVANT, mapOf(BOB_NAME to ONLY_RELEVANT)).encrypt()) val timeWindow = RecoveryTimeWindow(fromTime = now().minus(1, ChronoUnit.DAYS)) transactionRecovery.queryReceiverDistributionRecords(timeWindow, initiators = setOf(ALICE_NAME)).let { assertEquals(3, it.size) - assertEquals(transactionRecovery.decrypt(it[0].distributionList).peerHashToStatesToRecord.map { it.value }[0], ALL_VISIBLE) - assertEquals(transactionRecovery.decrypt(it[1].distributionList).peerHashToStatesToRecord.map { it.value }[0], ONLY_RELEVANT) - assertEquals(transactionRecovery.decrypt(it[2].distributionList).peerHashToStatesToRecord.map { it.value }[0], NONE) + assertEquals(transactionRecovery.decryptHashedDistributionList(it[0].distributionList).peerHashToStatesToRecord.values.first(), ALL_VISIBLE) + assertEquals(transactionRecovery.decryptHashedDistributionList(it[1].distributionList).peerHashToStatesToRecord.values.first(), ONLY_RELEVANT) + assertEquals(transactionRecovery.decryptHashedDistributionList(it[2].distributionList).peerHashToStatesToRecord.values.first(), NONE) } assertEquals(1, transactionRecovery.queryReceiverDistributionRecords(timeWindow, initiators = setOf(BOB_NAME)).size) assertEquals(1, transactionRecovery.queryReceiverDistributionRecords(timeWindow, initiators = setOf(CHARLIE_NAME)).size) @@ -241,13 +245,14 @@ class DBTransactionStorageLedgerRecoveryTests { val receiverTransaction = newTransaction() transactionRecovery.addUnnotarisedTransaction(receiverTransaction) transactionRecovery.addReceiverTransactionRecoveryMetadata(receiverTransaction.id, ALICE_NAME, BOB_NAME, ALL_VISIBLE, - DistributionList(ONLY_RELEVANT, mapOf(BOB_NAME to ALL_VISIBLE)).toWire()) + DistributionList(ONLY_RELEVANT, mapOf(BOB_NAME to ALL_VISIBLE)).encrypt()) assertEquals(IN_FLIGHT, readTransactionFromDB(receiverTransaction.id).status) - readReceiverDistributionRecordFromDB(receiverTransaction.id).let { - assertEquals(ONLY_RELEVANT, it.statesToRecord) - assertEquals(ALL_VISIBLE, it.peersToStatesToRecord.map { it.value }[0]) - assertEquals(ALICE_NAME, partyInfoCache.getCordaX500NameByPartyId(it.initiatorPartyId)) - assertEquals(setOf(BOB_NAME), it.peersToStatesToRecord.map { (peer, _) -> partyInfoCache.getCordaX500NameByPartyId(peer) }.toSet() ) + readReceiverDistributionRecordFromDB(receiverTransaction.id).let { record -> + val distList = transactionRecovery.decryptHashedDistributionList(record.encryptedDistributionList.bytes) + assertEquals(ONLY_RELEVANT, distList.senderStatesToRecord) + assertEquals(ALL_VISIBLE, distList.peerHashToStatesToRecord.values.first()) + assertEquals(ALICE_NAME, partyInfoCache.getCordaX500NameByPartyId(record.initiatorPartyId)) + assertEquals(setOf(BOB_NAME), distList.peerHashToStatesToRecord.map { (peer) -> partyInfoCache.getCordaX500NameByPartyId(peer) }.toSet() ) } } @@ -269,7 +274,7 @@ class DBTransactionStorageLedgerRecoveryTests { val senderTransaction = newTransaction(notarySig = false) transactionRecovery.addUnnotarisedTransaction(senderTransaction) transactionRecovery.addReceiverTransactionRecoveryMetadata(senderTransaction.id, ALICE.name, BOB.name, ONLY_RELEVANT, - DistributionList(ONLY_RELEVANT, mapOf(BOB.name to ONLY_RELEVANT, CHARLIE_NAME to ONLY_RELEVANT)).toWire()) + DistributionList(ONLY_RELEVANT, mapOf(BOB.name to ONLY_RELEVANT, CHARLIE_NAME to ONLY_RELEVANT)).encrypt()) assertNull(transactionRecovery.getTransaction(senderTransaction.id)) assertEquals(IN_FLIGHT, readTransactionFromDB(senderTransaction.id).status) @@ -281,7 +286,7 @@ class DBTransactionStorageLedgerRecoveryTests { val receiverTransaction = newTransaction(notarySig = false) transactionRecovery.addUnnotarisedTransaction(receiverTransaction) transactionRecovery.addReceiverTransactionRecoveryMetadata(receiverTransaction.id, ALICE.name, BOB.name, ONLY_RELEVANT, - DistributionList(ONLY_RELEVANT, mapOf(BOB.name to ONLY_RELEVANT)).toWire()) + DistributionList(ONLY_RELEVANT, mapOf(BOB.name to ONLY_RELEVANT)).encrypt()) assertNull(transactionRecovery.getTransaction(receiverTransaction.id)) assertEquals(IN_FLIGHT, readTransactionFromDB(receiverTransaction.id).status) @@ -293,51 +298,53 @@ class DBTransactionStorageLedgerRecoveryTests { @Test(timeout = 300_000) fun `test lightweight serialization and deserialization of hashed distribution list payload`() { - val dl = HashedDistributionList(ALL_VISIBLE, - mapOf(BOB.name.hashCode().toLong() to NONE, CHARLIE_NAME.hashCode().toLong() to ONLY_RELEVANT), now()) - assertEquals(dl, dl.serialize().let { HashedDistributionList.deserialize(it) }) + val hashedDistList = HashedDistributionList( + ALL_VISIBLE, + mapOf(BOB.name.hashCode().toLong() to NONE, CHARLIE_NAME.hashCode().toLong() to ONLY_RELEVANT), + HashedDistributionList.PublicHeader(now()) + ) + val roundtrip = HashedDistributionList.decrypt(hashedDistList.encrypt(encryptionService), encryptionService) + assertThat(roundtrip).isEqualTo(hashedDistList) } - private fun readTransactionFromDB(id: SecureHash): DBTransactionStorage.DBTransaction { + private fun readTransactionFromDB(txId: SecureHash): DBTransactionStorage.DBTransaction { val fromDb = database.transaction { session.createQuery( "from ${DBTransactionStorage.DBTransaction::class.java.name} where txId = :transactionId", DBTransactionStorage.DBTransaction::class.java - ).setParameter("transactionId", id.toString()).resultList.map { it } + ).setParameter("transactionId", txId.toString()).resultList } assertEquals(1, fromDb.size) return fromDb[0] } - private fun readSenderDistributionRecordFromDB(id: SecureHash? = null): List { + private fun readSenderDistributionRecordFromDB(txId: SecureHash? = null): List { return database.transaction { - if (id != null) + if (txId != null) session.createQuery( - "from ${DBTransactionStorageLedgerRecovery.DBSenderDistributionRecord::class.java.name} where txId = :transactionId", - DBTransactionStorageLedgerRecovery.DBSenderDistributionRecord::class.java - ).setParameter("transactionId", id.toString()).resultList.map { it.toSenderDistributionRecord() } + "from ${DBSenderDistributionRecord::class.java.name} where txId = :transactionId", + DBSenderDistributionRecord::class.java + ).setParameter("transactionId", txId.toString()).resultList.map { it.toSenderDistributionRecord() } else session.createQuery( - "from ${DBTransactionStorageLedgerRecovery.DBSenderDistributionRecord::class.java.name}", - DBTransactionStorageLedgerRecovery.DBSenderDistributionRecord::class.java + "from ${DBSenderDistributionRecord::class.java.name}", + DBSenderDistributionRecord::class.java ).resultList.map { it.toSenderDistributionRecord() } } } - private fun readReceiverDistributionRecordFromDB(id: SecureHash): ReceiverDistributionRecord { + private fun readReceiverDistributionRecordFromDB(txId: SecureHash): ReceiverDistributionRecord { val fromDb = database.transaction { session.createQuery( - "from ${DBTransactionStorageLedgerRecovery.DBReceiverDistributionRecord::class.java.name} where txId = :transactionId", - DBTransactionStorageLedgerRecovery.DBReceiverDistributionRecord::class.java - ).setParameter("transactionId", id.toString()).resultList.map { it } + "from ${DBReceiverDistributionRecord::class.java.name} where txId = :transactionId", + DBReceiverDistributionRecord::class.java + ).setParameter("transactionId", txId.toString()).resultList } assertEquals(1, fromDb.size) - return fromDb[0].toReceiverDistributionRecord(MockCryptoService(emptyMap())) + return fromDb[0].toReceiverDistributionRecord() } - private fun newTransactionRecovery(cacheSizeBytesOverride: Long? = null, clock: CordaClock = SimpleClock(Clock.systemUTC()), - cryptoService: CryptoService = MockCryptoService(emptyMap())) { - + private fun newTransactionRecovery(cacheSizeBytesOverride: Long? = null, clock: CordaClock = SimpleClock(Clock.systemUTC())) { val networkMapCache = PersistentNetworkMapCache(TestingNamedCacheFactory(), database, InMemoryIdentityService(trustRoot = DEV_ROOT_CA.certificate)) val alice = createNodeInfo(listOf(ALICE)) val bob = createNodeInfo(listOf(BOB)) @@ -345,8 +352,13 @@ class DBTransactionStorageLedgerRecoveryTests { networkMapCache.addOrUpdateNodes(listOf(alice, bob, charlie)) partyInfoCache = PersistentPartyInfoCache(networkMapCache, TestingNamedCacheFactory(), database) partyInfoCache.start() - transactionRecovery = DBTransactionStorageLedgerRecovery(database, TestingNamedCacheFactory(cacheSizeBytesOverride - ?: 1024), clock, cryptoService, partyInfoCache) + transactionRecovery = DBTransactionStorageLedgerRecovery( + database, + TestingNamedCacheFactory(cacheSizeBytesOverride ?: 1024), + clock, + encryptionService, + partyInfoCache + ) } private var portCounter = 1000 @@ -385,14 +397,13 @@ class DBTransactionStorageLedgerRecoveryTests { private fun notarySig(txId: SecureHash) = DUMMY_NOTARY.keyPair.sign(SignableData(txId, SignatureMetadata(1, Crypto.findSignatureScheme(DUMMY_NOTARY.publicKey).schemeNumberID))) - private fun DistributionList.toWire(cryptoService: CryptoService = MockCryptoService(emptyMap())): ByteArray { - val hashedPeersToStatesToRecord = this.peersToStatesToRecord.map { (peer, statesToRecord) -> - partyInfoCache.getPartyIdByCordaX500Name(peer) to statesToRecord }.toMap() - val hashedDistributionList = HashedDistributionList(this.senderStatesToRecord, hashedPeersToStatesToRecord, now()) - return cryptoService.encrypt(hashedDistributionList.serialize()) + private fun DistributionList.encrypt(): ByteArray { + val hashedPeersToStatesToRecord = this.peersToStatesToRecord.mapKeys { (peer) -> partyInfoCache.getPartyIdByCordaX500Name(peer) } + val hashedDistributionList = HashedDistributionList( + this.senderStatesToRecord, + hashedPeersToStatesToRecord, + HashedDistributionList.PublicHeader(now()) + ) + return hashedDistributionList.encrypt(encryptionService) } } - -internal fun DBTransactionStorageLedgerRecovery.decrypt(distributionList: ByteArray): HashedDistributionList { - return HashedDistributionList.deserialize(this.cryptoService.decrypt(distributionList)) -} diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/MockEncryptionService.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/MockEncryptionService.kt new file mode 100644 index 0000000000..1c3875191c --- /dev/null +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/MockEncryptionService.kt @@ -0,0 +1,39 @@ +package net.corda.testing.node.internal + +import net.corda.core.internal.copyBytes +import net.corda.node.services.EncryptionService +import net.corda.nodeapi.internal.crypto.AesEncryption +import java.nio.ByteBuffer +import javax.crypto.SecretKey + +class MockEncryptionService(private val aesKey: SecretKey = AesEncryption.randomKey()) : EncryptionService { + override fun encrypt(plaintext: ByteArray, additionalData: ByteArray?): ByteArray { + val ciphertext = AesEncryption.encrypt(aesKey, plaintext, additionalData) + val buffer = ByteBuffer.allocate(Integer.BYTES + (additionalData?.size ?: 0) + ciphertext.size) + if (additionalData != null) { + buffer.putInt(additionalData.size) + buffer.put(additionalData) + } else { + buffer.putInt(0) + } + buffer.put(ciphertext) + return buffer.array() + } + + override fun decrypt(ciphertext: ByteArray): EncryptionService.PlaintextAndAAD { + val buffer = ByteBuffer.wrap(ciphertext) + val additionalData = buffer.getAdditionaData() + val plaintext = AesEncryption.decrypt(aesKey, buffer.copyBytes(), additionalData) + // Only now is the additional data authenticated + return EncryptionService.PlaintextAndAAD(plaintext, additionalData) + } + + override fun extractUnauthenticatedAdditionalData(ciphertext: ByteArray): ByteArray? { + return ByteBuffer.wrap(ciphertext).getAdditionaData() + } + + private fun ByteBuffer.getAdditionaData(): ByteArray? { + val additionalDataSize = getInt() + return if (additionalDataSize > 0) ByteArray(additionalDataSize).also { get(it) } else null + } +} diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/MockTransactionStorage.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/MockTransactionStorage.kt index f850aab58b..9f23bf6beb 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/MockTransactionStorage.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/MockTransactionStorage.kt @@ -61,9 +61,13 @@ open class MockTransactionStorage : WritableTransactionStorage, SingletonSeriali return txns.putIfAbsent(transaction.id, TxHolder(transaction, status = TransactionStatus.IN_FLIGHT)) == null } - override fun addSenderTransactionRecoveryMetadata(id: SecureHash, metadata: TransactionMetadata): ByteArray? { return null } + override fun addSenderTransactionRecoveryMetadata(txId: SecureHash, metadata: TransactionMetadata): ByteArray? { return null } - override fun addReceiverTransactionRecoveryMetadata(id: SecureHash, sender: CordaX500Name, receiver: CordaX500Name, receiverStatesToRecord: StatesToRecord, encryptedDistributionList: ByteArray) { } + override fun addReceiverTransactionRecoveryMetadata(txId: SecureHash, + sender: CordaX500Name, + receiver: CordaX500Name, + receiverStatesToRecord: StatesToRecord, + encryptedDistributionList: ByteArray) { } override fun removeUnnotarisedTransaction(id: SecureHash): Boolean { return txns.remove(id) != null From 4fef01a5b0f11770afa86efbf04e3e4a2e945fed Mon Sep 17 00:00:00 2001 From: Jose Coll Date: Tue, 22 Aug 2023 16:03:13 +0100 Subject: [PATCH 065/126] Clean-up. --- .../coretests/flows/FinalityFlowTests.kt | 58 +++++++++---------- .../DBTransactionStorageLedgerRecovery.kt | 3 +- ...DBTransactionStorageLedgerRecoveryTests.kt | 20 ++----- 3 files changed, 33 insertions(+), 48 deletions(-) diff --git a/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt b/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt index 644ea2013a..da7a128e6f 100644 --- a/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt +++ b/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt @@ -51,8 +51,6 @@ import net.corda.finance.test.flows.CashIssueWithObserversFlow import net.corda.finance.test.flows.CashPaymentWithObserversFlow import net.corda.node.services.persistence.DBTransactionStorage import net.corda.node.services.persistence.DBTransactionStorageLedgerRecovery -import net.corda.node.services.persistence.DBTransactionStorageLedgerRecovery.DBReceiverDistributionRecord -import net.corda.node.services.persistence.DBTransactionStorageLedgerRecovery.DBSenderDistributionRecord import net.corda.node.services.persistence.HashedDistributionList import net.corda.node.services.persistence.ReceiverDistributionRecord import net.corda.node.services.persistence.SenderDistributionRecord @@ -76,8 +74,8 @@ import net.corda.testing.node.internal.enclosedCordapp import net.corda.testing.node.internal.findCordapp import org.assertj.core.api.Assertions.assertThat import org.junit.After +import org.junit.Assert.assertNotNull import org.junit.Test -import org.junit.jupiter.api.assertThrows import java.sql.SQLException import java.util.Random import kotlin.test.assertEquals @@ -358,10 +356,12 @@ class FinalityFlowTests : WithFinality { assertEquals(StatesToRecord.ALL_VISIBLE, this[0].statesToRecord) assertEquals(BOB_NAME.hashCode().toLong(), this[0].peerPartyId) } - val rdr = getReceiverRecoveryData(stx.id, bobNode, aliceNode).apply { - assertEquals(StatesToRecord.ONLY_RELEVANT, this?.statesToRecord) - assertEquals(aliceNode.info.singleIdentity().name.hashCode().toLong(), this?.initiatorPartyId) - assertEquals(mapOf(BOB_NAME.hashCode().toLong() to StatesToRecord.ALL_VISIBLE), this?.peersToStatesToRecord) + val rdr = getReceiverRecoveryData(stx.id, bobNode).apply { + assertNotNull(this) + val hashedDL = HashedDistributionList.decrypt(this!!.encryptedDistributionList.bytes, aliceNode.internals.encryptionService) + assertEquals(StatesToRecord.ONLY_RELEVANT, hashedDL.senderStatesToRecord) + assertEquals(aliceNode.info.singleIdentity().name.hashCode().toLong(), this.initiatorPartyId) + assertEquals(mapOf(BOB_NAME.hashCode().toLong() to StatesToRecord.ALL_VISIBLE), hashedDL.peerHashToStatesToRecord) } validateSenderAndReceiverTimestamps(sdrs, rdr!!) } @@ -391,14 +391,16 @@ class FinalityFlowTests : WithFinality { assertEquals(StatesToRecord.ALL_VISIBLE, this[1].statesToRecord) assertEquals(CHARLIE_NAME.hashCode().toLong(), this[1].peerPartyId) } - val rdr = getReceiverRecoveryData(stx.id, bobNode, aliceNode).apply { - assertEquals(StatesToRecord.ONLY_RELEVANT, this?.statesToRecord) - assertEquals(aliceNode.info.singleIdentity().name.hashCode().toLong(), this?.initiatorPartyId) + val rdr = getReceiverRecoveryData(stx.id, bobNode).apply { + assertNotNull(this) + val hashedDL = HashedDistributionList.decrypt(this!!.encryptedDistributionList.bytes, aliceNode.internals.encryptionService) + assertEquals(StatesToRecord.ONLY_RELEVANT, hashedDL.senderStatesToRecord) + assertEquals(aliceNode.info.singleIdentity().name.hashCode().toLong(), this.initiatorPartyId) // note: Charlie assertion here is using the hinted StatesToRecord value passed to it from Alice assertEquals(mapOf( BOB_NAME.hashCode().toLong() to StatesToRecord.ONLY_RELEVANT, CHARLIE_NAME.hashCode().toLong() to StatesToRecord.ALL_VISIBLE - ), distList.peerHashToStatesToRecord) + ), hashedDL.peerHashToStatesToRecord) } validateSenderAndReceiverTimestamps(sdrs, rdr!!) @@ -417,12 +419,12 @@ class FinalityFlowTests : WithFinality { assertEquals(2, this.size) assertEquals(this[0].timestamp, this[1].timestamp) } - getReceiverRecoveryData(stx3.id, bobNode, aliceNode).apply { - assertThat(getReceiverRecoveryData(stx3.id, bobNode, aliceNode)).isNotNull + getReceiverRecoveryData(stx3.id, bobNode).apply { + assertThat(this).isNotNull assertEquals(senderDistributionRecords[0].timestamp, this!!.timestamp) } - getReceiverRecoveryData(stx3.id, charlieNode, aliceNode).apply { - assertThat(getReceiverRecoveryData(stx3.id, charlieNode, aliceNode)).isNotNull + getReceiverRecoveryData(stx3.id, charlieNode).apply { + assertThat(this).isNotNull assertEquals(senderDistributionRecords[0].timestamp, this!!.timestamp) } } @@ -451,10 +453,12 @@ class FinalityFlowTests : WithFinality { assertEquals(StatesToRecord.ONLY_RELEVANT, this[0].statesToRecord) assertEquals(BOB_NAME.hashCode().toLong(), this[0].peerPartyId) } - val rdr = getReceiverRecoveryData(stx.id, bobNode, aliceNode).apply { - assertEquals(StatesToRecord.ONLY_RELEVANT, this?.statesToRecord) - assertEquals(aliceNode.info.singleIdentity().name.hashCode().toLong(), this?.initiatorPartyId) - assertEquals(mapOf(BOB_NAME.hashCode().toLong() to StatesToRecord.ONLY_RELEVANT), this?.peersToStatesToRecord) + val rdr = getReceiverRecoveryData(stx.id, bobNode).apply { + assertNotNull(this) + val hashedDL = HashedDistributionList.decrypt(this!!.encryptedDistributionList.bytes, aliceNode.internals.encryptionService) + assertEquals(StatesToRecord.ONLY_RELEVANT, hashedDL.senderStatesToRecord) + assertEquals(aliceNode.info.singleIdentity().name.hashCode().toLong(), this.initiatorPartyId) + assertEquals(mapOf(BOB_NAME.hashCode().toLong() to StatesToRecord.ONLY_RELEVANT), hashedDL.peerHashToStatesToRecord) } validateSenderAndReceiverTimestamps(sdr, rdr!!) } @@ -466,24 +470,16 @@ class FinalityFlowTests : WithFinality { DBTransactionStorageLedgerRecovery.DBSenderDistributionRecord::class.java ).setParameter("transactionId", id.toString()).resultList } - return fromDb.map { it.toSenderDistributionRecord() }.also { println("SenderDistributionRecord\n$it") } + return fromDb.map { it.toSenderDistributionRecord() } } - private fun getReceiverRecoveryData(txId: SecureHash, receiver: TestStartedNode, sender: TestStartedNode): ReceiverDistributionRecord? { - val fromDb = receiver.database.transaction { + private fun getReceiverRecoveryData(txId: SecureHash, receiver: TestStartedNode): ReceiverDistributionRecord? { + return receiver.database.transaction { session.createQuery( "from ${DBTransactionStorageLedgerRecovery.DBReceiverDistributionRecord::class.java.name} where txId = :transactionId", DBTransactionStorageLedgerRecovery.DBReceiverDistributionRecord::class.java ).setParameter("transactionId", txId.toString()).resultList - }.singleOrNull() - - // The receiver should not be able to decrypt the distribution list - assertThrows { - fromDb?.toReceiverDistributionRecord(receiver.internals.encryptionService) - } - - // Only the sender can - return fromDb?.toReceiverDistributionRecord(sender.internals.encryptionService) + }.singleOrNull()?.toReceiverDistributionRecord() } @StartableByRPC diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecovery.kt b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecovery.kt index db1c176f0a..69df1fe9b3 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecovery.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecovery.kt @@ -101,8 +101,7 @@ class DBTransactionStorageLedgerRecovery(private val database: CordaPersistence, receiverStatesToRecord = receiverStatesToRecord ) @VisibleForTesting - fun toReceiverDistributionRecord(encryptionService: EncryptionService): ReceiverDistributionRecord { - val hashedDL = HashedDistributionList.decrypt(this.distributionList, encryptionService) + fun toReceiverDistributionRecord(): ReceiverDistributionRecord { return ReceiverDistributionRecord( SecureHash.parse(this.txId), this.compositeKey.peerPartyId, diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecoveryTests.kt b/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecoveryTests.kt index 1ffd27e736..3c81be2ab8 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecoveryTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecoveryTests.kt @@ -331,7 +331,7 @@ class DBTransactionStorageLedgerRecoveryTests { session.createQuery( "from ${DBTransactionStorage.DBTransaction::class.java.name} where txId = :transactionId", DBTransactionStorage.DBTransaction::class.java - ).setParameter("transactionId", id.toString()).resultList + ).setParameter("transactionId", txId.toString()).resultList } assertEquals(1, fromDb.size) return fromDb[0] @@ -355,12 +355,12 @@ class DBTransactionStorageLedgerRecoveryTests { private fun readReceiverDistributionRecordFromDB(txId: SecureHash): ReceiverDistributionRecord { val fromDb = database.transaction { session.createQuery( - "from ${DBTransactionStorageLedgerRecovery.DBReceiverDistributionRecord::class.java.name} where txId = :transactionId", - DBTransactionStorageLedgerRecovery.DBReceiverDistributionRecord::class.java - ).setParameter("transactionId", id.toString()).resultList + "from ${DBReceiverDistributionRecord::class.java.name} where txId = :transactionId", + DBReceiverDistributionRecord::class.java + ).setParameter("transactionId", txId.toString()).resultList } assertEquals(1, fromDb.size) - return fromDb[0].toReceiverDistributionRecord(encryptionService) + return fromDb[0].toReceiverDistributionRecord() } private fun newTransactionRecovery(cacheSizeBytesOverride: Long? = null, clock: CordaClock = SimpleClock(Clock.systemUTC())) { @@ -415,15 +415,5 @@ class DBTransactionStorageLedgerRecoveryTests { private fun notarySig(txId: SecureHash) = DUMMY_NOTARY.keyPair.sign(SignableData(txId, SignatureMetadata(1, Crypto.findSignatureScheme(DUMMY_NOTARY.publicKey).schemeNumberID))) - - private fun SenderDistributionList.toWire(): ByteArray { - val hashedPeersToStatesToRecord = this.peersToStatesToRecord.mapKeys { (peer) -> partyInfoCache.getPartyIdByCordaX500Name(peer) } - val hashedDistributionList = HashedDistributionList( - this.senderStatesToRecord, - hashedPeersToStatesToRecord, - HashedDistributionList.PublicHeader(now()) - ) - return hashedDistributionList.encrypt(encryptionService) - } } From 1aaff8e6ae363c716270ce877efe164e34541e1e Mon Sep 17 00:00:00 2001 From: Jose Coll Date: Tue, 22 Aug 2023 16:08:00 +0100 Subject: [PATCH 066/126] Clean-up. --- .../net/corda/coretests/flows/FinalityFlowTests.kt | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt b/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt index da7a128e6f..8e01c60505 100644 --- a/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt +++ b/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt @@ -50,7 +50,8 @@ import net.corda.finance.issuedBy import net.corda.finance.test.flows.CashIssueWithObserversFlow import net.corda.finance.test.flows.CashPaymentWithObserversFlow import net.corda.node.services.persistence.DBTransactionStorage -import net.corda.node.services.persistence.DBTransactionStorageLedgerRecovery +import net.corda.node.services.persistence.DBTransactionStorageLedgerRecovery.DBReceiverDistributionRecord +import net.corda.node.services.persistence.DBTransactionStorageLedgerRecovery.DBSenderDistributionRecord import net.corda.node.services.persistence.HashedDistributionList import net.corda.node.services.persistence.ReceiverDistributionRecord import net.corda.node.services.persistence.SenderDistributionRecord @@ -466,8 +467,8 @@ class FinalityFlowTests : WithFinality { private fun getSenderRecoveryData(id: SecureHash, database: CordaPersistence): List { val fromDb = database.transaction { session.createQuery( - "from ${DBTransactionStorageLedgerRecovery.DBSenderDistributionRecord::class.java.name} where txId = :transactionId", - DBTransactionStorageLedgerRecovery.DBSenderDistributionRecord::class.java + "from ${DBSenderDistributionRecord::class.java.name} where txId = :transactionId", + DBSenderDistributionRecord::class.java ).setParameter("transactionId", id.toString()).resultList } return fromDb.map { it.toSenderDistributionRecord() } @@ -476,8 +477,8 @@ class FinalityFlowTests : WithFinality { private fun getReceiverRecoveryData(txId: SecureHash, receiver: TestStartedNode): ReceiverDistributionRecord? { return receiver.database.transaction { session.createQuery( - "from ${DBTransactionStorageLedgerRecovery.DBReceiverDistributionRecord::class.java.name} where txId = :transactionId", - DBTransactionStorageLedgerRecovery.DBReceiverDistributionRecord::class.java + "from ${DBReceiverDistributionRecord::class.java.name} where txId = :transactionId", + DBReceiverDistributionRecord::class.java ).setParameter("transactionId", txId.toString()).resultList }.singleOrNull()?.toReceiverDistributionRecord() } From ec261cb0c3f2dee877b01e218039d6b699b58413 Mon Sep 17 00:00:00 2001 From: Rick Parker Date: Wed, 23 Aug 2023 10:09:42 +0100 Subject: [PATCH 067/126] ENT-10306 Swap logic from receive finality to receive transaction flows (#7451) * Swap logic from receive finality to receive transaction flows * Remote TPV check * Make finality check more robust * Make emulation of finality in tests compliant with changes * Improve deferring of ack when issue transaction * Remove API checking of SignedTransactionWithDistributionList as added it 4.11 so cannot be incompatible, yet. * Regenerated API file from 4.10 to check only compatibility with 4.10 * Move function to private * Revert "Regenerated API file from 4.10 to check only compatibility with 4.10" This reverts commit 6428f957e188050ec19e41945223010ef180a1ca. * Reset ReceiveTransactionFlow and ReceiveFinalityFlow APIs --- .ci/api-current.txt | 23 +---- .../coretests/flows/FinalityFlowTests.kt | 4 +- .../net/corda/core/flows/FinalityFlow.kt | 63 ++----------- .../core/flows/ReceiveTransactionFlow.kt | 93 +++++++++++++++---- .../corda/core/flows/SendTransactionFlow.kt | 14 +-- 5 files changed, 94 insertions(+), 103 deletions(-) diff --git a/.ci/api-current.txt b/.ci/api-current.txt index 200b056099..10e6fcc297 100644 --- a/.ci/api-current.txt +++ b/.ci/api-current.txt @@ -3222,8 +3222,7 @@ public final class net.corda.core.flows.ReceiveFinalityFlow extends net.corda.co public (net.corda.core.flows.FlowSession) public (net.corda.core.flows.FlowSession, net.corda.core.crypto.SecureHash) public (net.corda.core.flows.FlowSession, net.corda.core.crypto.SecureHash, net.corda.core.node.StatesToRecord) - public (net.corda.core.flows.FlowSession, net.corda.core.crypto.SecureHash, net.corda.core.node.StatesToRecord, Boolean) - public (net.corda.core.flows.FlowSession, net.corda.core.crypto.SecureHash, net.corda.core.node.StatesToRecord, Boolean, int, kotlin.jvm.internal.DefaultConstructorMarker) + public (net.corda.core.flows.FlowSession, net.corda.core.crypto.SecureHash, net.corda.core.node.StatesToRecord, int, kotlin.jvm.internal.DefaultConstructorMarker) @Suspendable @NotNull public net.corda.core.transactions.SignedTransaction call() @@ -3239,8 +3238,6 @@ public class net.corda.core.flows.ReceiveTransactionFlow extends net.corda.core. public (net.corda.core.flows.FlowSession, boolean) public (net.corda.core.flows.FlowSession, boolean, net.corda.core.node.StatesToRecord) public (net.corda.core.flows.FlowSession, boolean, net.corda.core.node.StatesToRecord, int, kotlin.jvm.internal.DefaultConstructorMarker) - public (net.corda.core.flows.FlowSession, boolean, net.corda.core.node.StatesToRecord, boolean) - public (net.corda.core.flows.FlowSession, boolean, net.corda.core.node.StatesToRecord, boolean, int, kotlin.jvm.internal.DefaultConstructorMarker) @Suspendable @NotNull public net.corda.core.transactions.SignedTransaction call() @@ -3334,24 +3331,6 @@ public static final class net.corda.core.flows.SignTransactionFlow$Companion$SIG public static final class net.corda.core.flows.SignTransactionFlow$Companion$VERIFYING extends net.corda.core.utilities.ProgressTracker$Step public static final net.corda.core.flows.SignTransactionFlow$Companion$VERIFYING INSTANCE ## -@CordaSerializable -public final class net.corda.core.flows.SignedTransactionWithDistributionList extends java.lang.Object - public (net.corda.core.transactions.SignedTransaction, byte[]) - @NotNull - public final net.corda.core.transactions.SignedTransaction component1() - @NotNull - public final byte[] component2() - @NotNull - public final net.corda.core.flows.SignedTransactionWithDistributionList copy(net.corda.core.transactions.SignedTransaction, byte[]) - public boolean equals(Object) - @NotNull - public final byte[] getDistributionList() - @NotNull - public final net.corda.core.transactions.SignedTransaction getStx() - public int hashCode() - @NotNull - public String toString() -## public final class net.corda.core.flows.StackFrameDataToken extends java.lang.Object public (String) @NotNull diff --git a/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt b/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt index 49628a8307..e65b2825c8 100644 --- a/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt +++ b/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt @@ -593,7 +593,9 @@ class FinalityFlowTests : WithFinality { val txBuilder = DummyContract.move(stateAndRef, newOwner) val stxn = serviceHub.signInitialTransaction(txBuilder, ourIdentity.owningKey) val sessionWithCounterParty = initiateFlow(newOwner) - subFlow(SendTransactionFlow(stxn, setOf(sessionWithCounterParty), emptySet(), StatesToRecord.ONLY_RELEVANT)) + subFlow(object : SendTransactionFlow(stxn, setOf(sessionWithCounterParty), emptySet(), StatesToRecord.ONLY_RELEVANT, true) { + override fun isFinality(): Boolean = true + }) throw UnexpectedFlowEndException("${stxn.id}") } } diff --git a/core/src/main/kotlin/net/corda/core/flows/FinalityFlow.kt b/core/src/main/kotlin/net/corda/core/flows/FinalityFlow.kt index 62d85b9fff..9f2277f968 100644 --- a/core/src/main/kotlin/net/corda/core/flows/FinalityFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/FinalityFlow.kt @@ -5,10 +5,8 @@ import net.corda.core.CordaInternal import net.corda.core.crypto.SecureHash import net.corda.core.crypto.TransactionSignature import net.corda.core.crypto.isFulfilledBy -import net.corda.core.flows.NotarySigCheck.needsNotarySignature import net.corda.core.identity.Party import net.corda.core.identity.groupAbstractPartyByWellKnownParty -import net.corda.core.internal.FetchDataFlow import net.corda.core.internal.PlatformVersionSwitches import net.corda.core.internal.ServiceHubCoreInternal import net.corda.core.internal.pushToLoggingContext @@ -22,7 +20,6 @@ import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.Try import net.corda.core.utilities.debug -import net.corda.core.utilities.unwrap import java.time.Duration /** @@ -219,8 +216,6 @@ class FinalityFlow private constructor(val transaction: SignedTransaction, val requiresNotarisation = needsNotarySignature(transaction) val useTwoPhaseFinality = serviceHub.myInfo.platformVersion >= PlatformVersionSwitches.TWO_PHASE_FINALITY - && serviceHub.getAppContext().cordapp.targetPlatformVersion >= PlatformVersionSwitches.TWO_PHASE_FINALITY - if (useTwoPhaseFinality) { val stxn = if (requiresNotarisation) { recordLocallyAndBroadcast(newPlatformSessions, transaction) @@ -285,7 +280,9 @@ class FinalityFlow private constructor(val transaction: SignedTransaction, try { logger.debug { "Sending transaction to party sessions: $sessions." } val (participantSessions, observerSessions) = deriveSessions(sessions) - subFlow(SendTransactionFlow(tx, participantSessions, observerSessions, statesToRecord, true)) + subFlow(object : SendTransactionFlow(tx, participantSessions, observerSessions, statesToRecord, true) { + override fun isFinality(): Boolean = true + }) } catch (e: UnexpectedFlowEndException) { throw UnexpectedFlowEndException( "One of the sessions ${sessions.map { it.counterparty }} has finished prematurely and we're trying to send them a transaction." + @@ -499,55 +496,13 @@ class ReceiveFinalityFlow(private val otherSideSession: FlowSession, @Suppress("ComplexMethod", "NestedBlockDepth") @Suspendable override fun call(): SignedTransaction { - val stx = subFlow(ReceiveTransactionFlow(otherSideSession, false, statesToRecord, true)) - - val requiresNotarisation = needsNotarySignature(stx) - val fromTwoPhaseFinalityNode = serviceHub.networkMapCache.getNodeByLegalIdentity(otherSideSession.counterparty)?.platformVersion!! >= PlatformVersionSwitches.TWO_PHASE_FINALITY - && serviceHub.getAppContext().cordapp.targetPlatformVersion >= PlatformVersionSwitches.TWO_PHASE_FINALITY - - if (fromTwoPhaseFinalityNode) { - if (requiresNotarisation) { - serviceHub.telemetryServiceInternal.span("${this::class.java.name}#recordUnnotarisedTransaction", flowLogic = this) { - logger.debug { "Peer recording transaction without notary signature." } - (serviceHub as ServiceHubCoreInternal).recordUnnotarisedTransaction(stx) + return subFlow(object : ReceiveTransactionFlow(otherSideSession, checkSufficientSignatures = true, statesToRecord = statesToRecord, handlePropagatedNotaryError = handlePropagatedNotaryError) { + override fun checkBeforeRecording(stx: SignedTransaction) { + require(expectedTxId == null || expectedTxId == stx.id) { + "We expected to receive transaction with ID $expectedTxId but instead got ${stx.id}. Transaction was" + + "not recorded and nor its states sent to the vault." } - otherSideSession.send(FetchDataFlow.Request.End) // Finish fetching data (deferredAck) - logger.info("Peer recorded transaction without notary signature. Waiting to receive notary signature.") - try { - val notarySignatures = otherSideSession.receive>>().unwrap { it.getOrThrow() } - serviceHub.telemetryServiceInternal.span("${this::class.java.name}#finalizeTransactionWithExtraSignatures", flowLogic = this) { - logger.debug { "Peer received notarised signature." } - (serviceHub as ServiceHubCoreInternal).finalizeTransactionWithExtraSignatures(stx, notarySignatures, statesToRecord) - logger.info("Peer finalised transaction with notary signature.") - } - } catch (e: NotaryException) { - logger.info("Peer received notary error.") - val overrideHandlePropagatedNotaryError = handlePropagatedNotaryError ?: - (serviceHub.cordappProvider.getAppContext().cordapp.targetPlatformVersion >= PlatformVersionSwitches.TWO_PHASE_FINALITY) - if (overrideHandlePropagatedNotaryError) { - (serviceHub as ServiceHubCoreInternal).removeUnnotarisedTransaction(stx.id) - sleep(Duration.ZERO) // force checkpoint to persist db update. - throw e - } - else { - otherSideSession.receive() // simulate unexpected flow end - } - } - } else { - serviceHub.telemetryServiceInternal.span("${this::class.java.name}#finalizeTransaction", flowLogic = this) { - (serviceHub as ServiceHubCoreInternal).finalizeTransaction(stx, statesToRecord) - logger.info("Peer recorded transaction with recovery metadata.") - } - otherSideSession.send(FetchDataFlow.Request.End) // Finish fetching data (deferredAck) } - } else { - logger.warnOnce("The current usage of ReceiveFinalityFlow is not using Two Phase Finality. Please consider upgrading your CorDapp (refer to Corda 4.11 release notes).") - serviceHub.telemetryServiceInternal.span("${this::class.java.name}#recordTransactions", flowLogic = this) { - serviceHub.recordTransactions(statesToRecord, setOf(stx)) - } - otherSideSession.send(FetchDataFlow.Request.End) // Finish fetching data (deferredAck) - logger.info("Peer successfully recorded received transaction.") - } - return stx + }) } } diff --git a/core/src/main/kotlin/net/corda/core/flows/ReceiveTransactionFlow.kt b/core/src/main/kotlin/net/corda/core/flows/ReceiveTransactionFlow.kt index a4f2defa3a..c21485e041 100644 --- a/core/src/main/kotlin/net/corda/core/flows/ReceiveTransactionFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/ReceiveTransactionFlow.kt @@ -6,15 +6,22 @@ import net.corda.core.contracts.ContractState import net.corda.core.contracts.StateAndRef import net.corda.core.contracts.TransactionResolutionException import net.corda.core.contracts.TransactionVerificationException +import net.corda.core.crypto.TransactionSignature +import net.corda.core.internal.FetchDataFlow +import net.corda.core.internal.PlatformVersionSwitches import net.corda.core.internal.ResolveTransactionsFlow import net.corda.core.internal.ServiceHubCoreInternal import net.corda.core.internal.checkParameterHash import net.corda.core.internal.pushToLoggingContext +import net.corda.core.internal.telemetry.telemetryServiceInternal import net.corda.core.node.StatesToRecord import net.corda.core.transactions.SignedTransaction +import net.corda.core.utilities.Try +import net.corda.core.utilities.debug import net.corda.core.utilities.trace import net.corda.core.utilities.unwrap import java.security.SignatureException +import java.time.Duration /** * The [ReceiveTransactionFlow] should be called in response to the [SendTransactionFlow]. @@ -39,12 +46,13 @@ import java.security.SignatureException open class ReceiveTransactionFlow constructor(private val otherSideSession: FlowSession, private val checkSufficientSignatures: Boolean = true, private val statesToRecord: StatesToRecord = StatesToRecord.NONE, - private val deferredAck: Boolean = false) : FlowLogic() { - @JvmOverloads constructor( + private val handlePropagatedNotaryError: Boolean? = null) : FlowLogic() { + @JvmOverloads + constructor( otherSideSession: FlowSession, checkSufficientSignatures: Boolean = true, statesToRecord: StatesToRecord = StatesToRecord.NONE - ) : this(otherSideSession, checkSufficientSignatures, statesToRecord, false) + ) : this(otherSideSession, checkSufficientSignatures, statesToRecord, null) @Suppress("KDocMissingDocumentation") @Suspendable @@ -60,32 +68,83 @@ open class ReceiveTransactionFlow constructor(private val otherSideSession: Flow } val payload = otherSideSession.receive().unwrap { it } + return if (isReallyReceiveFinality(payload)) { + doReceiveFinality(payload) + } else { + val deferredAck = isDeferredAck(payload) + val stx = resolvePayload(payload) + stx.pushToLoggingContext() + logger.info("Received transaction acknowledgement request from party ${otherSideSession.counterparty}.") + checkParameterHash(stx.networkParametersHash) + subFlow(ResolveTransactionsFlow(stx, otherSideSession, statesToRecord, deferredAck)) + logger.info("Transaction dependencies resolution completed.") + try { + stx.verify(serviceHub, checkSufficientSignatures) + } catch (e: Exception) { + logger.warn("Transaction verification failed.") + throw e + } + if (checkSufficientSignatures) { + // We should only send a transaction to the vault for processing if we did in fact fully verify it, and + // there are no missing signatures. We don't want partly signed stuff in the vault. + checkBeforeRecording(stx) + logger.info("Successfully received fully signed tx. Sending it to the vault for processing.") + serviceHub.recordTransactions(statesToRecord, setOf(stx)) + logger.info("Successfully recorded received transaction locally.") + if (deferredAck) otherSideSession.send(FetchDataFlow.Request.End) // Finish fetching data (deferredAck) + } + stx + } + } + + private fun isDeferredAck(payload: Any): Boolean { + return payload is SignedTransactionWithDistributionList && checkSufficientSignatures && payload.isFinality + } + + @Suspendable + private fun doReceiveFinality(payload: Any): SignedTransaction { val stx = resolvePayload(payload) stx.pushToLoggingContext() logger.info("Received transaction acknowledgement request from party ${otherSideSession.counterparty}.") checkParameterHash(stx.networkParametersHash) - subFlow(ResolveTransactionsFlow(stx, otherSideSession, statesToRecord, deferredAck)) + subFlow(ResolveTransactionsFlow(stx, otherSideSession, statesToRecord, true)) logger.info("Transaction dependencies resolution completed.") - try { - stx.verify(serviceHub, checkSufficientSignatures) - } catch (e: Exception) { - logger.warn("Transaction verification failed.") - throw e + + serviceHub.telemetryServiceInternal.span("${this::class.java.name}#recordUnnotarisedTransaction", flowLogic = this) { + logger.debug { "Peer recording transaction without notary signature." } + (serviceHub as ServiceHubCoreInternal).recordUnnotarisedTransaction(stx) } - if (checkSufficientSignatures) { - // We should only send a transaction to the vault for processing if we did in fact fully verify it, and - // there are no missing signatures. We don't want partly signed stuff in the vault. - checkBeforeRecording(stx) - logger.info("Successfully received fully signed tx. Sending it to the vault for processing.") - serviceHub.recordTransactions(statesToRecord, setOf(stx)) - logger.info("Successfully recorded received transaction locally.") + otherSideSession.send(FetchDataFlow.Request.End) // Finish fetching data (deferredAck) + logger.info("Peer recorded transaction without notary signature. Waiting to receive notary signature.") + try { + val notarySignatures = otherSideSession.receive>>().unwrap { it.getOrThrow() } + serviceHub.telemetryServiceInternal.span("${this::class.java.name}#finalizeTransactionWithExtraSignatures", flowLogic = this) { + logger.debug { "Peer received notarised signature." } + (serviceHub as ServiceHubCoreInternal).finalizeTransactionWithExtraSignatures(stx, notarySignatures, statesToRecord) + logger.info("Peer finalised transaction with notary signature.") + } + } catch (e: NotaryException) { + logger.info("Peer received notary error.") + val overrideHandlePropagatedNotaryError = handlePropagatedNotaryError + ?: (serviceHub.cordappProvider.getAppContext().cordapp.targetPlatformVersion >= PlatformVersionSwitches.TWO_PHASE_FINALITY) + if (overrideHandlePropagatedNotaryError) { + (serviceHub as ServiceHubCoreInternal).removeUnnotarisedTransaction(stx.id) + sleep(Duration.ZERO) // force checkpoint to persist db update. + throw e + } else { + otherSideSession.receive() // simulate unexpected flow end + } } return stx } + private fun isReallyReceiveFinality(payload: Any): Boolean { + return payload is SignedTransactionWithDistributionList && checkSufficientSignatures && payload.isFinality && NotarySigCheck.needsNotarySignature(payload.stx) + } + open fun resolvePayload(payload: Any): SignedTransaction { return if (payload is SignedTransactionWithDistributionList) { - if (checkSufficientSignatures || deferredAck) { + if (checkSufficientSignatures) { (serviceHub as ServiceHubCoreInternal).recordReceiverTransactionRecoveryMetadata(payload.stx.id, otherSideSession.counterparty.name, ourIdentity.name, statesToRecord, payload.distributionList) payload.stx } else payload.stx diff --git a/core/src/main/kotlin/net/corda/core/flows/SendTransactionFlow.kt b/core/src/main/kotlin/net/corda/core/flows/SendTransactionFlow.kt index 93ed7d0c97..1217a6a085 100644 --- a/core/src/main/kotlin/net/corda/core/flows/SendTransactionFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/SendTransactionFlow.kt @@ -15,13 +15,6 @@ import net.corda.core.serialization.serialize import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.trace import net.corda.core.utilities.unwrap -import kotlin.collections.List -import kotlin.collections.MutableSet -import kotlin.collections.Set -import kotlin.collections.flatMap -import kotlin.collections.map -import kotlin.collections.mutableSetOf -import kotlin.collections.plus import kotlin.collections.toSet /** @@ -136,6 +129,8 @@ open class DataVendingFlow(val otherSessions: Set, val payload: Any // User can override this method to perform custom request verification. } + protected open fun isFinality(): Boolean = false + @Suppress("ComplexCondition", "ComplexMethod", "LongMethod") @Suspendable override fun call(): Void? { @@ -174,7 +169,7 @@ open class DataVendingFlow(val otherSessions: Set, val payload: Any val payloadWithMetadata = if (txnMetadata != null && toTwoPhaseFinalityNode && useTwoPhaseFinality && payload is SignedTransaction) { val encryptedDistributionList = (serviceHub as ServiceHubCoreInternal).recordSenderTransactionRecoveryMetadata(payload.id, txnMetadata.copy(initiator = ourIdentity.name)) - SignedTransactionWithDistributionList(payload, encryptedDistributionList!!) + SignedTransactionWithDistributionList(payload, encryptedDistributionList!!, isFinality()) } else null otherSessions.forEachIndexed { idx, otherSideSession -> @@ -311,5 +306,6 @@ open class DataVendingFlow(val otherSessions: Set, val payload: Any @CordaSerializable data class SignedTransactionWithDistributionList( val stx: SignedTransaction, - val distributionList: ByteArray + val distributionList: ByteArray, + val isFinality: Boolean ) \ No newline at end of file From 6edb1b779c2151c2160fe19853ec8cdced23f983 Mon Sep 17 00:00:00 2001 From: Shams Asari Date: Wed, 23 Aug 2023 15:03:08 +0100 Subject: [PATCH 068/126] Placate Detekt --- .../kotlin/net/corda/node/services/vault/NodeVaultService.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt index a1bf1893b0..1b6c43e33b 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt @@ -93,6 +93,7 @@ import kotlin.collections.component2 * TODO: keep an audit trail with time stamps of previously unconsumed states "as of" a particular point in time. * TODO: have transaction storage do some caching. */ +@Suppress("LargeClass") class NodeVaultService( private val clock: Clock, private val keyManagementService: KeyManagementService, From 17a1e149fa4785a2167becfe915b534de31e9030 Mon Sep 17 00:00:00 2001 From: Jose Coll Date: Thu, 24 Aug 2023 16:56:11 +0100 Subject: [PATCH 069/126] Update notary demo to ensure txn signed by all before recording. --- samples/notary-demo/build.gradle | 36 +++++++++++++++++++ .../net/corda/notarydemo/client/Notarise.kt | 4 +-- .../notarydemo/flows/DummyIssueAndMove.kt | 23 ++++++++++-- 3 files changed, 57 insertions(+), 6 deletions(-) diff --git a/samples/notary-demo/build.gradle b/samples/notary-demo/build.gradle index 6f87b55b35..e448345573 100644 --- a/samples/notary-demo/build.gradle +++ b/samples/notary-demo/build.gradle @@ -58,6 +58,15 @@ task deployNodesSingle(type: Cordform, dependsOn: ['jar', nodeTask, webTask]) { } rpcUsers = [[user: "demou", password: "demop", permissions: ["ALL"]]] } + node { + name "O=Bob Plc,L=Rome,C=IT" + p2pPort 10005 + rpcSettings { + address "localhost:10006" + adminAddress "localhost:10007" + } + rpcUsers = [[user: "demou", password: "demop", permissions: ["ALL"]]] + } node { name "O=Notary Node,L=Zurich,C=CH" p2pPort 10009 @@ -90,6 +99,15 @@ task deployNodesCustom(type: Cordform, dependsOn: ['jar', nodeTask, webTask]) { } rpcUsers = [[user: "demou", password: "demop", permissions: ["ALL"]]] } + node { + name "O=Bob Plc,L=Rome,C=IT" + p2pPort 10005 + rpcSettings { + address "localhost:10006" + adminAddress "localhost:10007" + } + rpcUsers = [[user: "demou", password: "demop", permissions: ["ALL"]]] + } node { name "O=Notary Node,L=Zurich,C=CH" p2pPort 10009 @@ -124,6 +142,15 @@ task deployNodesRaft(type: Cordform, dependsOn: ['jar', nodeTask, webTask]) { } rpcUsers = [[user: "demou", password: "demop", permissions: ["ALL"]]] } + node { + name "O=Bob Plc,L=Rome,C=IT" + p2pPort 10005 + rpcSettings { + address "localhost:10006" + adminAddress "localhost:10007" + } + rpcUsers = [[user: "demou", password: "demop", permissions: ["ALL"]]] + } node { name "O=Notary Service 0,L=Zurich,C=CH" p2pPort 10009 @@ -193,6 +220,15 @@ task deployNodesBFT(type: Cordform, dependsOn: ['jar', nodeTask, webTask]) { } rpcUsers = [[user: "demou", password: "demop", permissions: ["ALL"]]] } + node { + name "O=Bob Plc,L=Rome,C=IT" + p2pPort 10005 + rpcSettings { + address "localhost:10006" + adminAddress "localhost:10007" + } + rpcUsers = [[user: "demou", password: "demop", permissions: ["ALL"]]] + } node { name "O=Notary Service 0,L=Zurich,C=CH" p2pPort 10009 diff --git a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/client/Notarise.kt b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/client/Notarise.kt index 0f1648752d..711f7042fd 100644 --- a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/client/Notarise.kt +++ b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/client/Notarise.kt @@ -2,10 +2,8 @@ package net.corda.notarydemo.client import net.corda.client.rpc.CordaRPCClient import net.corda.core.crypto.CompositeKey -import net.corda.core.crypto.Crypto import net.corda.core.crypto.toStringShort import net.corda.core.identity.CordaX500Name -import net.corda.core.identity.Party import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.startFlow import net.corda.core.transactions.SignedTransaction @@ -32,7 +30,7 @@ private class NotaryDemoClientApi(val rpc: CordaRPCOps) { /** A dummy identity. */ private val BOB_NAME = CordaX500Name("Bob Plc", "Rome", "IT") - private val counterparty = Party(BOB_NAME, Crypto.generateKeyPair(Crypto.DEFAULT_SIGNATURE_SCHEME).public) + private val counterparty = rpc.wellKnownPartyFromX500Name(BOB_NAME) ?: throw IllegalArgumentException("Couldn't find Bob Plc party") /** Makes calls to the node rpc to start transaction notarisation. */ fun notarise(count: Int) { diff --git a/samples/notary-demo/workflows/src/main/kotlin/net/corda/notarydemo/flows/DummyIssueAndMove.kt b/samples/notary-demo/workflows/src/main/kotlin/net/corda/notarydemo/flows/DummyIssueAndMove.kt index dfa7c02ae0..a73a805e31 100644 --- a/samples/notary-demo/workflows/src/main/kotlin/net/corda/notarydemo/flows/DummyIssueAndMove.kt +++ b/samples/notary-demo/workflows/src/main/kotlin/net/corda/notarydemo/flows/DummyIssueAndMove.kt @@ -2,15 +2,22 @@ package net.corda.notarydemo.flows import co.paralleluniverse.fibers.Suspendable import net.corda.core.contracts.ContractState +import net.corda.core.flows.FinalityFlow import net.corda.core.flows.FlowLogic +import net.corda.core.flows.FlowSession +import net.corda.core.flows.InitiatedBy +import net.corda.core.flows.InitiatingFlow +import net.corda.core.flows.ReceiveFinalityFlow import net.corda.core.flows.StartableByRPC import net.corda.core.identity.Party +import net.corda.core.node.StatesToRecord import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.notarydemo.contracts.DO_NOTHING_PROGRAM_ID import net.corda.notarydemo.contracts.DummyCommand import net.corda.notarydemo.contracts.DummyState +@InitiatingFlow @StartableByRPC class DummyIssueAndMove(private val notary: Party, private val counterpartyNode: Party, private val discriminator: Int) : FlowLogic() { @Suspendable @@ -22,12 +29,22 @@ class DummyIssueAndMove(private val notary: Party, private val counterpartyNode: addCommand(DummyCommand(), listOf(ourIdentity.owningKey)) }) serviceHub.recordTransactions(issueTx) - // Move ownership of the asset to the counterparty - // We don't check signatures because we know that the notary's signature is missing - return serviceHub.signInitialTransaction(TransactionBuilder(notary).apply { + + val stx = serviceHub.signInitialTransaction(TransactionBuilder(notary).apply { addInputState(issueTx.tx.outRef(0)) addOutputState(state.copy(participants = listOf(counterpartyNode)), DO_NOTHING_PROGRAM_ID) addCommand(DummyCommand(), listOf(ourIdentity.owningKey)) }) + + return subFlow(FinalityFlow(stx, initiateFlow(counterpartyNode))) } } + +@InitiatedBy(DummyIssueAndMove::class) +class DummyIssueAndMoveResponder(private val otherSide: FlowSession) : FlowLogic() { + @Suspendable + override fun call() { + // As a non-participant to the transaction we need to record all states + subFlow(ReceiveFinalityFlow(otherSide, statesToRecord = StatesToRecord.ALL_VISIBLE)) + } +} \ No newline at end of file From 3ae6db8c043805d315594c9db2457ae6dd3c7567 Mon Sep 17 00:00:00 2001 From: Adel El-Beik <48713346+adelel1@users.noreply.github.com> Date: Wed, 30 Aug 2023 11:33:01 +0100 Subject: [PATCH 070/126] ENT-10122: Now also added consuming transaction id in the resolveAndMakeUpdate code path. (#7459) --- .../kotlin/net/corda/node/services/vault/NodeVaultService.kt | 3 ++- .../net/corda/node/services/vault/NodeVaultServiceTest.kt | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt index 1b6c43e33b..ffcd38b108 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt @@ -410,7 +410,8 @@ class NodeVaultService( } else { Vault.UpdateType.NOTARY_CHANGE } - return Vault.Update(consumedStateAndRefs.toSet(), producedStateAndRefs.toSet(), null, updateType, referenceStateAndRefs.toSet()) + val consumedTxIds = consumedStateAndRefs.associate { Pair(it.ref, tx.id) } + return Vault.Update(consumedStateAndRefs.toSet(), producedStateAndRefs.toSet(), null, updateType, referenceStateAndRefs.toSet(), consumingTxIds = consumedTxIds) } diff --git a/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt index 3b01205ba7..35691970c7 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt @@ -740,7 +740,7 @@ class NodeVaultServiceTest { } val expectedIssueUpdate = Vault.Update(emptySet(), setOf(initialCashState), null) - val expectedNotaryChangeUpdate = Vault.Update(setOf(initialCashState), setOf(cashStateWithNewNotary), null, Vault.UpdateType.NOTARY_CHANGE) + val expectedNotaryChangeUpdate = Vault.Update(setOf(initialCashState), setOf(cashStateWithNewNotary), null, Vault.UpdateType.NOTARY_CHANGE, consumingTxIds = mapOf(initialCashState.ref to changeNotaryTx.id)) val expectedMoveUpdate = Vault.Update(setOf(cashStateWithNewNotary), emptySet(), null, consumingTxIds = mapOf(cashStateWithNewNotary.ref to moveTx.id)) val observedUpdates = vaultSubscriber.onNextEvents From b0ea7a655184e2709b21200bb4ce51cd0e09d3c8 Mon Sep 17 00:00:00 2001 From: Jose Coll Date: Fri, 1 Sep 2023 10:01:31 +0100 Subject: [PATCH 071/126] ENT-4973 Remove redundant 2PF warning message. (#7470) * Remove redundant warning message. Additional assertion to validate database records deleted. --- core/src/main/kotlin/net/corda/core/flows/FinalityFlow.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/core/src/main/kotlin/net/corda/core/flows/FinalityFlow.kt b/core/src/main/kotlin/net/corda/core/flows/FinalityFlow.kt index 9f2277f968..a2f3e23609 100644 --- a/core/src/main/kotlin/net/corda/core/flows/FinalityFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/FinalityFlow.kt @@ -246,7 +246,6 @@ class FinalityFlow private constructor(val transaction: SignedTransaction, return stxn } else { - logger.warnOnce("The current usage of FinalityFlow is not using Two Phase Finality. Please consider upgrading your CorDapp (refer to Corda 4.11 release notes).") val stxn = if (requiresNotarisation) { notarise().first } else transaction From d7a5428665b8909d9bbe9b81395f0b0f9694c6e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Waldemar=20=C5=BBurowski?= <45210402+wzur-r3@users.noreply.github.com> Date: Mon, 4 Sep 2023 12:08:52 +0100 Subject: [PATCH 072/126] Updated for new branch --- .ci/dev/forward-merge/Jenkinsfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.ci/dev/forward-merge/Jenkinsfile b/.ci/dev/forward-merge/Jenkinsfile index c2da16efe6..ccbd4dca13 100644 --- a/.ci/dev/forward-merge/Jenkinsfile +++ b/.ci/dev/forward-merge/Jenkinsfile @@ -1,8 +1,8 @@ @Library('corda-shared-build-pipeline-steps@5.1') _ forwardMerger( - targetBranch: 'release/os/4.11', - originBranch: 'release/os/4.10', + targetBranch: 'release/os/4.12', + originBranch: 'release/os/4.11', slackChannel: '#build-team-automated-notifications' ) From 5d84d0a5c9f08dda8b4249a9dfd44f9c744331aa Mon Sep 17 00:00:00 2001 From: Rick Parker Date: Wed, 6 Sep 2023 16:32:47 +0100 Subject: [PATCH 073/126] ENT-10306 Missed actually verifying transaction from recent refactor (#7483) --- .../corda/core/flows/ReceiveTransactionFlow.kt | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/flows/ReceiveTransactionFlow.kt b/core/src/main/kotlin/net/corda/core/flows/ReceiveTransactionFlow.kt index 7b45d972af..31bbc4cf3b 100644 --- a/core/src/main/kotlin/net/corda/core/flows/ReceiveTransactionFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/ReceiveTransactionFlow.kt @@ -78,12 +78,7 @@ open class ReceiveTransactionFlow constructor(private val otherSideSession: Flow checkParameterHash(stx.networkParametersHash) subFlow(ResolveTransactionsFlow(stx, otherSideSession, statesToRecord, deferredAck)) logger.info("Transaction dependencies resolution completed.") - try { - stx.verify(serviceHub, checkSufficientSignatures) - } catch (e: Exception) { - logger.warn("Transaction verification failed.") - throw e - } + verifyTx(stx, checkSufficientSignatures) if (checkSufficientSignatures) { // We should only send a transaction to the vault for processing if we did in fact fully verify it, and // there are no missing signatures. We don't want partly signed stuff in the vault. @@ -97,6 +92,15 @@ open class ReceiveTransactionFlow constructor(private val otherSideSession: Flow } } + private fun verifyTx(stx: SignedTransaction, localCheckSufficientSignatures: Boolean) { + try { + stx.verify(serviceHub, localCheckSufficientSignatures) + } catch (e: Exception) { + logger.warn("Transaction verification failed.") + throw e + } + } + private fun isDeferredAck(payload: Any): Boolean { return payload is SignedTransactionWithDistributionList && checkSufficientSignatures && payload.isFinality } @@ -109,7 +113,7 @@ open class ReceiveTransactionFlow constructor(private val otherSideSession: Flow checkParameterHash(stx.networkParametersHash) subFlow(ResolveTransactionsFlow(stx, otherSideSession, statesToRecord, true)) logger.info("Transaction dependencies resolution completed.") - + verifyTx(stx, false) serviceHub.telemetryServiceInternal.span("${this::class.java.name}#recordUnnotarisedTransaction", flowLogic = this) { logger.debug { "Peer recording transaction without notary signature." } (serviceHub as ServiceHubCoreInternal).recordUnnotarisedTransaction(stx) From b16c93e76bd262e760c16eda3dd80aac9dfc3265 Mon Sep 17 00:00:00 2001 From: Jose Coll Date: Wed, 20 Sep 2023 08:44:56 +0100 Subject: [PATCH 074/126] ENT-6875 Deserialisation exception on output state can cause ledger inconsistency (#7476) --- .../version1/AttachmentContract.kt | 33 +++ .../version2/AttachmentContract.kt | 25 ++ .../incompatible/version1/AttachmentFlow.kt | 94 +++++++ .../incompatible/version2/AttachmentFlow.kt | 58 ++++ .../incompatible/version3/AttachmentFlow.kt | 56 ++++ .../node/VaultUpdateDeserializationTest.kt | 249 ++++++++++++++++++ .../node/services/vault/NodeVaultService.kt | 38 ++- .../corda/testing/driver/NodeParameters.kt | 96 ++++++- .../testing/node/internal/DriverDSLImpl.kt | 2 +- 9 files changed, 637 insertions(+), 14 deletions(-) create mode 100644 node/src/integration-test/kotlin/net/corda/contracts/incompatible/version1/AttachmentContract.kt create mode 100644 node/src/integration-test/kotlin/net/corda/contracts/incompatible/version2/AttachmentContract.kt create mode 100644 node/src/integration-test/kotlin/net/corda/flows/incompatible/version1/AttachmentFlow.kt create mode 100644 node/src/integration-test/kotlin/net/corda/flows/incompatible/version2/AttachmentFlow.kt create mode 100644 node/src/integration-test/kotlin/net/corda/flows/incompatible/version3/AttachmentFlow.kt create mode 100644 node/src/integration-test/kotlin/net/corda/node/VaultUpdateDeserializationTest.kt diff --git a/node/src/integration-test/kotlin/net/corda/contracts/incompatible/version1/AttachmentContract.kt b/node/src/integration-test/kotlin/net/corda/contracts/incompatible/version1/AttachmentContract.kt new file mode 100644 index 0000000000..f4c3fa965b --- /dev/null +++ b/node/src/integration-test/kotlin/net/corda/contracts/incompatible/version1/AttachmentContract.kt @@ -0,0 +1,33 @@ +package net.corda.contracts.incompatible.version1 + +import net.corda.core.contracts.Contract +import net.corda.core.contracts.ContractState +import net.corda.core.contracts.TransactionVerificationException +import net.corda.core.contracts.TypeOnlyCommandData +import net.corda.core.crypto.SecureHash +import net.corda.core.identity.AbstractParty +import net.corda.core.serialization.internal.AttachmentsClassLoader +import net.corda.core.transactions.LedgerTransaction + +class AttachmentContract : Contract { + + private val FAIL_CONTRACT_VERIFY = java.lang.Boolean.getBoolean("net.corda.contracts.incompatible.AttachmentContract.fail.verify") + override fun verify(tx: LedgerTransaction) { + if (FAIL_CONTRACT_VERIFY) throw object:TransactionVerificationException(tx.id, "AttachmentContract verify failed.", null) {} + val state = tx.outputsOfType().single() + // we check that at least one has the matching hash, the other will be the contract + require(tx.attachments.any { it.id == state.hash }) {"At least one attachment in transaction must match hash ${state.hash}"} + } + + object Command : TypeOnlyCommandData() + + data class State(val hash: SecureHash.SHA256) : ContractState { + private val FAIL_CONTRACT_STATE = java.lang.Boolean.getBoolean("net.corda.contracts.incompatible.AttachmentContract.fail.state") && (this.javaClass.classLoader !is AttachmentsClassLoader) + init { + if (FAIL_CONTRACT_STATE) throw TransactionVerificationException.TransactionRequiredContractUnspecifiedException(hash,"AttachmentContract state initialisation failed.") + } + override val participants: List = emptyList() + } +} + +const val ATTACHMENT_PROGRAM_ID = "net.corda.contracts.incompatible.version1.AttachmentContract" diff --git a/node/src/integration-test/kotlin/net/corda/contracts/incompatible/version2/AttachmentContract.kt b/node/src/integration-test/kotlin/net/corda/contracts/incompatible/version2/AttachmentContract.kt new file mode 100644 index 0000000000..00bb7e2371 --- /dev/null +++ b/node/src/integration-test/kotlin/net/corda/contracts/incompatible/version2/AttachmentContract.kt @@ -0,0 +1,25 @@ +package net.corda.contracts.incompatible.version2 + +import net.corda.core.contracts.Contract +import net.corda.core.contracts.ContractState +import net.corda.core.contracts.TypeOnlyCommandData +import net.corda.core.crypto.SecureHash +import net.corda.core.identity.AbstractParty +import net.corda.core.transactions.LedgerTransaction +import net.corda.core.utilities.OpaqueBytes + +class AttachmentContract : Contract { + override fun verify(tx: LedgerTransaction) { + val state = tx.outputsOfType().single() + // we check that at least one has the matching hash, the other will be the contract + require(tx.attachments.any { it.id == SecureHash.SHA256(state.opaqueBytes.bytes) }) {"At least one attachment in transaction must match hash ${state.opaqueBytes}"} + } + + object Command : TypeOnlyCommandData() + + data class State(val opaqueBytes: OpaqueBytes) : ContractState { + override val participants: List = emptyList() + } +} + +const val ATTACHMENT_PROGRAM_ID = "net.corda.contracts.incompatible.version2.AttachmentContract" diff --git a/node/src/integration-test/kotlin/net/corda/flows/incompatible/version1/AttachmentFlow.kt b/node/src/integration-test/kotlin/net/corda/flows/incompatible/version1/AttachmentFlow.kt new file mode 100644 index 0000000000..6a4ccd9d04 --- /dev/null +++ b/node/src/integration-test/kotlin/net/corda/flows/incompatible/version1/AttachmentFlow.kt @@ -0,0 +1,94 @@ +package net.corda.flows.incompatible.version1 + +import co.paralleluniverse.fibers.Suspendable +import net.corda.contracts.incompatible.version1.ATTACHMENT_PROGRAM_ID +import net.corda.contracts.incompatible.version1.AttachmentContract +import net.corda.core.contracts.Command +import net.corda.core.contracts.StateAndRef +import net.corda.core.contracts.TransactionState +import net.corda.core.crypto.SecureHash +import net.corda.core.flows.CollectSignaturesFlow +import net.corda.core.flows.FinalityFlow +import net.corda.core.flows.FlowLogic +import net.corda.core.flows.FlowSession +import net.corda.core.flows.InitiatedBy +import net.corda.core.flows.InitiatingFlow +import net.corda.core.flows.ReceiveFinalityFlow +import net.corda.core.flows.SignTransactionFlow +import net.corda.core.flows.StartableByRPC +import net.corda.core.identity.Party +import net.corda.core.node.StatesToRecord +import net.corda.core.transactions.SignedTransaction +import net.corda.core.transactions.TransactionBuilder +import net.corda.core.utilities.ProgressTracker +import net.corda.core.utilities.unwrap + +@InitiatingFlow +@StartableByRPC +class AttachmentFlow(private val otherSide: Party, + private val notary: Party, + private val attachId: SecureHash.SHA256, + private val notariseInputState: StateAndRef? = null) : FlowLogic() { + + object SIGNING : ProgressTracker.Step("Signing transaction") + + override val progressTracker: ProgressTracker = ProgressTracker(SIGNING) + + @Suspendable + override fun call(): SignedTransaction { + val session = initiateFlow(otherSide) + val notarise = notariseInputState != null + session.send(notarise) // inform peer whether to sign for notarisation + + // Create a trivial transaction with an output that describes the attachment, and the attachment itself + val ptx = TransactionBuilder(notary) + .addOutputState(AttachmentContract.State(attachId), ATTACHMENT_PROGRAM_ID) + .addAttachment(attachId) + if (notarise) { + ptx.addInputState(notariseInputState!!) + ptx.addCommand(AttachmentContract.Command, ourIdentity.owningKey, otherSide.owningKey) + } + else + ptx.addCommand(AttachmentContract.Command, ourIdentity.owningKey) + + progressTracker.currentStep = SIGNING + + val stx = serviceHub.signInitialTransaction(ptx) + val ftx = if (notarise) { + subFlow(CollectSignaturesFlow(stx, listOf(session))) + } else stx + + return subFlow(FinalityFlow(ftx, setOf(session), statesToRecord = StatesToRecord.ALL_VISIBLE)) + } +} + +@InitiatedBy(AttachmentFlow::class) +class StoreAttachmentFlow(private val otherSide: FlowSession) : FlowLogic() { + @Suspendable + override fun call() { + val notarise = otherSide.receive().unwrap { it } + if (notarise) { + val stx = subFlow(object : SignTransactionFlow(otherSide) { + override fun checkTransaction(stx: SignedTransaction) { + } + }) + subFlow(ReceiveFinalityFlow(otherSide, stx.id, statesToRecord = StatesToRecord.ALL_VISIBLE)) + } else { + subFlow(ReceiveFinalityFlow(otherSide, statesToRecord = StatesToRecord.ALL_VISIBLE)) + } + } +} + +@StartableByRPC +class AttachmentIssueFlow(private val attachId: SecureHash.SHA256, + private val notary: Party): FlowLogic() { + @Suspendable + override fun call(): SignedTransaction { + val builder = TransactionBuilder(notary) + builder.addAttachment(attachId) + builder.addOutputState(TransactionState(AttachmentContract.State(attachId), ATTACHMENT_PROGRAM_ID, notary)) + builder.addCommand(Command(AttachmentContract.Command, listOf(ourIdentity.owningKey))) + val tx = serviceHub.signInitialTransaction(builder, ourIdentity.owningKey) + return subFlow(FinalityFlow(tx, emptySet(), statesToRecord = StatesToRecord.ALL_VISIBLE)) + } +} diff --git a/node/src/integration-test/kotlin/net/corda/flows/incompatible/version2/AttachmentFlow.kt b/node/src/integration-test/kotlin/net/corda/flows/incompatible/version2/AttachmentFlow.kt new file mode 100644 index 0000000000..2a1faadef7 --- /dev/null +++ b/node/src/integration-test/kotlin/net/corda/flows/incompatible/version2/AttachmentFlow.kt @@ -0,0 +1,58 @@ +package net.corda.flows.incompatible.version2 + +import co.paralleluniverse.fibers.Suspendable +import net.corda.contracts.incompatible.version2.ATTACHMENT_PROGRAM_ID +import net.corda.contracts.incompatible.version2.AttachmentContract +import net.corda.core.crypto.SecureHash +import net.corda.core.flows.FinalityFlow +import net.corda.core.flows.FlowLogic +import net.corda.core.flows.FlowSession +import net.corda.core.flows.InitiatedBy +import net.corda.core.flows.InitiatingFlow +import net.corda.core.flows.ReceiveTransactionFlow +import net.corda.core.flows.StartableByRPC +import net.corda.core.identity.Party +import net.corda.core.node.StatesToRecord +import net.corda.core.transactions.SignedTransaction +import net.corda.core.transactions.TransactionBuilder +import net.corda.core.utilities.ProgressTracker + +@InitiatingFlow +@StartableByRPC +class AttachmentFlow(private val otherSide: Party, + private val notary: Party, + private val attachId: SecureHash.SHA256) : FlowLogic() { + + object SIGNING : ProgressTracker.Step("Signing transaction") + + override val progressTracker: ProgressTracker = ProgressTracker(SIGNING) + + @Suspendable + override fun call(): SignedTransaction { + // Create a trivial transaction with an output that describes the attachment, and the attachment itself + val ptx = TransactionBuilder(notary) + .addOutputState(AttachmentContract.State(attachId), ATTACHMENT_PROGRAM_ID) + .addCommand(AttachmentContract.Command, ourIdentity.owningKey) + .addAttachment(attachId) + + progressTracker.currentStep = SIGNING + + val stx = serviceHub.signInitialTransaction(ptx) + + // Send the transaction to the other recipient + return subFlow(FinalityFlow(stx, initiateFlow(otherSide))) + } +} + +@InitiatedBy(AttachmentFlow::class) +class StoreAttachmentFlow(private val otherSide: FlowSession) : FlowLogic() { + @Suspendable + override fun call() { + // purposely prevent transaction verification and recording in ReceiveTransactionFlow + val stx = subFlow(ReceiveTransactionFlow(otherSide, checkSufficientSignatures = false, statesToRecord = StatesToRecord.ALL_VISIBLE)) + logger.info("StoreAttachmentFlow: successfully received fully signed tx. Sending it to the vault for processing.") + + serviceHub.recordTransactions(StatesToRecord.ALL_VISIBLE, setOf(stx)) + logger.info("StoreAttachmentFlow: successfully recorded received transaction locally.") + } +} diff --git a/node/src/integration-test/kotlin/net/corda/flows/incompatible/version3/AttachmentFlow.kt b/node/src/integration-test/kotlin/net/corda/flows/incompatible/version3/AttachmentFlow.kt new file mode 100644 index 0000000000..082f4da8aa --- /dev/null +++ b/node/src/integration-test/kotlin/net/corda/flows/incompatible/version3/AttachmentFlow.kt @@ -0,0 +1,56 @@ +package net.corda.flows.incompatible.version3 + +import co.paralleluniverse.fibers.Suspendable +import net.corda.contracts.incompatible.version1.ATTACHMENT_PROGRAM_ID +import net.corda.contracts.incompatible.version1.AttachmentContract +import net.corda.core.crypto.SecureHash +import net.corda.core.flows.FinalityFlow +import net.corda.core.flows.FlowLogic +import net.corda.core.flows.FlowSession +import net.corda.core.flows.InitiatedBy +import net.corda.core.flows.InitiatingFlow +import net.corda.core.flows.ReceiveTransactionFlow +import net.corda.core.flows.StartableByRPC +import net.corda.core.identity.Party +import net.corda.core.node.StatesToRecord +import net.corda.core.transactions.SignedTransaction +import net.corda.core.transactions.TransactionBuilder +import net.corda.core.utilities.ProgressTracker + +@InitiatingFlow +@StartableByRPC +class AttachmentFlow(private val otherSide: Party, + private val notary: Party, + private val attachId: SecureHash.SHA256) : FlowLogic() { + + object SIGNING : ProgressTracker.Step("Signing transaction") + + override val progressTracker: ProgressTracker = ProgressTracker(SIGNING) + + @Suspendable + override fun call(): SignedTransaction { + // Create a trivial transaction with an output that describes the attachment, and the attachment itself + val ptx = TransactionBuilder(notary) + .addOutputState(AttachmentContract.State(attachId), ATTACHMENT_PROGRAM_ID) + .addCommand(AttachmentContract.Command, ourIdentity.owningKey) + .addAttachment(attachId) + + progressTracker.currentStep = SIGNING + + val stx = serviceHub.signInitialTransaction(ptx) + + // Send the transaction to the other recipient + return subFlow(FinalityFlow(stx, initiateFlow(otherSide))) + } +} + +@InitiatedBy(AttachmentFlow::class) +class StoreAttachmentFlow(private val otherSide: FlowSession) : FlowLogic() { + @Suspendable + override fun call() { + // purposely enable transaction verification and recording in ReceiveTransactionFlow + subFlow(ReceiveTransactionFlow(otherSide, checkSufficientSignatures = true, statesToRecord = StatesToRecord.ALL_VISIBLE)) + logger.info("StoreAttachmentFlow: successfully received fully signed tx. Sending it to the vault for processing.") + } +} + diff --git a/node/src/integration-test/kotlin/net/corda/node/VaultUpdateDeserializationTest.kt b/node/src/integration-test/kotlin/net/corda/node/VaultUpdateDeserializationTest.kt new file mode 100644 index 0000000000..9b0bc3f0e9 --- /dev/null +++ b/node/src/integration-test/kotlin/net/corda/node/VaultUpdateDeserializationTest.kt @@ -0,0 +1,249 @@ +package net.corda.node + +import co.paralleluniverse.strands.Strand +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertNotNull +import junit.framework.TestCase.assertTrue +import net.corda.core.flows.UnexpectedFlowEndException +import net.corda.core.internal.InputStreamAndHash +import net.corda.core.internal.deleteRecursively +import net.corda.core.internal.div +import net.corda.core.messaging.startFlow +import net.corda.core.messaging.vaultQueryBy +import net.corda.core.utilities.getOrThrow +import net.corda.core.utilities.seconds +import net.corda.flows.incompatible.version1.AttachmentIssueFlow +import net.corda.testing.core.ALICE_NAME +import net.corda.testing.core.BOB_NAME +import net.corda.testing.core.DUMMY_NOTARY_NAME +import net.corda.testing.core.singleIdentity +import net.corda.testing.driver.DriverParameters +import net.corda.testing.driver.NodeHandle +import net.corda.testing.driver.NodeParameters +import net.corda.testing.driver.OutOfProcess +import net.corda.testing.driver.driver +import net.corda.testing.driver.internal.incrementalPortAllocation +import net.corda.testing.flows.waitForAllFlowsToComplete +import net.corda.testing.node.NotarySpec +import net.corda.testing.node.TestCordapp +import net.corda.testing.node.internal.cordappWithPackages +import org.junit.Test +import java.util.concurrent.TimeoutException +import net.corda.contracts.incompatible.version1.AttachmentContract as AttachmentContractV1 +import net.corda.flows.incompatible.version1.AttachmentFlow as AttachmentFlowV1 + +class VaultUpdateDeserializationTest { + companion object { + // uses ReceiveFinalityFlow + val flowVersion1 = cordappWithPackages("net.corda.flows.incompatible.version1") + // single state field of type SecureHash.SHA256 with system property driven run-time behaviour: + // -force contract verify failure: -Dnet.corda.contracts.incompatible.AttachmentContract.fail.verify=true + // -force contract state init failure: -Dnet.corda.contracts.incompatible.AttachmentContract.fail.state=true + val contractVersion1 = cordappWithPackages("net.corda.contracts.incompatible.version1") + + fun driverParameters(cordapps: List): DriverParameters { + return DriverParameters( + portAllocation = incrementalPortAllocation(), + inMemoryDB = false, + startNodesInProcess = false, + notarySpecs = listOf(NotarySpec(DUMMY_NOTARY_NAME)), + cordappsForAllNodes = cordapps + ) + } + } + + /* + * Transaction sent from A -> B with Notarisation + * Test that a deserialization error is raised where the receiver node of a transaction has an incompatible contract jar. + * In the case of a notarised transaction, a deserialisation error is thrown in the receiver SignTransactionFlow (before finality) + * upon receiving the transaction to be signed and attempting to record its dependencies. + * The ledger will not record any transactions, and the flow must be retried by the sender upon installing the correct contract jar + * version at the receiver and re-starting the node. + */ + @Test(timeout=300_000) + fun `Notarised transaction fails completely upon receiver deserialization failure collecting signatures when using incompatible contract jar`() { + driver(driverParameters(listOf(flowVersion1, contractVersion1))) { + val alice = startNode(NodeParameters(additionalCordapps = listOf(flowVersion1, contractVersion1)), + providedName = ALICE_NAME).getOrThrow() + val bob = startNode(NodeParameters(additionalCordapps = listOf(flowVersion1, contractVersion1), + systemProperties = mapOf("net.corda.contracts.incompatible.AttachmentContract.fail.state" to "true")), + providedName = BOB_NAME).getOrThrow() + + val (inputStream, hash) = InputStreamAndHash.createInMemoryTestZip(1024, 0) + alice.rpc.uploadAttachment(inputStream) + + val stx = alice.rpc.startFlow(::AttachmentIssueFlow, hash, defaultNotaryIdentity).returnValue.getOrThrow(30.seconds) + val spendableState = stx.coreTransaction.outRef(0) + + // NOTE: exception is propagated from Receiver + try { + alice.rpc.startFlow(::AttachmentFlowV1, bob.nodeInfo.singleIdentity(), defaultNotaryIdentity, hash, spendableState).returnValue.getOrThrow(30.seconds) + } + catch(e: UnexpectedFlowEndException) { + println("Bob fails to deserialise transaction upon receipt of transaction for signing.") + } + assertEquals(0, bob.rpc.vaultQueryBy().states.size) + assertEquals(1, alice.rpc.vaultQueryBy().states.size) + // check transaction records + @Suppress("DEPRECATION") + assertEquals(1, alice.rpc.internalVerifiedTransactionsSnapshot().size) // issuance only + @Suppress("DEPRECATION") + assertTrue(bob.rpc.internalVerifiedTransactionsSnapshot().isEmpty()) + + // restart Bob with correct contract jar version + (bob as OutOfProcess).process.destroyForcibly() + bob.stop() + (baseDirectory(BOB_NAME) / "cordapps").deleteRecursively() + + val restartedBob = startNode(NodeParameters(additionalCordapps = listOf(flowVersion1, contractVersion1)), + providedName = BOB_NAME).getOrThrow() + // re-run failed flow + alice.rpc.startFlow(::AttachmentFlowV1, restartedBob.nodeInfo.singleIdentity(), defaultNotaryIdentity, hash, spendableState).returnValue.getOrThrow(30.seconds) + + assertEquals(1, waitForVaultUpdate(restartedBob)) + assertEquals(1, alice.rpc.vaultQueryBy().states.size) + @Suppress("DEPRECATION") + assertTrue(restartedBob.rpc.internalVerifiedTransactionsSnapshot().isNotEmpty()) + } + } + + /* + * Transaction sent from A -> B with Notarisation + * + * Test original deserialization failure behaviour by setting a new configurable java system property. + * The ledger will enter an inconsistent state from which is cannot auto-recover. + */ + @Test(timeout=300_000) + fun `Notarised transaction when using incompatible contract jar and overriden system property`() { + driver(driverParameters(listOf(flowVersion1, contractVersion1))) { + val alice = startNode(NodeParameters(additionalCordapps = listOf(flowVersion1, contractVersion1)), + providedName = ALICE_NAME).getOrThrow() + val bob = startNode(NodeParameters(additionalCordapps = listOf(flowVersion1, contractVersion1), + systemProperties = mapOf("net.corda.contracts.incompatible.AttachmentContract.fail.state" to "true", + "net.corda.vaultupdate.ignore.transaction.deserialization.errors" to "true", + "net.corda.recordtransaction.signature.verification.disabled" to "true")), + providedName = BOB_NAME).getOrThrow() + + val (inputStream, hash) = InputStreamAndHash.createInMemoryTestZip(1024, 0) + alice.rpc.uploadAttachment(inputStream) + + val stx = alice.rpc.startFlow(::AttachmentIssueFlow, hash, defaultNotaryIdentity).returnValue.getOrThrow(30.seconds) + val spendableState = stx.coreTransaction.outRef(0) + + // Flow completes successfully (deserialisation error on Receiver node is ignored) + alice.rpc.startFlow(::AttachmentFlowV1, bob.nodeInfo.singleIdentity(), defaultNotaryIdentity, hash, spendableState).returnValue.getOrThrow(30.seconds) + + // sender node correctly updated + @Suppress("DEPRECATION") + assertEquals(2, alice.rpc.internalVerifiedTransactionsSnapshot().size) + assertEquals(1, alice.rpc.vaultQueryBy().states.size) + + // receiver node has transaction but vault not updated! + @Suppress("DEPRECATION") + assertEquals(2, bob.rpc.internalVerifiedTransactionsSnapshot().size) + assertEquals(0, bob.rpc.vaultQueryBy().states.size) + } + } + + /* + * Transaction sent from A -> B without Notarisation + * Test that a deserialization error is raised where the receiver node of a finality flow has an incompatible contract jar. + * The ledger will be temporarily inconsistent until the correct contract jar version is installed and the receiver node is re-started. + */ + @Test(timeout=300_000) + fun `un-notarised transaction is hospitalized at receiver upon deserialization failure in vault update when using incompatible contract jar`() { + driver(driverParameters(emptyList())) { + val alice = startNode(NodeParameters(additionalCordapps = listOf(flowVersion1, contractVersion1)), + providedName = ALICE_NAME).getOrThrow() + val bob = startNode(NodeParameters(additionalCordapps = listOf(flowVersion1, contractVersion1), + systemProperties = mapOf("net.corda.contracts.incompatible.AttachmentContract.fail.state" to "true")), + providedName = BOB_NAME).getOrThrow() + + val (inputStream, hash) = InputStreamAndHash.createInMemoryTestZip(1024, 0) + alice.rpc.uploadAttachment(inputStream) + + // ISSUE: exception is not propagating from Receiver + try { + alice.rpc.startFlow(::AttachmentFlowV1, bob.nodeInfo.singleIdentity(), defaultNotaryIdentity, hash, null).returnValue.getOrThrow(30.seconds) + } + catch(e: TimeoutException) { + println("Alice: Timeout awaiting flow completion.") + } + assertEquals(0, bob.rpc.vaultQueryBy().states.size) + // check transaction records + @Suppress("DEPRECATION") + assertTrue(alice.rpc.internalVerifiedTransactionsSnapshot().isNotEmpty()) + @Suppress("DEPRECATION") + assertTrue(bob.rpc.internalVerifiedTransactionsSnapshot().isEmpty()) + + // restart Bob with correct contract jar version + (bob as OutOfProcess).process.destroyForcibly() + bob.stop() + (baseDirectory(BOB_NAME) / "cordapps").deleteRecursively() + + val restartedBob = startNode(NodeParameters(additionalCordapps = listOf(flowVersion1, contractVersion1)), + providedName = BOB_NAME).getOrThrow() + // original hospitalized transaction should now have been re-processed with correct contract jar + assertEquals(1, waitForVaultUpdate(restartedBob)) + @Suppress("DEPRECATION") + assertTrue(restartedBob.rpc.internalVerifiedTransactionsSnapshot().isNotEmpty()) + } + } + + /* + * Transaction sent from A -> B without Notarisation + * Test original deserialization failure behaviour by setting a new configurable java system property. + * The ledger will enter an inconsistent state from which is cannot auto-recover. + */ + @Test(timeout = 300_000) + fun `un-notarised transaction ignores deserialization failure in vault update when using incompatible contract jar and overriden system property`() { + driver(driverParameters(emptyList())) { + val alice = startNode(NodeParameters(additionalCordapps = listOf(flowVersion1, contractVersion1)), + providedName = ALICE_NAME).getOrThrow() + val bob = startNode(NodeParameters(additionalCordapps = listOf(flowVersion1, contractVersion1), + systemProperties = mapOf( + "net.corda.contracts.incompatible.AttachmentContract.fail.state" to "true", + "net.corda.vaultupdate.ignore.transaction.deserialization.errors" to "true", + "net.corda.recordtransaction.signature.verification.disabled" to "true")), + providedName = BOB_NAME).getOrThrow() + + val (inputStream, hash) = InputStreamAndHash.createInMemoryTestZip(1024, 0) + alice.rpc.uploadAttachment(inputStream) + + // Note: TransactionDeserialisationException is swallowed on the receiver node (without updating the vault). + val stx = alice.rpc.startFlow(::AttachmentFlowV1, bob.nodeInfo.singleIdentity(), defaultNotaryIdentity, hash, null).returnValue.getOrThrow(30.seconds) + println("Alice txId: ${stx.id}") + + waitForAllFlowsToComplete(bob) + val txId = bob.rpc.stateMachineRecordedTransactionMappingSnapshot().single().transactionId + println("Bob txId: $txId") + + assertEquals(0, bob.rpc.vaultQueryBy().states.size) + + // restart Bob with correct contract jar version + (bob as OutOfProcess).process.destroyForcibly() + bob.stop() + (baseDirectory(BOB_NAME) / "cordapps").deleteRecursively() + + val restartedBob = startNode(NodeParameters(additionalCordapps = listOf(flowVersion1, contractVersion1)), + providedName = BOB_NAME).getOrThrow() + // transaction recorded + @Suppress("DEPRECATION") + assertNotNull(restartedBob.rpc.internalFindVerifiedTransaction(txId)) + // but vault states not updated + assertEquals(0, restartedBob.rpc.vaultQueryBy().states.size) + } + } + + private fun waitForVaultUpdate(nodeHandle: NodeHandle, maxIterations: Int = 5, iterationDelay: Long = 500): Int { + repeat((0..maxIterations).count()) { + val count = nodeHandle.rpc.vaultQueryBy().states + if (count.isNotEmpty()) { + return count.size + } + Strand.sleep(iterationDelay) + } + return 0 + } +} + diff --git a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt index a444b11cc4..a2623b638d 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt @@ -21,6 +21,7 @@ import net.corda.core.internal.VisibleForTesting import net.corda.core.internal.bufferUntilSubscribed import net.corda.core.internal.tee import net.corda.core.internal.uncheckedCast +import net.corda.core.internal.warnOnce import net.corda.core.messaging.DataFeed import net.corda.core.node.StatesToRecord import net.corda.core.node.services.KeyManagementService @@ -107,6 +108,8 @@ class NodeVaultService( const val DEFAULT_SOFT_LOCKING_SQL_IN_CLAUSE_SIZE = 16 + private val IGNORE_TRANSACTION_DESERIALIZATION_ERRORS = java.lang.Boolean.getBoolean("net.corda.vaultupdate.ignore.transaction.deserialization.errors") + /** * Establish whether a given state is relevant to a node, given the node's public keys. * @@ -307,18 +310,29 @@ class NodeVaultService( private fun makeUpdates(batch: Iterable, statesToRecord: StatesToRecord, previouslySeen: Boolean): List> { - fun withValidDeserialization(list: List, txId: SecureHash): Map = (0 until list.size).mapNotNull { idx -> - try { - idx to list[idx] - } catch (e: TransactionDeserialisationException) { - // When resolving transaction dependencies we might encounter contracts we haven't installed locally. - // This will cause a failure as we can't deserialize such states in the context of the `appClassloader`. - // For now we ignore these states. - // In the future we will use the AttachmentsClassloader to correctly deserialize and asses the relevancy. - log.warn("Could not deserialize state $idx from transaction $txId. Cause: $e") - null - } - }.toMap() + fun withValidDeserialization(list: List, txId: SecureHash): Map { + var error: TransactionDeserialisationException? = null + val map = (0 until list.size).mapNotNull { idx -> + try { + idx to list[idx] + } catch (e: TransactionDeserialisationException) { + // When resolving transaction dependencies we might encounter contracts we haven't installed locally. + // This will cause a failure as we can't deserialize such states in the context of the `appClassloader`. + // For now we ignore these states. + // In the future we will use the AttachmentsClassloader to correctly deserialize and asses the relevancy. + if (IGNORE_TRANSACTION_DESERIALIZATION_ERRORS) { + log.warnOnce("The current usage of transaction deserialization for the vault is unsafe." + + "Ignoring vault updates due to failed deserialized states may lead to severe problems with ledger consistency. ") + log.warn("Could not deserialize state $idx from transaction $txId. Cause: $e") + } else { + log.error("Could not deserialize state $idx from transaction $txId. Cause: $e") + if(error == null) error = e + } + null + } + }.toMap() + return error?.let { throw it } ?: map + } // Returns only output states that can be deserialised successfully. fun WireTransaction.deserializableOutputStates(): Map> = withValidDeserialization(this.outputs, this.id) diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/NodeParameters.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/NodeParameters.kt index b1604b94df..12b55fc146 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/NodeParameters.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/NodeParameters.kt @@ -36,7 +36,8 @@ data class NodeParameters( val additionalCordapps: Collection = emptySet(), val flowOverrides: Map>, Class>> = emptyMap(), val logLevelOverride: String? = null, - val rpcAddress: NetworkHostAndPort? = null + val rpcAddress: NetworkHostAndPort? = null, + val systemProperties: Map = emptyMap() ) { /** * Create a new node parameters object with default values. Each parameter can be specified with its wither method which returns a copy @@ -127,4 +128,97 @@ data class NodeParameters( flowOverrides = flowOverrides, logLevelOverride = logLevelOverride) + constructor( + providedName: CordaX500Name?, + rpcUsers: List, + verifierType: VerifierType, + customOverrides: Map, + startInSameProcess: Boolean?, + maximumHeapSize: String, + additionalCordapps: Collection = emptySet(), + flowOverrides: Map>, Class>>, + logLevelOverride: String? = null + ) : this( + providedName, + rpcUsers, + verifierType, + customOverrides, + startInSameProcess, + maximumHeapSize, + additionalCordapps, + flowOverrides, + logLevelOverride, + rpcAddress = null) + + @Suppress("LongParameterList") + fun copy( + providedName: CordaX500Name?, + rpcUsers: List, + verifierType: VerifierType, + customOverrides: Map, + startInSameProcess: Boolean?, + maximumHeapSize: String, + additionalCordapps: Collection = emptySet(), + flowOverrides: Map>, Class>>, + logLevelOverride: String? = null + ) = this.copy( + providedName = providedName, + rpcUsers = rpcUsers, + verifierType = verifierType, + customOverrides = customOverrides, + startInSameProcess = startInSameProcess, + maximumHeapSize = maximumHeapSize, + additionalCordapps = additionalCordapps, + flowOverrides = flowOverrides, + logLevelOverride = logLevelOverride, + rpcAddress = rpcAddress) + + constructor( + providedName: CordaX500Name?, + rpcUsers: List, + verifierType: VerifierType, + customOverrides: Map, + startInSameProcess: Boolean?, + maximumHeapSize: String, + additionalCordapps: Collection = emptySet(), + flowOverrides: Map>, Class>>, + logLevelOverride: String? = null, + rpcAddress: NetworkHostAndPort? = null + ) : this( + providedName, + rpcUsers, + verifierType, + customOverrides, + startInSameProcess, + maximumHeapSize, + additionalCordapps, + flowOverrides, + logLevelOverride, + rpcAddress, + systemProperties = emptyMap()) + + @Suppress("LongParameterList") + fun copy( + providedName: CordaX500Name?, + rpcUsers: List, + verifierType: VerifierType, + customOverrides: Map, + startInSameProcess: Boolean?, + maximumHeapSize: String, + additionalCordapps: Collection = emptySet(), + flowOverrides: Map>, Class>>, + logLevelOverride: String? = null, + rpcAddress: NetworkHostAndPort? = null + ) = this.copy( + providedName = providedName, + rpcUsers = rpcUsers, + verifierType = verifierType, + customOverrides = customOverrides, + startInSameProcess = startInSameProcess, + maximumHeapSize = maximumHeapSize, + additionalCordapps = additionalCordapps, + flowOverrides = flowOverrides, + logLevelOverride = logLevelOverride, + rpcAddress = rpcAddress, + systemProperties = systemProperties) } diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt index b4b7d673e8..6ce7a2e419 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt @@ -755,7 +755,7 @@ class DriverDSLImpl( debugPort, bytemanJarPath, bytemanPort, - systemProperties, + systemProperties + parameters.systemProperties, parameters.maximumHeapSize, parameters.logLevelOverride, identifier, From 6243088ebb4e1cc1cb11bcc7a19b5d611fb6ff44 Mon Sep 17 00:00:00 2001 From: Balwant Kothari Date: Thu, 21 Sep 2023 21:52:27 +0530 Subject: [PATCH 075/126] ENT-10700 Updating timeWindow to nullable (#7498) * ENT-10700 Updating timeWindow to nullable --- .../net/corda/core/flows/LedgerRecoverFlow.kt | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/flows/LedgerRecoverFlow.kt b/core/src/main/kotlin/net/corda/core/flows/LedgerRecoverFlow.kt index f9d7353ed4..41a869e2ff 100644 --- a/core/src/main/kotlin/net/corda/core/flows/LedgerRecoverFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/LedgerRecoverFlow.kt @@ -15,7 +15,7 @@ import net.corda.core.utilities.ProgressTracker @InitiatingFlow class LedgerRecoveryFlow( private val recoveryPeers: Collection, - private val timeWindow: RecoveryTimeWindow, + private val timeWindow: RecoveryTimeWindow? = null, private val useAllNetworkNodes: Boolean = false, private val transactionRole: TransactionRole = TransactionRole.ALL, private val dryRun: Boolean = false, @@ -24,7 +24,7 @@ class LedgerRecoveryFlow( @CordaInternal data class ExtraConstructorArgs(val recoveryPeers: Collection, - val timeWindow: RecoveryTimeWindow, + val timeWindow: RecoveryTimeWindow? = null, val useAllNetworkNodes: Boolean, val transactionRole: TransactionRole, val dryRun: Boolean, @@ -33,13 +33,13 @@ class LedgerRecoveryFlow( fun getExtraConstructorArgs() = ExtraConstructorArgs(recoveryPeers, timeWindow, useAllNetworkNodes, transactionRole, dryRun, optimisticInitiatorRecovery) // unused constructors added to facilitate Node Shell command invocation - constructor(recoveryPeer: Party, timeWindow: RecoveryTimeWindow) : this(setOf(recoveryPeer), timeWindow, false, TransactionRole.ALL, false, false) - constructor(recoveryPeer: Party, timeWindow: RecoveryTimeWindow, dryRun: Boolean) : this(setOf(recoveryPeer), timeWindow, false, TransactionRole.ALL, dryRun, false) + constructor(recoveryPeer: Party, timeWindow: RecoveryTimeWindow?) : this(setOf(recoveryPeer), timeWindow, false, TransactionRole.ALL, false, false) + constructor(recoveryPeer: Party, timeWindow: RecoveryTimeWindow?, dryRun: Boolean) : this(setOf(recoveryPeer), timeWindow, false, TransactionRole.ALL, dryRun, false) - constructor(timeWindow: RecoveryTimeWindow, dryRun: Boolean) : this(emptySet(), timeWindow, false, TransactionRole.ALL, dryRun, false) - constructor(timeWindow: RecoveryTimeWindow, dryRun: Boolean, optimisticInitiatorRecovery: Boolean) : this(emptySet(), timeWindow, false, TransactionRole.ALL, dryRun, optimisticInitiatorRecovery) - constructor(recoveryPeers: Collection, timeWindow: RecoveryTimeWindow, dryRun: Boolean) : this(recoveryPeers, timeWindow, false, TransactionRole.ALL, dryRun, false) - constructor(recoveryPeers: Collection, timeWindow: RecoveryTimeWindow, dryRun: Boolean, optimisticInitiatorRecovery: Boolean) : this(recoveryPeers, timeWindow, false, TransactionRole.ALL, dryRun, optimisticInitiatorRecovery) + constructor(timeWindow: RecoveryTimeWindow?, dryRun: Boolean) : this(emptySet(), timeWindow, false, TransactionRole.ALL, dryRun, false) + constructor(timeWindow: RecoveryTimeWindow?, dryRun: Boolean, optimisticInitiatorRecovery: Boolean) : this(emptySet(), timeWindow, false, TransactionRole.ALL, dryRun, optimisticInitiatorRecovery) + constructor(recoveryPeers: Collection, timeWindow: RecoveryTimeWindow?, dryRun: Boolean) : this(recoveryPeers, timeWindow, false, TransactionRole.ALL, dryRun, false) + constructor(recoveryPeers: Collection, timeWindow: RecoveryTimeWindow?, dryRun: Boolean, optimisticInitiatorRecovery: Boolean) : this(recoveryPeers, timeWindow, false, TransactionRole.ALL, dryRun, optimisticInitiatorRecovery) @Suspendable @Throws(LedgerRecoveryException::class) From ea4bcd53691788d5582c75bba71eedf2084df170 Mon Sep 17 00:00:00 2001 From: Shams Asari Date: Fri, 22 Sep 2023 15:08:11 +0100 Subject: [PATCH 076/126] ENT-10811: Removed index on primary key column in node_aes_encryption_keys table The index is redundant on the primary key, and causes an issue in the schema migration for some databases, such as Oracle. --- .../main/resources/migration/node-core.changelog-v26.xml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/node/src/main/resources/migration/node-core.changelog-v26.xml b/node/src/main/resources/migration/node-core.changelog-v26.xml index b0d4925c7a..f2bdef4833 100644 --- a/node/src/main/resources/migration/node-core.changelog-v26.xml +++ b/node/src/main/resources/migration/node-core.changelog-v26.xml @@ -19,10 +19,4 @@ - - - - - - From 3b243020452a4e2ae981ec7518e57d51af309adb Mon Sep 17 00:00:00 2001 From: Chris Cochrane Date: Wed, 27 Sep 2023 11:27:58 +0100 Subject: [PATCH 077/126] Bumped Jetty version --- constants.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/constants.properties b/constants.properties index 55e9e68db7..6c78be4da5 100644 --- a/constants.properties +++ b/constants.properties @@ -52,7 +52,7 @@ artemisVersion=2.19.1 # TODO Upgrade Jackson only when corda is using kotlin 1.3.10 jacksonVersion=2.13.5 jacksonKotlinVersion=2.9.7 -jettyVersion=9.4.19.v20190610 +jettyVersion=9.4.52.v20230823 jerseyVersion=2.25 servletVersion=4.0.1 assertjVersion=3.12.2 From 238608224b2260da8429c40456fcdd92db8c0991 Mon Sep 17 00:00:00 2001 From: Adel El-Beik <48713346+adelel1@users.noreply.github.com> Date: Wed, 27 Sep 2023 14:01:44 +0100 Subject: [PATCH 078/126] =?UTF-8?q?ENT-10607:=20Reduced=20length=20of=20di?= =?UTF-8?q?stribution=20records=20table=20name=20to=20under=E2=80=A6=20(#7?= =?UTF-8?q?505)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ENT-10607: Reduced length of distribution records table name to under 30 chars for Oracle. * ENT-10607: Need to update the entity classes as well. --- .../flows/FinalityFlowErrorHandlingTest.kt | 2 +- .../DBTransactionStorageLedgerRecovery.kt | 4 ++-- .../migration/node-core.changelog-v25.xml | 24 +++++++++---------- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/node/src/integration-test-slow/kotlin/net/corda/node/flows/FinalityFlowErrorHandlingTest.kt b/node/src/integration-test-slow/kotlin/net/corda/node/flows/FinalityFlowErrorHandlingTest.kt index 9617c21fb3..2ea07f4c7a 100644 --- a/node/src/integration-test-slow/kotlin/net/corda/node/flows/FinalityFlowErrorHandlingTest.kt +++ b/node/src/integration-test-slow/kotlin/net/corda/node/flows/FinalityFlowErrorHandlingTest.kt @@ -94,7 +94,7 @@ class GetFlowTransaction(private val txId: SecureHash) : FlowLogic ps.executeQuery().use { rs -> diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecovery.kt b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecovery.kt index 69df1fe9b3..31c5dc4a59 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecovery.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecovery.kt @@ -54,7 +54,7 @@ class DBTransactionStorageLedgerRecovery(private val database: CordaPersistence, @CordaSerializable @Entity - @Table(name = "${NODE_DATABASE_PREFIX}sender_distribution_records") + @Table(name = "${NODE_DATABASE_PREFIX}sender_distr_recs") data class DBSenderDistributionRecord( @EmbeddedId var compositeKey: PersistentKey, @@ -77,7 +77,7 @@ class DBTransactionStorageLedgerRecovery(private val database: CordaPersistence, @CordaSerializable @Entity - @Table(name = "${NODE_DATABASE_PREFIX}receiver_distribution_records") + @Table(name = "${NODE_DATABASE_PREFIX}receiver_distr_recs") class DBReceiverDistributionRecord( @EmbeddedId var compositeKey: PersistentKey, diff --git a/node/src/main/resources/migration/node-core.changelog-v25.xml b/node/src/main/resources/migration/node-core.changelog-v25.xml index 9ea40bada9..c720483efa 100644 --- a/node/src/main/resources/migration/node-core.changelog-v25.xml +++ b/node/src/main/resources/migration/node-core.changelog-v25.xml @@ -11,7 +11,7 @@ - + @@ -30,13 +30,13 @@ - - + + - + @@ -58,9 +58,9 @@ - - + + @@ -79,14 +79,14 @@ - - From 3a23f60199337ba84f654d73982ffcc5a8e21fb4 Mon Sep 17 00:00:00 2001 From: Adel El-Beik <48713346+adelel1@users.noreply.github.com> Date: Fri, 29 Sep 2023 09:03:00 +0100 Subject: [PATCH 079/126] =?UTF-8?q?ENT-9943:=20Now=20use=20SecureHash=20to?= =?UTF-8?q?=20represent=20CordaX500Name=20in=20distributi=E2=80=A6=20(#750?= =?UTF-8?q?1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../coretests/flows/FinalityFlowTests.kt | 24 ++++++++--------- .../network/PersistentPartyInfoCacheTest.kt | 25 ++++++++--------- .../network/PersistentPartyInfoCache.kt | 27 ++++++++++--------- .../DBTransactionStorageLedgerRecovery.kt | 26 +++++++++--------- .../persistence/HashedDistributionList.kt | 12 ++++++--- .../migration/node-core.changelog-v25.xml | 6 ++--- ...DBTransactionStorageLedgerRecoveryTests.kt | 6 ++--- 7 files changed, 66 insertions(+), 60 deletions(-) diff --git a/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt b/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt index 421e1581cf..bfbb93c96b 100644 --- a/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt +++ b/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt @@ -355,14 +355,14 @@ class FinalityFlowTests : WithFinality { val sdrs = getSenderRecoveryData(stx.id, aliceNode.database).apply { assertEquals(1, this.size) assertEquals(StatesToRecord.ALL_VISIBLE, this[0].statesToRecord) - assertEquals(BOB_NAME.hashCode().toLong(), this[0].peerPartyId) + assertEquals(SecureHash.sha256(BOB_NAME.toString()), this[0].peerPartyId) } val rdr = getReceiverRecoveryData(stx.id, bobNode).apply { assertNotNull(this) val hashedDL = HashedDistributionList.decrypt(this!!.encryptedDistributionList.bytes, aliceNode.internals.encryptionService) assertEquals(StatesToRecord.ONLY_RELEVANT, hashedDL.senderStatesToRecord) - assertEquals(aliceNode.info.singleIdentity().name.hashCode().toLong(), this.initiatorPartyId) - assertEquals(mapOf(BOB_NAME.hashCode().toLong() to StatesToRecord.ALL_VISIBLE), hashedDL.peerHashToStatesToRecord) + assertEquals(SecureHash.sha256(aliceNode.info.singleIdentity().name.toString()), this.initiatorPartyId) + assertEquals(mapOf(SecureHash.sha256(BOB_NAME.toString()) to StatesToRecord.ALL_VISIBLE), hashedDL.peerHashToStatesToRecord) } validateSenderAndReceiverTimestamps(sdrs, rdr!!) } @@ -388,19 +388,19 @@ class FinalityFlowTests : WithFinality { val sdrs = getSenderRecoveryData(stx.id, aliceNode.database).apply { assertEquals(2, this.size) assertEquals(StatesToRecord.ONLY_RELEVANT, this[0].statesToRecord) - assertEquals(BOB_NAME.hashCode().toLong(), this[0].peerPartyId) + assertEquals(SecureHash.sha256(BOB_NAME.toString()), this[0].peerPartyId) assertEquals(StatesToRecord.ALL_VISIBLE, this[1].statesToRecord) - assertEquals(CHARLIE_NAME.hashCode().toLong(), this[1].peerPartyId) + assertEquals(SecureHash.sha256(CHARLIE_NAME.toString()), this[1].peerPartyId) } val rdr = getReceiverRecoveryData(stx.id, bobNode).apply { assertNotNull(this) val hashedDL = HashedDistributionList.decrypt(this!!.encryptedDistributionList.bytes, aliceNode.internals.encryptionService) assertEquals(StatesToRecord.ONLY_RELEVANT, hashedDL.senderStatesToRecord) - assertEquals(aliceNode.info.singleIdentity().name.hashCode().toLong(), this.initiatorPartyId) + assertEquals(SecureHash.sha256(aliceNode.info.singleIdentity().name.toString()), this.initiatorPartyId) // note: Charlie assertion here is using the hinted StatesToRecord value passed to it from Alice - assertEquals(mapOf( - BOB_NAME.hashCode().toLong() to StatesToRecord.ONLY_RELEVANT, - CHARLIE_NAME.hashCode().toLong() to StatesToRecord.ALL_VISIBLE + assertEquals(mapOf( + SecureHash.sha256(BOB_NAME.toString()) to StatesToRecord.ONLY_RELEVANT, + SecureHash.sha256(CHARLIE_NAME.toString()) to StatesToRecord.ALL_VISIBLE ), hashedDL.peerHashToStatesToRecord) } validateSenderAndReceiverTimestamps(sdrs, rdr!!) @@ -452,14 +452,14 @@ class FinalityFlowTests : WithFinality { val sdr = getSenderRecoveryData(stx.id, aliceNode.database).apply { assertEquals(1, this.size) assertEquals(StatesToRecord.ONLY_RELEVANT, this[0].statesToRecord) - assertEquals(BOB_NAME.hashCode().toLong(), this[0].peerPartyId) + assertEquals(SecureHash.sha256(BOB_NAME.toString()), this[0].peerPartyId) } val rdr = getReceiverRecoveryData(stx.id, bobNode).apply { assertNotNull(this) val hashedDL = HashedDistributionList.decrypt(this!!.encryptedDistributionList.bytes, aliceNode.internals.encryptionService) assertEquals(StatesToRecord.ONLY_RELEVANT, hashedDL.senderStatesToRecord) - assertEquals(aliceNode.info.singleIdentity().name.hashCode().toLong(), this.initiatorPartyId) - assertEquals(mapOf(BOB_NAME.hashCode().toLong() to StatesToRecord.ONLY_RELEVANT), hashedDL.peerHashToStatesToRecord) + assertEquals(SecureHash.sha256(aliceNode.info.singleIdentity().name.toString()), this.initiatorPartyId) + assertEquals(mapOf(SecureHash.sha256(BOB_NAME.toString()) to StatesToRecord.ONLY_RELEVANT), hashedDL.peerHashToStatesToRecord) } validateSenderAndReceiverTimestamps(sdr, rdr!!) } diff --git a/node/src/integration-test/kotlin/net/corda/node/services/network/PersistentPartyInfoCacheTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/network/PersistentPartyInfoCacheTest.kt index 42931cebc0..bb8e4ec9fc 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/network/PersistentPartyInfoCacheTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/network/PersistentPartyInfoCacheTest.kt @@ -1,5 +1,6 @@ package net.corda.node.services.network +import net.corda.core.crypto.SecureHash import net.corda.core.node.NodeInfo import net.corda.core.utilities.NetworkHostAndPort import net.corda.node.services.identity.InMemoryIdentityService @@ -35,9 +36,9 @@ class PersistentPartyInfoCacheTest { createNodeInfo(listOf(CHARLIE)))) val partyInfoCache = PersistentPartyInfoCache(charlieNetMapCache, TestingNamedCacheFactory(), database) partyInfoCache.start() - assertThat(partyInfoCache.getPartyIdByCordaX500Name(ALICE.name)).isEqualTo(ALICE.name.hashCode().toLong()) - assertThat(partyInfoCache.getPartyIdByCordaX500Name(BOB.name)).isEqualTo(BOB.name.hashCode().toLong()) - assertThat(partyInfoCache.getPartyIdByCordaX500Name(CHARLIE.name)).isEqualTo(CHARLIE.name.hashCode().toLong()) + assertThat(partyInfoCache.getPartyIdByCordaX500Name(ALICE.name)).isEqualTo(SecureHash.sha256(ALICE.name.toString())) + assertThat(partyInfoCache.getPartyIdByCordaX500Name(BOB.name)).isEqualTo(SecureHash.sha256(BOB.name.toString())) + assertThat(partyInfoCache.getPartyIdByCordaX500Name(CHARLIE.name)).isEqualTo(SecureHash.sha256(CHARLIE.name.toString())) } @Test(timeout=300_000) @@ -50,9 +51,9 @@ class PersistentPartyInfoCacheTest { // clear network map cache & bootstrap another PersistentInfoCache charlieNetMapCache.clearNetworkMapCache() val partyInfoCache = PersistentPartyInfoCache(charlieNetMapCache, TestingNamedCacheFactory(), database) - assertThat(partyInfoCache.getPartyIdByCordaX500Name(ALICE.name)).isEqualTo(ALICE.name.hashCode().toLong()) - assertThat(partyInfoCache.getPartyIdByCordaX500Name(BOB.name)).isEqualTo(BOB.name.hashCode().toLong()) - assertThat(partyInfoCache.getPartyIdByCordaX500Name(CHARLIE.name)).isEqualTo(CHARLIE.name.hashCode().toLong()) + assertThat(partyInfoCache.getPartyIdByCordaX500Name(ALICE.name)).isEqualTo(SecureHash.sha256(ALICE.name.toString())) + assertThat(partyInfoCache.getPartyIdByCordaX500Name(BOB.name)).isEqualTo(SecureHash.sha256(BOB.name.toString())) + assertThat(partyInfoCache.getPartyIdByCordaX500Name(CHARLIE.name)).isEqualTo(SecureHash.sha256(CHARLIE.name.toString())) } @Test(timeout=300_000) @@ -63,9 +64,9 @@ class PersistentPartyInfoCacheTest { createNodeInfo(listOf(CHARLIE)))) val partyInfoCache = PersistentPartyInfoCache(charlieNetMapCache, TestingNamedCacheFactory(), database) partyInfoCache.start() - assertThat(partyInfoCache.getCordaX500NameByPartyId(ALICE.name.hashCode().toLong())).isEqualTo(ALICE.name) - assertThat(partyInfoCache.getCordaX500NameByPartyId(BOB.name.hashCode().toLong())).isEqualTo(BOB.name) - assertThat(partyInfoCache.getCordaX500NameByPartyId(CHARLIE.name.hashCode().toLong())).isEqualTo(CHARLIE.name) + assertThat(partyInfoCache.getCordaX500NameByPartyId(SecureHash.sha256(ALICE.name.toString()))).isEqualTo(ALICE.name) + assertThat(partyInfoCache.getCordaX500NameByPartyId(SecureHash.sha256(BOB.name.toString()))).isEqualTo(BOB.name) + assertThat(partyInfoCache.getCordaX500NameByPartyId(SecureHash.sha256(CHARLIE.name.toString()))).isEqualTo(CHARLIE.name) } @Test(timeout=300_000) @@ -78,9 +79,9 @@ class PersistentPartyInfoCacheTest { // clear network map cache & bootstrap another PersistentInfoCache charlieNetMapCache.clearNetworkMapCache() val partyInfoCache = PersistentPartyInfoCache(charlieNetMapCache, TestingNamedCacheFactory(), database) - assertThat(partyInfoCache.getCordaX500NameByPartyId(ALICE.name.hashCode().toLong())).isEqualTo(ALICE.name) - assertThat(partyInfoCache.getCordaX500NameByPartyId(BOB.name.hashCode().toLong())).isEqualTo(BOB.name) - assertThat(partyInfoCache.getCordaX500NameByPartyId(CHARLIE.name.hashCode().toLong())).isEqualTo(CHARLIE.name) + assertThat(partyInfoCache.getCordaX500NameByPartyId(SecureHash.sha256(ALICE.name.toString()))).isEqualTo(ALICE.name) + assertThat(partyInfoCache.getCordaX500NameByPartyId(SecureHash.sha256(BOB.name.toString()))).isEqualTo(BOB.name) + assertThat(partyInfoCache.getCordaX500NameByPartyId(SecureHash.sha256(CHARLIE.name.toString()))).isEqualTo(CHARLIE.name) } private fun createNodeInfo(identities: List, diff --git a/node/src/main/kotlin/net/corda/node/services/network/PersistentPartyInfoCache.kt b/node/src/main/kotlin/net/corda/node/services/network/PersistentPartyInfoCache.kt index e9ba12a3a6..6cee01c45a 100644 --- a/node/src/main/kotlin/net/corda/node/services/network/PersistentPartyInfoCache.kt +++ b/node/src/main/kotlin/net/corda/node/services/network/PersistentPartyInfoCache.kt @@ -1,5 +1,6 @@ package net.corda.node.services.network +import net.corda.core.crypto.SecureHash import net.corda.core.identity.CordaX500Name import net.corda.core.internal.NamedCacheFactory import net.corda.core.node.services.NetworkMapCache @@ -14,13 +15,13 @@ class PersistentPartyInfoCache(private val networkMapCache: PersistentNetworkMap private val database: CordaPersistence) { // probably better off using a BiMap here: https://www.baeldung.com/guava-bimap - private val cordaX500NameToPartyIdCache = NonInvalidatingCache( + private val cordaX500NameToPartyIdCache = NonInvalidatingCache( cacheFactory = cacheFactory, name = "RecoveryPartyInfoCache_byCordaX500Name") { key -> database.transaction { queryByCordaX500Name(session, key) } } - private val partyIdToCordaX500NameCache = NonInvalidatingCache( + private val partyIdToCordaX500NameCache = NonInvalidatingCache( cacheFactory = cacheFactory, name = "RecoveryPartyInfoCache_byPartyId") { key -> database.transaction { queryByPartyId(session, key) } @@ -32,48 +33,48 @@ class PersistentPartyInfoCache(private val networkMapCache: PersistentNetworkMap val (snapshot, updates) = networkMapCache.track() snapshot.map { entry -> entry.legalIdentities.map { party -> - add(party.name.hashCode().toLong(), party.name) + add(SecureHash.sha256(party.name.toString()), party.name) } } trackNetworkMapUpdates = updates trackNetworkMapUpdates.cache().forEach { nodeInfo -> nodeInfo.node.legalIdentities.map { party -> - add(party.name.hashCode().toLong(), party.name) + add(SecureHash.sha256(party.name.toString()), party.name) } } } - fun getPartyIdByCordaX500Name(name: CordaX500Name): Long = cordaX500NameToPartyIdCache[name] ?: throw IllegalStateException("Missing cache entry for $name") + fun getPartyIdByCordaX500Name(name: CordaX500Name): SecureHash = cordaX500NameToPartyIdCache[name] ?: throw IllegalStateException("Missing cache entry for $name") - fun getCordaX500NameByPartyId(partyId: Long): CordaX500Name = partyIdToCordaX500NameCache[partyId] ?: throw IllegalStateException("Missing cache entry for $partyId") + fun getCordaX500NameByPartyId(partyId: SecureHash): CordaX500Name = partyIdToCordaX500NameCache[partyId] ?: throw IllegalStateException("Missing cache entry for $partyId") - private fun add(partyHashCode: Long, partyName: CordaX500Name) { + private fun add(partyHashCode: SecureHash, partyName: CordaX500Name) { partyIdToCordaX500NameCache.cache.put(partyHashCode, partyName) cordaX500NameToPartyIdCache.cache.put(partyName, partyHashCode) updateInfoDB(partyHashCode, partyName) } - private fun updateInfoDB(partyHashCode: Long, partyName: CordaX500Name) { + private fun updateInfoDB(partyHashCode: SecureHash, partyName: CordaX500Name) { database.transaction { if (queryByPartyId(session, partyHashCode) == null) { - session.save(DBTransactionStorageLedgerRecovery.DBRecoveryPartyInfo(partyHashCode, partyName.toString())) + session.save(DBTransactionStorageLedgerRecovery.DBRecoveryPartyInfo(partyHashCode.toString(), partyName.toString())) } } } - private fun queryByCordaX500Name(session: Session, key: CordaX500Name): Long? { + private fun queryByCordaX500Name(session: Session, key: CordaX500Name): SecureHash? { val query = session.createQuery( "FROM ${DBTransactionStorageLedgerRecovery.DBRecoveryPartyInfo::class.java.name} WHERE partyName = :partyName", DBTransactionStorageLedgerRecovery.DBRecoveryPartyInfo::class.java) query.setParameter("partyName", key.toString()) - return query.resultList.singleOrNull()?.partyId + return query.resultList.singleOrNull()?.let { SecureHash.parse(it.partyId) } } - private fun queryByPartyId(session: Session, key: Long): CordaX500Name? { + private fun queryByPartyId(session: Session, key: SecureHash): CordaX500Name? { val query = session.createQuery( "FROM ${DBTransactionStorageLedgerRecovery.DBRecoveryPartyInfo::class.java.name} WHERE partyId = :partyId", DBTransactionStorageLedgerRecovery.DBRecoveryPartyInfo::class.java) - query.setParameter("partyId", key) + query.setParameter("partyId", key.toString()) return query.resultList.singleOrNull()?.partyName?.let { CordaX500Name.parse(it) } } } \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecovery.kt b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecovery.kt index 31c5dc4a59..aafd27503e 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecovery.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecovery.kt @@ -39,8 +39,8 @@ class DBTransactionStorageLedgerRecovery(private val database: CordaPersistence, @Immutable data class PersistentKey( /** PartyId of flow peer **/ - @Column(name = "peer_party_id", nullable = false) - var peerPartyId: Long, + @Column(name = "peer_party_id", length = 144, nullable = false) + var peerPartyId: String, @Column(name = "timestamp", nullable = false) var timestamp: Instant, @@ -49,7 +49,7 @@ class DBTransactionStorageLedgerRecovery(private val database: CordaPersistence, var timestampDiscriminator: Int ) : Serializable { - constructor(key: Key) : this(key.partyId, key.timestamp, key.timestampDiscriminator) + constructor(key: Key) : this(key.partyId.toString(), key.timestamp, key.timestampDiscriminator) } @CordaSerializable @@ -69,7 +69,7 @@ class DBTransactionStorageLedgerRecovery(private val database: CordaPersistence, fun toSenderDistributionRecord() = SenderDistributionRecord( SecureHash.parse(this.txId), - this.compositeKey.peerPartyId, + SecureHash.parse(this.compositeKey.peerPartyId), this.statesToRecord, this.compositeKey.timestamp ) @@ -104,7 +104,7 @@ class DBTransactionStorageLedgerRecovery(private val database: CordaPersistence, fun toReceiverDistributionRecord(): ReceiverDistributionRecord { return ReceiverDistributionRecord( SecureHash.parse(this.txId), - this.compositeKey.peerPartyId, + SecureHash.parse(this.compositeKey.peerPartyId), OpaqueBytes(this.distributionList), this.compositeKey.timestamp ) @@ -116,8 +116,8 @@ class DBTransactionStorageLedgerRecovery(private val database: CordaPersistence, data class DBRecoveryPartyInfo( @Id /** CordaX500Name hashCode() **/ - @Column(name = "party_id", nullable = false) - var partyId: Long, + @Column(name = "party_id", length = 144, nullable = false) + var partyId: String, /** CordaX500Name of party **/ @Column(name = "party_name", nullable = false) @@ -127,11 +127,11 @@ class DBTransactionStorageLedgerRecovery(private val database: CordaPersistence, data class TimestampKey(val timestamp: Instant, val timestampDiscriminator: Int) class Key( - val partyId: Long, + val partyId: SecureHash, val timestamp: Instant, val timestampDiscriminator: Int = nextDiscriminatorNumber.andIncrement ) { - constructor(key: TimestampKey, partyId: Long): this(partyId, key.timestamp, key.timestampDiscriminator) + constructor(key: TimestampKey, partyId: SecureHash): this(partyId, key.timestamp, key.timestampDiscriminator) companion object { val nextDiscriminatorNumber = AtomicInteger() } @@ -237,7 +237,7 @@ class DBTransactionStorageLedgerRecovery(private val database: CordaPersistence, excludingTxnIds.map { it.toString() })))) } if (peers.isNotEmpty()) { - val peerPartyIds = peers.map { partyInfoCache.getPartyIdByCordaX500Name(it) } + val peerPartyIds = peers.map { partyInfoCache.getPartyIdByCordaX500Name(it).toString() } predicates.add(criteriaBuilder.and(compositeKey.get(PersistentKey::peerPartyId.name).`in`(peerPartyIds))) } criteriaQuery.where(*predicates.toTypedArray()) @@ -275,7 +275,7 @@ class DBTransactionStorageLedgerRecovery(private val database: CordaPersistence, predicates.add(criteriaBuilder.and(criteriaBuilder.not(txId.`in`(excludingTxnIds.map { it.toString() })))) } if (initiators.isNotEmpty()) { - val initiatorPartyIds = initiators.map(partyInfoCache::getPartyIdByCordaX500Name) + val initiatorPartyIds = initiators.map { partyInfoCache.getPartyIdByCordaX500Name(it).toString() } predicates.add(criteriaBuilder.and(compositeKey.get(PersistentKey::peerPartyId.name).`in`(initiatorPartyIds))) } criteriaQuery.where(*predicates.toTypedArray()) @@ -319,7 +319,7 @@ abstract class DistributionRecord { @CordaSerializable data class SenderDistributionRecord( override val txId: SecureHash, - val peerPartyId: Long, // CordaX500Name hashCode() + val peerPartyId: SecureHash, // CordaX500Name hashCode() val statesToRecord: StatesToRecord, override val timestamp: Instant ) : DistributionRecord() @@ -327,7 +327,7 @@ data class SenderDistributionRecord( @CordaSerializable data class ReceiverDistributionRecord( override val txId: SecureHash, - val initiatorPartyId: Long, // CordaX500Name hashCode() + val initiatorPartyId: SecureHash, // CordaX500Name hashCode() val encryptedDistributionList: OpaqueBytes, override val timestamp: Instant ) : DistributionRecord() diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/HashedDistributionList.kt b/node/src/main/kotlin/net/corda/node/services/persistence/HashedDistributionList.kt index 910a00ce74..4f284f11b0 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/HashedDistributionList.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/HashedDistributionList.kt @@ -1,5 +1,6 @@ package net.corda.node.services.persistence +import net.corda.core.crypto.SecureHash import net.corda.core.node.StatesToRecord import net.corda.core.serialization.CordaSerializable import net.corda.node.services.EncryptionService @@ -13,7 +14,7 @@ import java.time.Instant @CordaSerializable data class HashedDistributionList( val senderStatesToRecord: StatesToRecord, - val peerHashToStatesToRecord: Map, + val peerHashToStatesToRecord: Map, val publicHeader: PublicHeader ) { /** @@ -28,7 +29,7 @@ data class HashedDistributionList( out.writeByte(senderStatesToRecord.ordinal) out.writeInt(peerHashToStatesToRecord.size) for (entry in peerHashToStatesToRecord) { - out.writeLong(entry.key) + entry.key.writeTo(out) out.writeByte(entry.value.ordinal) } return encryptionService.encrypt(baos.toByteArray(), publicHeader.serialise()) @@ -78,6 +79,7 @@ data class HashedDistributionList( // The version tag is serialised in the header, even though it is separate from the encrypted main body of the distribution list. // This is because the header and the dist list are cryptographically coupled and we want to avoid declaring the version field twice. private const val VERSION_TAG = 1 + private const val SECURE_HASH_LENGTH = 32 private val statesToRecordValues = StatesToRecord.values() // Cache the enum values since .values() returns a new array each time. /** @@ -91,9 +93,11 @@ data class HashedDistributionList( try { val senderStatesToRecord = statesToRecordValues[input.readByte().toInt()] val numPeerHashToStatesToRecords = input.readInt() - val peerHashToStatesToRecord = mutableMapOf() + val peerHashToStatesToRecord = mutableMapOf() repeat(numPeerHashToStatesToRecords) { - peerHashToStatesToRecord[input.readLong()] = statesToRecordValues[input.readByte().toInt()] + val secureHashBytes = ByteArray(SECURE_HASH_LENGTH) + input.readFully(secureHashBytes) + peerHashToStatesToRecord[SecureHash.createSHA256(secureHashBytes)] = statesToRecordValues[input.readByte().toInt()] } return HashedDistributionList(senderStatesToRecord, peerHashToStatesToRecord, publicHeader) } catch (e: Exception) { diff --git a/node/src/main/resources/migration/node-core.changelog-v25.xml b/node/src/main/resources/migration/node-core.changelog-v25.xml index c720483efa..0e32e3f0c4 100644 --- a/node/src/main/resources/migration/node-core.changelog-v25.xml +++ b/node/src/main/resources/migration/node-core.changelog-v25.xml @@ -21,7 +21,7 @@ - + @@ -46,7 +46,7 @@ - + @@ -65,7 +65,7 @@ - + diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecoveryTests.kt b/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecoveryTests.kt index 3c81be2ab8..debc39d102 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecoveryTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecoveryTests.kt @@ -147,12 +147,12 @@ class DBTransactionStorageLedgerRecoveryTests { val timeWindow = RecoveryTimeWindow(fromTime = now().minus(1, ChronoUnit.DAYS)) transactionRecovery.queryDistributionRecords(timeWindow, recordType = DistributionRecordType.SENDER).let { assertEquals(2, it.size) - assertEquals(BOB_NAME.hashCode().toLong(), it.senderRecords[0].compositeKey.peerPartyId) + assertEquals(SecureHash.sha256(BOB_NAME.toString()).toString(), it.senderRecords[0].compositeKey.peerPartyId) assertEquals(ALL_VISIBLE, it.senderRecords[0].statesToRecord) } transactionRecovery.queryDistributionRecords(timeWindow, recordType = DistributionRecordType.RECEIVER).let { assertEquals(1, it.size) - assertEquals(BOB_NAME.hashCode().toLong(), it.receiverRecords[0].compositeKey.peerPartyId) + assertEquals(SecureHash.sha256(BOB_NAME.toString()).toString(), it.receiverRecords[0].compositeKey.peerPartyId) assertEquals(ALL_VISIBLE, (HashedDistributionList.decrypt(it.receiverRecords[0].distributionList, encryptionService)).peerHashToStatesToRecord.map { it.value }[0]) } val resultsAll = transactionRecovery.queryDistributionRecords(timeWindow, recordType = DistributionRecordType.ALL) @@ -319,7 +319,7 @@ class DBTransactionStorageLedgerRecoveryTests { fun `test lightweight serialization and deserialization of hashed distribution list payload`() { val hashedDistList = HashedDistributionList( ALL_VISIBLE, - mapOf(BOB.name.hashCode().toLong() to NONE, CHARLIE_NAME.hashCode().toLong() to ONLY_RELEVANT), + mapOf(SecureHash.sha256(BOB.name.toString()) to NONE, SecureHash.sha256(CHARLIE_NAME.toString()) to ONLY_RELEVANT), HashedDistributionList.PublicHeader(now()) ) val roundtrip = HashedDistributionList.decrypt(hashedDistList.encrypt(encryptionService), encryptionService) From b7e484c3d8824503831c483e14b26595791f505c Mon Sep 17 00:00:00 2001 From: Adel El-Beik Date: Fri, 29 Sep 2023 16:14:10 +0100 Subject: [PATCH 080/126] ENT-9943: Missed test that had assumed party id was still a long, now updated to string. --- .../net/corda/node/flows/FinalityFlowErrorHandlingTest.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/node/src/integration-test-slow/kotlin/net/corda/node/flows/FinalityFlowErrorHandlingTest.kt b/node/src/integration-test-slow/kotlin/net/corda/node/flows/FinalityFlowErrorHandlingTest.kt index 2ea07f4c7a..dc0133f575 100644 --- a/node/src/integration-test-slow/kotlin/net/corda/node/flows/FinalityFlowErrorHandlingTest.kt +++ b/node/src/integration-test-slow/kotlin/net/corda/node/flows/FinalityFlowErrorHandlingTest.kt @@ -74,7 +74,7 @@ class FinalityFlowErrorHandlingTest : StateMachineErrorHandlingTest() { alice.rpc.startFlow(::GetFlowTransaction, txId).returnValue.getOrThrow().apply { assertEquals("V", this.first) // "V" -> VERIFIED - assertEquals(CHARLIE_NAME.hashCode().toLong(), this.second) // peer + assertEquals(SecureHash.sha256(CHARLIE_NAME.toString()).toString(), this.second) // peer } } } @@ -83,9 +83,9 @@ class FinalityFlowErrorHandlingTest : StateMachineErrorHandlingTest() { // Internal use for testing only!! @StartableByRPC -class GetFlowTransaction(private val txId: SecureHash) : FlowLogic>() { +class GetFlowTransaction(private val txId: SecureHash) : FlowLogic>() { @Suspendable - override fun call(): Pair { + override fun call(): Pair { val transactionStatus = serviceHub.jdbcSession().prepareStatement("select * from node_transactions where tx_id = ?") .apply { setString(1, txId.toString()) } .use { ps -> @@ -99,7 +99,7 @@ class GetFlowTransaction(private val txId: SecureHash) : FlowLogic ps.executeQuery().use { rs -> rs.next() - rs.getLong(4) // receiverPartyId + rs.getString(4) // receiverPartyId } } return Pair(transactionStatus, receiverPartyId) From 4e355b953ba8194bfa6978ed56bc6474b23c0ca0 Mon Sep 17 00:00:00 2001 From: Jose Coll Date: Mon, 2 Oct 2023 17:28:44 +0100 Subject: [PATCH 081/126] ENT-10110 timeDiscriminator must also be shared for DR synchronisation. (#7517) --- .../persistence/DBTransactionStorageLedgerRecovery.kt | 4 ++-- .../node/services/persistence/HashedDistributionList.kt | 9 ++++++--- .../DBTransactionStorageLedgerRecoveryTests.kt | 2 +- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecovery.kt b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecovery.kt index aafd27503e..a85084b288 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecovery.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecovery.kt @@ -155,7 +155,7 @@ class DBTransactionStorageLedgerRecovery(private val database: CordaPersistence, val hashedDistributionList = HashedDistributionList( distributionList.senderStatesToRecord, hashedPeersToStatesToRecord, - HashedDistributionList.PublicHeader(senderRecordingTimestamp) + HashedDistributionList.PublicHeader(senderRecordingTimestamp, timeDiscriminator) ) hashedDistributionList.encrypt(encryptionService) } @@ -170,7 +170,7 @@ class DBTransactionStorageLedgerRecovery(private val database: CordaPersistence, val publicHeader = HashedDistributionList.PublicHeader.unauthenticatedDeserialise(distributionList.opaqueData, encryptionService) database.transaction { val receiverDistributionRecord = DBReceiverDistributionRecord( - Key(partyInfoCache.getPartyIdByCordaX500Name(sender), publicHeader.senderRecordedTimestamp), + Key(partyInfoCache.getPartyIdByCordaX500Name(sender), publicHeader.senderRecordedTimestamp, publicHeader.timeDiscriminator), txId, distributionList.opaqueData, distributionList.receiverStatesToRecord diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/HashedDistributionList.kt b/node/src/main/kotlin/net/corda/node/services/persistence/HashedDistributionList.kt index 4f284f11b0..5fee0f24b2 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/HashedDistributionList.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/HashedDistributionList.kt @@ -38,12 +38,14 @@ data class HashedDistributionList( @CordaSerializable data class PublicHeader( - val senderRecordedTimestamp: Instant + val senderRecordedTimestamp: Instant, + val timeDiscriminator: Int ) { fun serialise(): ByteArray { - val buffer = ByteBuffer.allocate(1 + java.lang.Long.BYTES) + val buffer = ByteBuffer.allocate(1 + java.lang.Long.BYTES + Integer.BYTES) buffer.put(VERSION_TAG.toByte()) buffer.putLong(senderRecordedTimestamp.toEpochMilli()) + buffer.putInt(timeDiscriminator) return buffer.array() } @@ -67,7 +69,8 @@ data class HashedDistributionList( val version = buffer.get().toInt() require(version == VERSION_TAG) { "Unknown distribution list format $version" } val senderRecordedTimestamp = Instant.ofEpochMilli(buffer.getLong()) - return PublicHeader(senderRecordedTimestamp) + val timeDiscriminator = buffer.getInt() + return PublicHeader(senderRecordedTimestamp, timeDiscriminator) } catch (e: Exception) { throw IllegalArgumentException("Corrupt or not a distribution list header", e) } diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecoveryTests.kt b/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecoveryTests.kt index debc39d102..85e086ad61 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecoveryTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecoveryTests.kt @@ -320,7 +320,7 @@ class DBTransactionStorageLedgerRecoveryTests { val hashedDistList = HashedDistributionList( ALL_VISIBLE, mapOf(SecureHash.sha256(BOB.name.toString()) to NONE, SecureHash.sha256(CHARLIE_NAME.toString()) to ONLY_RELEVANT), - HashedDistributionList.PublicHeader(now()) + HashedDistributionList.PublicHeader(now(), 1) ) val roundtrip = HashedDistributionList.decrypt(hashedDistList.encrypt(encryptionService), encryptionService) assertThat(roundtrip).isEqualTo(hashedDistList) From b15ca0f394ad047a6b45a2d4204161307012e195 Mon Sep 17 00:00:00 2001 From: Jose Coll Date: Mon, 2 Oct 2023 17:29:48 +0100 Subject: [PATCH 082/126] ENT-10110 LedgerRecovery parameters + flow return type change. (#7516) --- .../net/corda/core/flows/LedgerRecoverFlow.kt | 44 +++++++------------ 1 file changed, 17 insertions(+), 27 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/flows/LedgerRecoverFlow.kt b/core/src/main/kotlin/net/corda/core/flows/LedgerRecoverFlow.kt index 41a869e2ff..b8cd728224 100644 --- a/core/src/main/kotlin/net/corda/core/flows/LedgerRecoverFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/LedgerRecoverFlow.kt @@ -14,36 +14,17 @@ import net.corda.core.utilities.ProgressTracker @StartableByRPC @InitiatingFlow class LedgerRecoveryFlow( - private val recoveryPeers: Collection, - private val timeWindow: RecoveryTimeWindow? = null, - private val useAllNetworkNodes: Boolean = false, - private val transactionRole: TransactionRole = TransactionRole.ALL, - private val dryRun: Boolean = false, - private val optimisticInitiatorRecovery: Boolean = false, - override val progressTracker: ProgressTracker = ProgressTracker()) : FlowLogic>() { + private val parameters: LedgerRecoveryParameters, + override val progressTracker: ProgressTracker = ProgressTracker()) : FlowLogic() { @CordaInternal - data class ExtraConstructorArgs(val recoveryPeers: Collection, - val timeWindow: RecoveryTimeWindow? = null, - val useAllNetworkNodes: Boolean, - val transactionRole: TransactionRole, - val dryRun: Boolean, - val optimisticInitiatorRecovery: Boolean) + data class ExtraConstructorArgs(val parameters: LedgerRecoveryParameters) @CordaInternal - fun getExtraConstructorArgs() = ExtraConstructorArgs(recoveryPeers, timeWindow, useAllNetworkNodes, transactionRole, dryRun, optimisticInitiatorRecovery) - - // unused constructors added to facilitate Node Shell command invocation - constructor(recoveryPeer: Party, timeWindow: RecoveryTimeWindow?) : this(setOf(recoveryPeer), timeWindow, false, TransactionRole.ALL, false, false) - constructor(recoveryPeer: Party, timeWindow: RecoveryTimeWindow?, dryRun: Boolean) : this(setOf(recoveryPeer), timeWindow, false, TransactionRole.ALL, dryRun, false) - - constructor(timeWindow: RecoveryTimeWindow?, dryRun: Boolean) : this(emptySet(), timeWindow, false, TransactionRole.ALL, dryRun, false) - constructor(timeWindow: RecoveryTimeWindow?, dryRun: Boolean, optimisticInitiatorRecovery: Boolean) : this(emptySet(), timeWindow, false, TransactionRole.ALL, dryRun, optimisticInitiatorRecovery) - constructor(recoveryPeers: Collection, timeWindow: RecoveryTimeWindow?, dryRun: Boolean) : this(recoveryPeers, timeWindow, false, TransactionRole.ALL, dryRun, false) - constructor(recoveryPeers: Collection, timeWindow: RecoveryTimeWindow?, dryRun: Boolean, optimisticInitiatorRecovery: Boolean) : this(recoveryPeers, timeWindow, false, TransactionRole.ALL, dryRun, optimisticInitiatorRecovery) + fun getExtraConstructorArgs() = ExtraConstructorArgs(parameters) @Suspendable @Throws(LedgerRecoveryException::class) - override fun call(): Map { + override fun call(): Long { throw NotImplementedError("Enterprise only feature") } } @@ -59,6 +40,18 @@ class ReceiveLedgerRecoveryFlow constructor(private val otherSideSession: FlowSe @CordaSerializable class LedgerRecoveryException(message: String) : FlowException("Ledger recovery failed: $message") +data class LedgerRecoveryParameters( + val recoveryPeers: Collection, + val timeWindow: RecoveryTimeWindow? = null, + val useAllNetworkNodes: Boolean = false, + val transactionRole: TransactionRole = TransactionRole.ALL, + val dryRun: Boolean = false, + val optimisticInitiatorRecovery: Boolean = false, + val useTimeWindowNarrowing: Boolean = false, + val verboseLogging: Boolean = true, + val recoveryBatchSize: Int = 1000 +) + /** * This specifies which type of transactions to recover based on the transaction role of the recovering node */ @@ -80,6 +73,3 @@ data class RecoveryResult( val synchronisedInitiated: Boolean = false, // only attempted if [optimisticInitiatorRecovery] option set to true and [TransactionRecoveryType.INITIATOR] val failureCause: String? = null // reason why a transaction failed to synchronise ) - - - From fdf8d5344bd334dfa57087449db95d93ecdb5b93 Mon Sep 17 00:00:00 2001 From: Jose Coll Date: Thu, 5 Oct 2023 16:53:20 +0100 Subject: [PATCH 083/126] ENT-10110 Ledger Recovery tweaks (#7519) --- .../net/corda/core/flows/LedgerRecoverFlow.kt | 14 +++++++++++--- .../net/corda/core/flows/SendTransactionFlow.kt | 13 ++++++++----- .../corda/core/internal/ResolveTransactionsFlow.kt | 3 +++ .../DBTransactionStorageLedgerRecovery.kt | 2 +- 4 files changed, 23 insertions(+), 9 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/flows/LedgerRecoverFlow.kt b/core/src/main/kotlin/net/corda/core/flows/LedgerRecoverFlow.kt index b8cd728224..cf91603133 100644 --- a/core/src/main/kotlin/net/corda/core/flows/LedgerRecoverFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/LedgerRecoverFlow.kt @@ -15,7 +15,7 @@ import net.corda.core.utilities.ProgressTracker @InitiatingFlow class LedgerRecoveryFlow( private val parameters: LedgerRecoveryParameters, - override val progressTracker: ProgressTracker = ProgressTracker()) : FlowLogic() { + override val progressTracker: ProgressTracker = ProgressTracker()) : FlowLogic() { @CordaInternal data class ExtraConstructorArgs(val parameters: LedgerRecoveryParameters) @@ -24,7 +24,7 @@ class LedgerRecoveryFlow( @Suspendable @Throws(LedgerRecoveryException::class) - override fun call(): Long { + override fun call(): LedgerRecoveryResult { throw NotImplementedError("Enterprise only feature") } } @@ -40,6 +40,7 @@ class ReceiveLedgerRecoveryFlow constructor(private val otherSideSession: FlowSe @CordaSerializable class LedgerRecoveryException(message: String) : FlowException("Ledger recovery failed: $message") +@CordaSerializable data class LedgerRecoveryParameters( val recoveryPeers: Collection, val timeWindow: RecoveryTimeWindow? = null, @@ -47,11 +48,18 @@ data class LedgerRecoveryParameters( val transactionRole: TransactionRole = TransactionRole.ALL, val dryRun: Boolean = false, val optimisticInitiatorRecovery: Boolean = false, - val useTimeWindowNarrowing: Boolean = false, + val useTimeWindowNarrowing: Boolean = true, val verboseLogging: Boolean = true, val recoveryBatchSize: Int = 1000 ) +@CordaSerializable +data class LedgerRecoveryResult( + val totalRecoveredRecords: Long, + val totalRecoveredTransactions: Long, + val totalErrors: Long +) + /** * This specifies which type of transactions to recover based on the transaction role of the recovering node */ diff --git a/core/src/main/kotlin/net/corda/core/flows/SendTransactionFlow.kt b/core/src/main/kotlin/net/corda/core/flows/SendTransactionFlow.kt index 3323d2d743..d10bbe0ebb 100644 --- a/core/src/main/kotlin/net/corda/core/flows/SendTransactionFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/SendTransactionFlow.kt @@ -132,7 +132,7 @@ open class DataVendingFlow(val otherSessions: Set, val payload: Any protected open fun isFinality(): Boolean = false - @Suppress("ComplexCondition", "ComplexMethod", "LongMethod") + @Suppress("ComplexCondition", "ComplexMethod", "LongMethod", "TooGenericExceptionThrown") @Suspendable override fun call(): Void? { val networkMaxMessageSize = serviceHub.networkParameters.maxMessageSize @@ -151,11 +151,14 @@ open class DataVendingFlow(val otherSessions: Set, val payload: Any is NotarisationPayload -> TransactionAuthorisationFilter().addAuthorised(getInputTransactions(payload.signedTransaction)) is SignedTransaction -> TransactionAuthorisationFilter().addAuthorised(getInputTransactions(payload)) is RetrieveAnyTransactionPayload -> TransactionAuthorisationFilter(acceptAll = true) - is List<*> -> TransactionAuthorisationFilter().addAuthorised(payload.flatMap { stateAndRef -> - if (stateAndRef is StateAndRef<*>) { - getInputTransactions(serviceHub.validatedTransactions.getTransaction(stateAndRef.ref.txhash)!!) + stateAndRef.ref.txhash + is List<*> -> TransactionAuthorisationFilter().addAuthorised(payload.flatMap { someObject -> + if (someObject is StateAndRef<*>) { + getInputTransactions(serviceHub.validatedTransactions.getTransaction(someObject.ref.txhash)!!) + someObject.ref.txhash + } + else if (someObject is NamedByHash) { + setOf(someObject.id) } else { - throw Exception("Unknown payload type: ${stateAndRef!!::class.java} ?") + throw Exception("Unknown payload type: ${someObject!!::class.java} ?") } }.toSet()) else -> throw Exception("Unknown payload type: ${payload::class.java} ?") diff --git a/core/src/main/kotlin/net/corda/core/internal/ResolveTransactionsFlow.kt b/core/src/main/kotlin/net/corda/core/internal/ResolveTransactionsFlow.kt index 882ca901fe..053015fef9 100644 --- a/core/src/main/kotlin/net/corda/core/internal/ResolveTransactionsFlow.kt +++ b/core/src/main/kotlin/net/corda/core/internal/ResolveTransactionsFlow.kt @@ -26,6 +26,9 @@ class ResolveTransactionsFlow private constructor( constructor(txHashes: Set, otherSide: FlowSession, statesToRecord: StatesToRecord = StatesToRecord.NONE) : this(null, txHashes, otherSide, statesToRecord) + constructor(txHashes: Set, otherSide: FlowSession, statesToRecord: StatesToRecord, deferredAck: Boolean) + : this(null, txHashes, otherSide, statesToRecord, deferredAck) + /** * Resolves and validates the dependencies of the specified [SignedTransaction]. Fetches the attachments, but does * *not* validate or store the [SignedTransaction] itself. diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecovery.kt b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecovery.kt index a85084b288..84ea4869ca 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecovery.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecovery.kt @@ -78,7 +78,7 @@ class DBTransactionStorageLedgerRecovery(private val database: CordaPersistence, @CordaSerializable @Entity @Table(name = "${NODE_DATABASE_PREFIX}receiver_distr_recs") - class DBReceiverDistributionRecord( + data class DBReceiverDistributionRecord( @EmbeddedId var compositeKey: PersistentKey, From a66042ec097f61791eb567067d6d98be948c5b26 Mon Sep 17 00:00:00 2001 From: Jose Coll Date: Tue, 10 Oct 2023 17:41:49 +0100 Subject: [PATCH 084/126] ENT-10110 Ledger Recovery database table tweaks (#7528) --- .../coretests/flows/FinalityFlowTests.kt | 16 ++--- .../DBTransactionStorageLedgerRecovery.kt | 58 ++++++++++--------- .../migration/node-core.changelog-v25.xml | 5 +- ...DBTransactionStorageLedgerRecoveryTests.kt | 21 +++---- 4 files changed, 53 insertions(+), 47 deletions(-) diff --git a/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt b/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt index bfbb93c96b..0c2c265efb 100644 --- a/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt +++ b/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt @@ -123,7 +123,6 @@ class FinalityFlowTests : WithFinality { fun `allow use of the old API if the CorDapp target version is 3`() { val oldBob = createBob(cordapps = listOf(tokenOldCordapp())) val stx = aliceNode.issuesCashTo(oldBob) - @Suppress("DEPRECATION") aliceNode.startFlowAndRunNetwork(OldFinalityInvoker(stx)).resultFuture.getOrThrow() assertThat(oldBob.services.validatedTransactions.getTransaction(stx.id)).isNotNull } @@ -239,7 +238,7 @@ class FinalityFlowTests : WithFinality { private fun assertTxnRemovedFromDatabase(node: TestStartedNode, stxId: SecureHash) { val fromDb = node.database.transaction { session.createQuery( - "from ${DBTransactionStorage.DBTransaction::class.java.name} where txId = :transactionId", + "from ${DBTransactionStorage.DBTransaction::class.java.name} where tx_id = :transactionId", DBTransactionStorage.DBTransaction::class.java ).setParameter("transactionId", stxId.toString()).resultList } @@ -354,7 +353,8 @@ class FinalityFlowTests : WithFinality { val sdrs = getSenderRecoveryData(stx.id, aliceNode.database).apply { assertEquals(1, this.size) - assertEquals(StatesToRecord.ALL_VISIBLE, this[0].statesToRecord) + assertEquals(StatesToRecord.ONLY_RELEVANT, this[0].senderStatesToRecord) + assertEquals(StatesToRecord.ALL_VISIBLE, this[0].receiverStatesToRecord) assertEquals(SecureHash.sha256(BOB_NAME.toString()), this[0].peerPartyId) } val rdr = getReceiverRecoveryData(stx.id, bobNode).apply { @@ -387,9 +387,9 @@ class FinalityFlowTests : WithFinality { val sdrs = getSenderRecoveryData(stx.id, aliceNode.database).apply { assertEquals(2, this.size) - assertEquals(StatesToRecord.ONLY_RELEVANT, this[0].statesToRecord) + assertEquals(StatesToRecord.ONLY_RELEVANT, this[0].senderStatesToRecord) assertEquals(SecureHash.sha256(BOB_NAME.toString()), this[0].peerPartyId) - assertEquals(StatesToRecord.ALL_VISIBLE, this[1].statesToRecord) + assertEquals(StatesToRecord.ALL_VISIBLE, this[1].receiverStatesToRecord) assertEquals(SecureHash.sha256(CHARLIE_NAME.toString()), this[1].peerPartyId) } val rdr = getReceiverRecoveryData(stx.id, bobNode).apply { @@ -451,7 +451,7 @@ class FinalityFlowTests : WithFinality { val sdr = getSenderRecoveryData(stx.id, aliceNode.database).apply { assertEquals(1, this.size) - assertEquals(StatesToRecord.ONLY_RELEVANT, this[0].statesToRecord) + assertEquals(StatesToRecord.ONLY_RELEVANT, this[0].senderStatesToRecord) assertEquals(SecureHash.sha256(BOB_NAME.toString()), this[0].peerPartyId) } val rdr = getReceiverRecoveryData(stx.id, bobNode).apply { @@ -467,7 +467,7 @@ class FinalityFlowTests : WithFinality { private fun getSenderRecoveryData(id: SecureHash, database: CordaPersistence): List { val fromDb = database.transaction { session.createQuery( - "from ${DBSenderDistributionRecord::class.java.name} where txId = :transactionId", + "from ${DBSenderDistributionRecord::class.java.name} where transaction_id = :transactionId", DBSenderDistributionRecord::class.java ).setParameter("transactionId", id.toString()).resultList } @@ -477,7 +477,7 @@ class FinalityFlowTests : WithFinality { private fun getReceiverRecoveryData(txId: SecureHash, receiver: TestStartedNode): ReceiverDistributionRecord? { return receiver.database.transaction { session.createQuery( - "from ${DBReceiverDistributionRecord::class.java.name} where txId = :transactionId", + "from ${DBReceiverDistributionRecord::class.java.name} where transaction_id = :transactionId", DBReceiverDistributionRecord::class.java ).setParameter("transactionId", txId.toString()).resultList }.singleOrNull()?.toReceiverDistributionRecord() diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecovery.kt b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecovery.kt index 84ea4869ca..84482acbd6 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecovery.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecovery.kt @@ -38,7 +38,9 @@ class DBTransactionStorageLedgerRecovery(private val database: CordaPersistence, @Embeddable @Immutable data class PersistentKey( - /** PartyId of flow peer **/ + @Column(name = "transaction_id", length = 144, nullable = false) + var txId: String, + @Column(name = "peer_party_id", length = 144, nullable = false) var peerPartyId: String, @@ -49,7 +51,7 @@ class DBTransactionStorageLedgerRecovery(private val database: CordaPersistence, var timestampDiscriminator: Int ) : Serializable { - constructor(key: Key) : this(key.partyId.toString(), key.timestamp, key.timestampDiscriminator) + constructor(key: Key) : this(key.txId.toString(), key.partyId.toString(), key.timestamp, key.timestampDiscriminator) } @CordaSerializable @@ -59,18 +61,20 @@ class DBTransactionStorageLedgerRecovery(private val database: CordaPersistence, @EmbeddedId var compositeKey: PersistentKey, - @Column(name = "transaction_id", length = 144, nullable = false) - var txId: String, + /** states to record: NONE, ALL_VISIBLE, ONLY_RELEVANT */ + @Column(name = "sender_states_to_record", nullable = false) + var senderStatesToRecord: StatesToRecord, /** states to record: NONE, ALL_VISIBLE, ONLY_RELEVANT */ - @Column(name = "states_to_record", nullable = false) - var statesToRecord: StatesToRecord + @Column(name = "receiver_states_to_record", nullable = false) + var receiverStatesToRecord: StatesToRecord ) { fun toSenderDistributionRecord() = SenderDistributionRecord( - SecureHash.parse(this.txId), + SecureHash.parse(this.compositeKey.txId), SecureHash.parse(this.compositeKey.peerPartyId), - this.statesToRecord, + this.senderStatesToRecord, + this.receiverStatesToRecord, this.compositeKey.timestamp ) } @@ -82,9 +86,6 @@ class DBTransactionStorageLedgerRecovery(private val database: CordaPersistence, @EmbeddedId var compositeKey: PersistentKey, - @Column(name = "transaction_id", length = 144, nullable = false) - var txId: String, - /** Encrypted recovery information for sole use by Sender **/ @Lob @Column(name = "distribution_list", nullable = false) @@ -94,16 +95,15 @@ class DBTransactionStorageLedgerRecovery(private val database: CordaPersistence, @Column(name = "receiver_states_to_record", nullable = false) val receiverStatesToRecord: StatesToRecord ) { - constructor(key: Key, txId: SecureHash, encryptedDistributionList: ByteArray, receiverStatesToRecord: StatesToRecord) : + constructor(key: Key, encryptedDistributionList: ByteArray, receiverStatesToRecord: StatesToRecord) : this(PersistentKey(key), - txId = txId.toString(), distributionList = encryptedDistributionList, receiverStatesToRecord = receiverStatesToRecord ) @VisibleForTesting fun toReceiverDistributionRecord(): ReceiverDistributionRecord { return ReceiverDistributionRecord( - SecureHash.parse(this.txId), + SecureHash.parse(this.compositeKey.txId), SecureHash.parse(this.compositeKey.peerPartyId), OpaqueBytes(this.distributionList), this.compositeKey.timestamp @@ -124,14 +124,12 @@ class DBTransactionStorageLedgerRecovery(private val database: CordaPersistence, val partyName: String ) - data class TimestampKey(val timestamp: Instant, val timestampDiscriminator: Int) - class Key( + val txId: SecureHash, val partyId: SecureHash, val timestamp: Instant, val timestampDiscriminator: Int = nextDiscriminatorNumber.andIncrement ) { - constructor(key: TimestampKey, partyId: SecureHash): this(partyId, key.timestamp, key.timestampDiscriminator) companion object { val nextDiscriminatorNumber = AtomicInteger() } @@ -144,8 +142,10 @@ class DBTransactionStorageLedgerRecovery(private val database: CordaPersistence, val distributionList = metadata.distributionList as? SenderDistributionList ?: throw IllegalStateException("Expecting SenderDistributionList") distributionList.peersToStatesToRecord.map { (peerCordaX500Name, peerStatesToRecord) -> val senderDistributionRecord = DBSenderDistributionRecord( - PersistentKey(Key(TimestampKey(senderRecordingTimestamp, timeDiscriminator), partyInfoCache.getPartyIdByCordaX500Name(peerCordaX500Name))), - txId.toString(), + PersistentKey(Key(txId, + partyInfoCache.getPartyIdByCordaX500Name(peerCordaX500Name), + senderRecordingTimestamp, timeDiscriminator)), + distributionList.senderStatesToRecord, peerStatesToRecord) session.save(senderDistributionRecord) } @@ -170,8 +170,7 @@ class DBTransactionStorageLedgerRecovery(private val database: CordaPersistence, val publicHeader = HashedDistributionList.PublicHeader.unauthenticatedDeserialise(distributionList.opaqueData, encryptionService) database.transaction { val receiverDistributionRecord = DBReceiverDistributionRecord( - Key(partyInfoCache.getPartyIdByCordaX500Name(sender), publicHeader.senderRecordedTimestamp, publicHeader.timeDiscriminator), - txId, + Key(txId, partyInfoCache.getPartyIdByCordaX500Name(sender), publicHeader.senderRecordedTimestamp, publicHeader.timeDiscriminator), distributionList.opaqueData, distributionList.receiverStatesToRecord ) @@ -187,12 +186,14 @@ class DBTransactionStorageLedgerRecovery(private val database: CordaPersistence, super.removeUnnotarisedTransaction(id) val criteriaBuilder = session.criteriaBuilder val deleteSenderDistributionRecords = criteriaBuilder.createCriteriaDelete(DBSenderDistributionRecord::class.java) - val root = deleteSenderDistributionRecords.from(DBSenderDistributionRecord::class.java) - deleteSenderDistributionRecords.where(criteriaBuilder.equal(root.get(DBSenderDistributionRecord::txId.name), id.toString())) + val rootSender = deleteSenderDistributionRecords.from(DBSenderDistributionRecord::class.java) + val compositeKeySender = rootSender.get("compositeKey") + deleteSenderDistributionRecords.where(criteriaBuilder.equal(compositeKeySender.get(PersistentKey::txId.name), id.toString())) val deletedSenderDistributionRecords = session.createQuery(deleteSenderDistributionRecords).executeUpdate() != 0 val deleteReceiverDistributionRecords = criteriaBuilder.createCriteriaDelete(DBReceiverDistributionRecord::class.java) - val rootReceiverDistributionRecord = deleteReceiverDistributionRecords.from(DBReceiverDistributionRecord::class.java) - deleteReceiverDistributionRecords.where(criteriaBuilder.equal(rootReceiverDistributionRecord.get(DBReceiverDistributionRecord::txId.name), id.toString())) + val rootReceiver = deleteReceiverDistributionRecords.from(DBReceiverDistributionRecord::class.java) + val compositeKeyReceiver = rootReceiver.get("compositeKey") + deleteReceiverDistributionRecords.where(criteriaBuilder.equal(compositeKeyReceiver.get(PersistentKey::txId.name), id.toString())) val deletedReceiverDistributionRecords = session.createQuery(deleteReceiverDistributionRecords).executeUpdate() != 0 deletedSenderDistributionRecords || deletedReceiverDistributionRecords } @@ -233,7 +234,7 @@ class DBTransactionStorageLedgerRecovery(private val database: CordaPersistence, predicates.add(criteriaBuilder.greaterThanOrEqualTo(compositeKey.get(PersistentKey::timestamp.name), timeWindow.fromTime)) predicates.add(criteriaBuilder.and(criteriaBuilder.lessThanOrEqualTo(compositeKey.get(PersistentKey::timestamp.name), timeWindow.untilTime))) if (excludingTxnIds.isNotEmpty()) { - predicates.add(criteriaBuilder.and(criteriaBuilder.not(txnMetadata.get(DBSenderDistributionRecord::txId.name).`in`( + predicates.add(criteriaBuilder.and(criteriaBuilder.not(compositeKey.get(PersistentKey::txId.name).`in`( excludingTxnIds.map { it.toString() })))) } if (peers.isNotEmpty()) { @@ -271,7 +272,7 @@ class DBTransactionStorageLedgerRecovery(private val database: CordaPersistence, predicates.add(criteriaBuilder.greaterThanOrEqualTo(timestamp, timeWindow.fromTime)) predicates.add(criteriaBuilder.and(criteriaBuilder.lessThanOrEqualTo(timestamp, timeWindow.untilTime))) if (excludingTxnIds.isNotEmpty()) { - val txId = txnMetadata.get(DBSenderDistributionRecord::txId.name) + val txId = compositeKey.get(PersistentKey::txId.name) predicates.add(criteriaBuilder.and(criteriaBuilder.not(txId.`in`(excludingTxnIds.map { it.toString() })))) } if (initiators.isNotEmpty()) { @@ -320,7 +321,8 @@ abstract class DistributionRecord { data class SenderDistributionRecord( override val txId: SecureHash, val peerPartyId: SecureHash, // CordaX500Name hashCode() - val statesToRecord: StatesToRecord, + val senderStatesToRecord: StatesToRecord, + val receiverStatesToRecord: StatesToRecord, override val timestamp: Instant ) : DistributionRecord() diff --git a/node/src/main/resources/migration/node-core.changelog-v25.xml b/node/src/main/resources/migration/node-core.changelog-v25.xml index 0e32e3f0c4..bc31223fe7 100644 --- a/node/src/main/resources/migration/node-core.changelog-v25.xml +++ b/node/src/main/resources/migration/node-core.changelog-v25.xml @@ -24,7 +24,10 @@ - + + + + diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecoveryTests.kt b/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecoveryTests.kt index 85e086ad61..60a48a073c 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecoveryTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecoveryTests.kt @@ -114,7 +114,7 @@ class DBTransactionStorageLedgerRecoveryTests { val timeWindow = RecoveryTimeWindow(fromTime = now().minus(1, ChronoUnit.DAYS)) val results = transactionRecovery.querySenderDistributionRecords(timeWindow, excludingTxnIds = setOf(transaction1.id)) assertEquals(1, results.size) - assertEquals(transaction2.id.toString(), results[0].txId) + assertEquals(transaction2.id.toString(), results[0].compositeKey.txId) } @Test(timeout = 300_000) @@ -128,7 +128,7 @@ class DBTransactionStorageLedgerRecoveryTests { val timeWindow = RecoveryTimeWindow(fromTime = now().minus(1, ChronoUnit.DAYS)) val results = transactionRecovery.querySenderDistributionRecords(timeWindow, peers = setOf(CHARLIE_NAME)) assertEquals(1, results.size) - assertEquals(transaction2.id.toString(), results[0].txId) + assertEquals(transaction2.id.toString(), results[0].compositeKey.txId) } @Test(timeout = 300_000) @@ -148,7 +148,7 @@ class DBTransactionStorageLedgerRecoveryTests { transactionRecovery.queryDistributionRecords(timeWindow, recordType = DistributionRecordType.SENDER).let { assertEquals(2, it.size) assertEquals(SecureHash.sha256(BOB_NAME.toString()).toString(), it.senderRecords[0].compositeKey.peerPartyId) - assertEquals(ALL_VISIBLE, it.senderRecords[0].statesToRecord) + assertEquals(ALL_VISIBLE, it.senderRecords[0].senderStatesToRecord) } transactionRecovery.queryDistributionRecords(timeWindow, recordType = DistributionRecordType.RECEIVER).let { assertEquals(1, it.size) @@ -181,8 +181,8 @@ class DBTransactionStorageLedgerRecoveryTests { val timeWindow = RecoveryTimeWindow(fromTime = now().minus(1, ChronoUnit.DAYS)) transactionRecovery.querySenderDistributionRecords(timeWindow, peers = setOf(BOB_NAME)).let { assertEquals(2, it.size) - assertEquals(it[0].statesToRecord, ALL_VISIBLE) - assertEquals(it[1].statesToRecord, ONLY_RELEVANT) + assertEquals(it[0].senderStatesToRecord, ALL_VISIBLE) + assertEquals(it[1].senderStatesToRecord, ONLY_RELEVANT) } assertEquals(1, transactionRecovery.querySenderDistributionRecords(timeWindow, peers = setOf(ALICE_NAME)).size) assertEquals(2, transactionRecovery.querySenderDistributionRecords(timeWindow, peers = setOf(CHARLIE_NAME)).size) @@ -251,7 +251,7 @@ class DBTransactionStorageLedgerRecoveryTests { assertEquals(IN_FLIGHT, readTransactionFromDB(senderTransaction.id).status) readSenderDistributionRecordFromDB(senderTransaction.id).let { assertEquals(1, it.size) - assertEquals(ALL_VISIBLE, it[0].statesToRecord) + assertEquals(ALL_VISIBLE, it[0].senderStatesToRecord) assertEquals(BOB_NAME, partyInfoCache.getCordaX500NameByPartyId(it[0].peerPartyId)) } @@ -280,7 +280,8 @@ class DBTransactionStorageLedgerRecoveryTests { assertEquals(VERIFIED, readTransactionFromDB(transaction.id).status) readSenderDistributionRecordFromDB(transaction.id).apply { assertEquals(1, this.size) - assertEquals(ALL_VISIBLE, this[0].statesToRecord) + assertEquals(ONLY_RELEVANT, this[0].senderStatesToRecord) + assertEquals(ALL_VISIBLE, this[0].receiverStatesToRecord) } } @@ -329,7 +330,7 @@ class DBTransactionStorageLedgerRecoveryTests { private fun readTransactionFromDB(txId: SecureHash): DBTransactionStorage.DBTransaction { val fromDb = database.transaction { session.createQuery( - "from ${DBTransactionStorage.DBTransaction::class.java.name} where txId = :transactionId", + "from ${DBTransactionStorage.DBTransaction::class.java.name} where tx_id = :transactionId", DBTransactionStorage.DBTransaction::class.java ).setParameter("transactionId", txId.toString()).resultList } @@ -341,7 +342,7 @@ class DBTransactionStorageLedgerRecoveryTests { return database.transaction { if (txId != null) session.createQuery( - "from ${DBSenderDistributionRecord::class.java.name} where txId = :transactionId", + "from ${DBSenderDistributionRecord::class.java.name} where transaction_id = :transactionId", DBSenderDistributionRecord::class.java ).setParameter("transactionId", txId.toString()).resultList.map { it.toSenderDistributionRecord() } else @@ -355,7 +356,7 @@ class DBTransactionStorageLedgerRecoveryTests { private fun readReceiverDistributionRecordFromDB(txId: SecureHash): ReceiverDistributionRecord { val fromDb = database.transaction { session.createQuery( - "from ${DBReceiverDistributionRecord::class.java.name} where txId = :transactionId", + "from ${DBReceiverDistributionRecord::class.java.name} where transaction_id = :transactionId", DBReceiverDistributionRecord::class.java ).setParameter("transactionId", txId.toString()).resultList } From 198133492185f019b2f79dc22ab751b568bf6146 Mon Sep 17 00:00:00 2001 From: Jose Coll Date: Fri, 13 Oct 2023 11:26:07 +0100 Subject: [PATCH 085/126] ENT-10110 Clean-up. (#7530) --- .../net/corda/core/flows/LedgerRecoverFlow.kt | 35 ------------------- 1 file changed, 35 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/flows/LedgerRecoverFlow.kt b/core/src/main/kotlin/net/corda/core/flows/LedgerRecoverFlow.kt index cf91603133..32c0ff3866 100644 --- a/core/src/main/kotlin/net/corda/core/flows/LedgerRecoverFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/LedgerRecoverFlow.kt @@ -2,8 +2,6 @@ package net.corda.core.flows import co.paralleluniverse.fibers.Suspendable import net.corda.core.CordaInternal -import net.corda.core.crypto.SecureHash -import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party import net.corda.core.serialization.CordaSerializable import net.corda.core.utilities.ProgressTracker @@ -12,7 +10,6 @@ import net.corda.core.utilities.ProgressTracker * Ledger Recovery Flow (available in Enterprise only). */ @StartableByRPC -@InitiatingFlow class LedgerRecoveryFlow( private val parameters: LedgerRecoveryParameters, override val progressTracker: ProgressTracker = ProgressTracker()) : FlowLogic() { @@ -29,14 +26,6 @@ class LedgerRecoveryFlow( } } -@InitiatedBy(LedgerRecoveryFlow::class) -class ReceiveLedgerRecoveryFlow constructor(private val otherSideSession: FlowSession) : FlowLogic() { - @Suspendable - override fun call() { - throw NotImplementedError("Enterprise only feature") - } -} - @CordaSerializable class LedgerRecoveryException(message: String) : FlowException("Ledger recovery failed: $message") @@ -45,9 +34,7 @@ data class LedgerRecoveryParameters( val recoveryPeers: Collection, val timeWindow: RecoveryTimeWindow? = null, val useAllNetworkNodes: Boolean = false, - val transactionRole: TransactionRole = TransactionRole.ALL, val dryRun: Boolean = false, - val optimisticInitiatorRecovery: Boolean = false, val useTimeWindowNarrowing: Boolean = true, val verboseLogging: Boolean = true, val recoveryBatchSize: Int = 1000 @@ -59,25 +46,3 @@ data class LedgerRecoveryResult( val totalRecoveredTransactions: Long, val totalErrors: Long ) - -/** - * This specifies which type of transactions to recover based on the transaction role of the recovering node - */ -@CordaSerializable -enum class TransactionRole { - ALL, - INITIATOR, // only recover transactions that I initiated - PEER, // only recover transactions where I am a participant on a transaction - OBSERVER, // only recover transactions where I am an observer (but not participant) to a transaction - PEER_AND_OBSERVER // recovery transactions where I am either participant or observer -} - -@CordaSerializable -data class RecoveryResult( - val transactionId: SecureHash, - val recoveryPeer: CordaX500Name, - val transactionRole: TransactionRole, // what role did I play in this transaction - val synchronised: Boolean, // whether the transaction was successfully synchronised (will always be false when dryRun option specified) - val synchronisedInitiated: Boolean = false, // only attempted if [optimisticInitiatorRecovery] option set to true and [TransactionRecoveryType.INITIATOR] - val failureCause: String? = null // reason why a transaction failed to synchronise -) From 6a2bad8077811da334f3fc66ba305f20044edafc Mon Sep 17 00:00:00 2001 From: Jose Coll Date: Tue, 17 Oct 2023 07:03:49 +0100 Subject: [PATCH 086/126] ENT-10110 Back-port changes from ENT + additional clean-up (#7532) --- .ci/api-current.txt | 10 ++ .../coretests/flows/FinalityFlowTests.kt | 10 +- .../net/corda/core/flows/FlowTransaction.kt | 78 --------- .../net/corda/core/flows/RecoveryTypes.kt | 150 ++++++++++++++++++ .../DBTransactionStorageLedgerRecovery.kt | 91 ++++------- ...DBTransactionStorageLedgerRecoveryTests.kt | 41 +++-- 6 files changed, 223 insertions(+), 157 deletions(-) delete mode 100644 core/src/main/kotlin/net/corda/core/flows/FlowTransaction.kt create mode 100644 core/src/main/kotlin/net/corda/core/flows/RecoveryTypes.kt diff --git a/.ci/api-current.txt b/.ci/api-current.txt index 1c01aec0a6..a7f5449ff7 100644 --- a/.ci/api-current.txt +++ b/.ci/api-current.txt @@ -2580,6 +2580,16 @@ public static final class net.corda.core.flows.DistributionList$SenderDistributi public int hashCode() @NotNull public String toString() +@CordaSerializable +public abstract class net.corda.core.flows.DistributionRecord extends java.lang.Object implements net.corda.core.contracts.NamedByHash + public () + @NotNull + public abstract net.corda.core.crypto.SecureHash getPeerPartyId() + @NotNull + public abstract java.time.Instant getTimestamp() + public abstract int getTimestampDiscriminator() + @NotNull + public abstract net.corda.core.crypto.SecureHash getTxId() ## @InitiatingFlow public final class net.corda.core.flows.FinalityFlow extends net.corda.core.flows.FlowLogic diff --git a/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt b/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt index 0c2c265efb..8048c99dbd 100644 --- a/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt +++ b/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt @@ -21,7 +21,9 @@ import net.corda.core.flows.NotaryException import net.corda.core.flows.NotarySigCheck import net.corda.core.flows.ReceiveFinalityFlow import net.corda.core.flows.ReceiveTransactionFlow +import net.corda.core.flows.ReceiverDistributionRecord import net.corda.core.flows.SendTransactionFlow +import net.corda.core.flows.SenderDistributionRecord import net.corda.core.flows.StartableByRPC import net.corda.core.flows.TransactionStatus import net.corda.core.flows.UnexpectedFlowEndException @@ -53,8 +55,6 @@ import net.corda.node.services.persistence.DBTransactionStorage import net.corda.node.services.persistence.DBTransactionStorageLedgerRecovery.DBReceiverDistributionRecord import net.corda.node.services.persistence.DBTransactionStorageLedgerRecovery.DBSenderDistributionRecord import net.corda.node.services.persistence.HashedDistributionList -import net.corda.node.services.persistence.ReceiverDistributionRecord -import net.corda.node.services.persistence.SenderDistributionRecord import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.testing.contracts.DummyContract import net.corda.testing.core.ALICE_NAME @@ -361,7 +361,7 @@ class FinalityFlowTests : WithFinality { assertNotNull(this) val hashedDL = HashedDistributionList.decrypt(this!!.encryptedDistributionList.bytes, aliceNode.internals.encryptionService) assertEquals(StatesToRecord.ONLY_RELEVANT, hashedDL.senderStatesToRecord) - assertEquals(SecureHash.sha256(aliceNode.info.singleIdentity().name.toString()), this.initiatorPartyId) + assertEquals(SecureHash.sha256(aliceNode.info.singleIdentity().name.toString()), this.peerPartyId) assertEquals(mapOf(SecureHash.sha256(BOB_NAME.toString()) to StatesToRecord.ALL_VISIBLE), hashedDL.peerHashToStatesToRecord) } validateSenderAndReceiverTimestamps(sdrs, rdr!!) @@ -396,7 +396,7 @@ class FinalityFlowTests : WithFinality { assertNotNull(this) val hashedDL = HashedDistributionList.decrypt(this!!.encryptedDistributionList.bytes, aliceNode.internals.encryptionService) assertEquals(StatesToRecord.ONLY_RELEVANT, hashedDL.senderStatesToRecord) - assertEquals(SecureHash.sha256(aliceNode.info.singleIdentity().name.toString()), this.initiatorPartyId) + assertEquals(SecureHash.sha256(aliceNode.info.singleIdentity().name.toString()), this.peerPartyId) // note: Charlie assertion here is using the hinted StatesToRecord value passed to it from Alice assertEquals(mapOf( SecureHash.sha256(BOB_NAME.toString()) to StatesToRecord.ONLY_RELEVANT, @@ -458,7 +458,7 @@ class FinalityFlowTests : WithFinality { assertNotNull(this) val hashedDL = HashedDistributionList.decrypt(this!!.encryptedDistributionList.bytes, aliceNode.internals.encryptionService) assertEquals(StatesToRecord.ONLY_RELEVANT, hashedDL.senderStatesToRecord) - assertEquals(SecureHash.sha256(aliceNode.info.singleIdentity().name.toString()), this.initiatorPartyId) + assertEquals(SecureHash.sha256(aliceNode.info.singleIdentity().name.toString()), this.peerPartyId) assertEquals(mapOf(SecureHash.sha256(BOB_NAME.toString()) to StatesToRecord.ONLY_RELEVANT), hashedDL.peerHashToStatesToRecord) } validateSenderAndReceiverTimestamps(sdr, rdr!!) diff --git a/core/src/main/kotlin/net/corda/core/flows/FlowTransaction.kt b/core/src/main/kotlin/net/corda/core/flows/FlowTransaction.kt deleted file mode 100644 index b213c6dbd0..0000000000 --- a/core/src/main/kotlin/net/corda/core/flows/FlowTransaction.kt +++ /dev/null @@ -1,78 +0,0 @@ -package net.corda.core.flows - -import net.corda.core.identity.CordaX500Name -import net.corda.core.node.StatesToRecord -import net.corda.core.serialization.CordaSerializable -import java.time.Instant - -/** - * Flow data object representing key information required for recovery. - */ - -@CordaSerializable -data class FlowTransactionInfo( - val stateMachineRunId: StateMachineRunId, - val txId: String, - val status: TransactionStatus, - val timestamp: Instant, - val metadata: TransactionMetadata? -) { - fun isInitiator(myCordaX500Name: CordaX500Name) = - this.metadata?.initiator == myCordaX500Name -} - -@CordaSerializable -data class TransactionMetadata( - val initiator: CordaX500Name, - val distributionList: DistributionList -) - -@CordaSerializable -sealed class DistributionList { - - @CordaSerializable - data class SenderDistributionList( - val senderStatesToRecord: StatesToRecord, - val peersToStatesToRecord: Map - ) : DistributionList() - - @CordaSerializable - data class ReceiverDistributionList( - val opaqueData: ByteArray, // decipherable only by sender - val receiverStatesToRecord: StatesToRecord // inferred or actual - ) : DistributionList() -} - -@CordaSerializable -enum class TransactionStatus { - UNVERIFIED, - VERIFIED, - IN_FLIGHT; -} - -@CordaSerializable -data class RecoveryTimeWindow(val fromTime: Instant, val untilTime: Instant = Instant.now()) { - - init { - if (untilTime < fromTime) { - throw IllegalArgumentException("$fromTime must be before $untilTime") - } - } - - companion object { - @JvmStatic - fun between(fromTime: Instant, untilTime: Instant): RecoveryTimeWindow { - return RecoveryTimeWindow(fromTime, untilTime) - } - - @JvmStatic - fun fromOnly(fromTime: Instant): RecoveryTimeWindow { - return RecoveryTimeWindow(fromTime = fromTime) - } - - @JvmStatic - fun untilOnly(untilTime: Instant): RecoveryTimeWindow { - return RecoveryTimeWindow(fromTime = Instant.EPOCH, untilTime = untilTime) - } - } -} \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/flows/RecoveryTypes.kt b/core/src/main/kotlin/net/corda/core/flows/RecoveryTypes.kt new file mode 100644 index 0000000000..2fbdb9c1fe --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/flows/RecoveryTypes.kt @@ -0,0 +1,150 @@ +package net.corda.core.flows + +import net.corda.core.contracts.NamedByHash +import net.corda.core.crypto.SecureHash +import net.corda.core.identity.CordaX500Name +import net.corda.core.node.StatesToRecord +import net.corda.core.serialization.CordaSerializable +import net.corda.core.utilities.OpaqueBytes +import java.time.Instant +import java.time.temporal.ChronoUnit + +/** + * Transaction recovery type information. + */ + +@CordaSerializable +data class FlowTransactionInfo( + val stateMachineRunId: StateMachineRunId, + val txId: String, + val status: TransactionStatus, + val timestamp: Instant, + val metadata: TransactionMetadata? +) { + fun isInitiator(myCordaX500Name: CordaX500Name) = + this.metadata?.initiator == myCordaX500Name +} + +@CordaSerializable +data class TransactionMetadata( + val initiator: CordaX500Name, + val distributionList: DistributionList +) + +@CordaSerializable +sealed class DistributionList { + + @CordaSerializable + data class SenderDistributionList( + val senderStatesToRecord: StatesToRecord, + val peersToStatesToRecord: Map + ) : DistributionList() + + @CordaSerializable + data class ReceiverDistributionList( + val opaqueData: ByteArray, // decipherable only by sender + val receiverStatesToRecord: StatesToRecord // inferred or actual + ) : DistributionList() +} + +@CordaSerializable +enum class TransactionStatus { + UNVERIFIED, + VERIFIED, + IN_FLIGHT; +} + +@CordaSerializable +class DistributionRecords( + val senderRecords: List = emptyList(), + val receiverRecords: List = emptyList() +) { + val size = senderRecords.size + receiverRecords.size +} + +@CordaSerializable +abstract class DistributionRecord : NamedByHash { + abstract val txId: SecureHash + abstract val peerPartyId: SecureHash + abstract val timestamp: Instant + abstract val timestampDiscriminator: Int +} + +@CordaSerializable +data class SenderDistributionRecord( + override val txId: SecureHash, + override val peerPartyId: SecureHash, + override val timestamp: Instant, + override val timestampDiscriminator: Int, + val senderStatesToRecord: StatesToRecord, + val receiverStatesToRecord: StatesToRecord +) : DistributionRecord() { + override val id: SecureHash + get() = this.txId +} + +@CordaSerializable +data class ReceiverDistributionRecord( + override val txId: SecureHash, + override val peerPartyId: SecureHash, + override val timestamp: Instant, + override val timestampDiscriminator: Int, + val encryptedDistributionList: OpaqueBytes, + val receiverStatesToRecord: StatesToRecord +) : DistributionRecord() { + override val id: SecureHash + get() = this.txId +} + +@CordaSerializable +enum class DistributionRecordType { + SENDER, RECEIVER, ALL +} +@CordaSerializable +data class DistributionRecordKey( + val txnId: SecureHash, + val timestamp: Instant, + val timestampDiscriminator: Int +) + +@CordaSerializable +data class RecoveryTimeWindow(val fromTime: Instant, val untilTime: Instant = Instant.now()) { + + init { + if (untilTime < fromTime) { + throw IllegalArgumentException("$fromTime must be before $untilTime") + } + } + + companion object { + @JvmStatic + fun between(fromTime: Instant, untilTime: Instant): RecoveryTimeWindow { + return RecoveryTimeWindow(fromTime, untilTime) + } + + @JvmStatic + fun fromOnly(fromTime: Instant): RecoveryTimeWindow { + return RecoveryTimeWindow(fromTime = fromTime) + } + + @JvmStatic + fun untilOnly(untilTime: Instant): RecoveryTimeWindow { + return RecoveryTimeWindow(fromTime = Instant.EPOCH, untilTime = untilTime) + } + } +} + +@CordaSerializable +data class ComparableRecoveryTimeWindow( + val fromTime: Instant, + val fromTimestampDiscriminator: Int, + val untilTime: Instant, + val untilTimestampDiscriminator: Int +) { + companion object { + fun from(timeWindow: RecoveryTimeWindow) = + ComparableRecoveryTimeWindow( + timeWindow.fromTime.truncatedTo(ChronoUnit.SECONDS), 0, + timeWindow.untilTime.truncatedTo(ChronoUnit.SECONDS), Int.MAX_VALUE) + } +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecovery.kt b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecovery.kt index 84482acbd6..c68942bd8e 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecovery.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecovery.kt @@ -3,14 +3,17 @@ package net.corda.node.services.persistence import net.corda.core.crypto.SecureHash import net.corda.core.flows.DistributionList.ReceiverDistributionList import net.corda.core.flows.DistributionList.SenderDistributionList +import net.corda.core.flows.DistributionRecordKey +import net.corda.core.flows.DistributionRecordType +import net.corda.core.flows.DistributionRecords +import net.corda.core.flows.ReceiverDistributionRecord import net.corda.core.flows.RecoveryTimeWindow +import net.corda.core.flows.SenderDistributionRecord import net.corda.core.flows.TransactionMetadata import net.corda.core.identity.CordaX500Name import net.corda.core.internal.NamedCacheFactory -import net.corda.core.internal.VisibleForTesting import net.corda.core.node.StatesToRecord import net.corda.core.node.services.vault.Sort -import net.corda.core.serialization.CordaSerializable import net.corda.core.utilities.OpaqueBytes import net.corda.node.CordaClock import net.corda.node.services.EncryptionService @@ -20,6 +23,7 @@ import net.corda.nodeapi.internal.persistence.NODE_DATABASE_PREFIX import org.hibernate.annotations.Immutable import java.io.Serializable import java.time.Instant +import java.time.temporal.ChronoUnit import java.util.concurrent.atomic.AtomicInteger import javax.persistence.Column import javax.persistence.Embeddable @@ -54,7 +58,6 @@ class DBTransactionStorageLedgerRecovery(private val database: CordaPersistence, constructor(key: Key) : this(key.txId.toString(), key.partyId.toString(), key.timestamp, key.timestampDiscriminator) } - @CordaSerializable @Entity @Table(name = "${NODE_DATABASE_PREFIX}sender_distr_recs") data class DBSenderDistributionRecord( @@ -69,17 +72,22 @@ class DBTransactionStorageLedgerRecovery(private val database: CordaPersistence, @Column(name = "receiver_states_to_record", nullable = false) var receiverStatesToRecord: StatesToRecord ) { + fun key() = DistributionRecordKey( + SecureHash.parse(this.compositeKey.txId), + this.compositeKey.timestamp, + this.compositeKey.timestampDiscriminator) + fun toSenderDistributionRecord() = SenderDistributionRecord( SecureHash.parse(this.compositeKey.txId), SecureHash.parse(this.compositeKey.peerPartyId), + this.compositeKey.timestamp, + this.compositeKey.timestampDiscriminator, this.senderStatesToRecord, - this.receiverStatesToRecord, - this.compositeKey.timestamp + this.receiverStatesToRecord ) } - @CordaSerializable @Entity @Table(name = "${NODE_DATABASE_PREFIX}receiver_distr_recs") data class DBReceiverDistributionRecord( @@ -100,13 +108,20 @@ class DBTransactionStorageLedgerRecovery(private val database: CordaPersistence, distributionList = encryptedDistributionList, receiverStatesToRecord = receiverStatesToRecord ) - @VisibleForTesting + + fun key() = DistributionRecordKey( + SecureHash.parse(this.compositeKey.txId), + this.compositeKey.timestamp, + this.compositeKey.timestampDiscriminator) + fun toReceiverDistributionRecord(): ReceiverDistributionRecord { return ReceiverDistributionRecord( SecureHash.parse(this.compositeKey.txId), SecureHash.parse(this.compositeKey.peerPartyId), + this.compositeKey.timestamp, + this.compositeKey.timestampDiscriminator, OpaqueBytes(this.distributionList), - this.compositeKey.timestamp + this.receiverStatesToRecord ) } } @@ -137,7 +152,7 @@ class DBTransactionStorageLedgerRecovery(private val database: CordaPersistence, override fun addSenderTransactionRecoveryMetadata(txId: SecureHash, metadata: TransactionMetadata): ByteArray { return database.transaction { - val senderRecordingTimestamp = clock.instant() + val senderRecordingTimestamp = clock.instant().truncatedTo(ChronoUnit.SECONDS) val timeDiscriminator = Key.nextDiscriminatorNumber.andIncrement val distributionList = metadata.distributionList as? SenderDistributionList ?: throw IllegalStateException("Expecting SenderDistributionList") distributionList.peersToStatesToRecord.map { (peerCordaX500Name, peerStatesToRecord) -> @@ -174,7 +189,7 @@ class DBTransactionStorageLedgerRecovery(private val database: CordaPersistence, distributionList.opaqueData, distributionList.receiverStatesToRecord ) - session.save(receiverDistributionRecord) + session.saveOrUpdate(receiverDistributionRecord) } } else -> throw IllegalStateException("Expecting ReceiverDistributionList") @@ -200,9 +215,9 @@ class DBTransactionStorageLedgerRecovery(private val database: CordaPersistence, } fun queryDistributionRecords(timeWindow: RecoveryTimeWindow, - recordType: DistributionRecordType = DistributionRecordType.ALL, - excludingTxnIds: Set = emptySet(), - orderByTimestamp: Sort.Direction? = null + recordType: DistributionRecordType = DistributionRecordType.ALL, + excludingTxnIds: Set = emptySet(), + orderByTimestamp: Sort.Direction? = null ): DistributionRecords { return when(recordType) { DistributionRecordType.SENDER -> @@ -224,7 +239,7 @@ class DBTransactionStorageLedgerRecovery(private val database: CordaPersistence, peers: Set = emptySet(), excludingTxnIds: Set = emptySet(), orderByTimestamp: Sort.Direction? = null - ): List { + ): List { return database.transaction { val criteriaBuilder = session.criteriaBuilder val criteriaQuery = criteriaBuilder.createQuery(DBSenderDistributionRecord::class.java) @@ -253,7 +268,7 @@ class DBTransactionStorageLedgerRecovery(private val database: CordaPersistence, criteriaQuery.orderBy(orderCriteria) } session.createQuery(criteriaQuery).resultList - } + }.map { it.toSenderDistributionRecord() } } @Suppress("SpreadOperator") @@ -261,7 +276,7 @@ class DBTransactionStorageLedgerRecovery(private val database: CordaPersistence, initiators: Set = emptySet(), excludingTxnIds: Set = emptySet(), orderByTimestamp: Sort.Direction? = null - ): List { + ): List { return database.transaction { val criteriaBuilder = session.criteriaBuilder val criteriaQuery = criteriaBuilder.createQuery(DBReceiverDistributionRecord::class.java) @@ -277,7 +292,7 @@ class DBTransactionStorageLedgerRecovery(private val database: CordaPersistence, } if (initiators.isNotEmpty()) { val initiatorPartyIds = initiators.map { partyInfoCache.getPartyIdByCordaX500Name(it).toString() } - predicates.add(criteriaBuilder.and(compositeKey.get(PersistentKey::peerPartyId.name).`in`(initiatorPartyIds))) + predicates.add(criteriaBuilder.and(compositeKey.get(PersistentKey::peerPartyId.name).`in`(initiatorPartyIds))) } criteriaQuery.where(*predicates.toTypedArray()) // optionally order by timestamp @@ -290,7 +305,7 @@ class DBTransactionStorageLedgerRecovery(private val database: CordaPersistence, criteriaQuery.orderBy(orderCriteria) } session.createQuery(criteriaQuery).resultList - } + }.map { it.toReceiverDistributionRecord() } } fun decryptHashedDistributionList(encryptedBytes: ByteArray): HashedDistributionList { @@ -298,43 +313,3 @@ class DBTransactionStorageLedgerRecovery(private val database: CordaPersistence, } } - -@CordaSerializable -class DistributionRecords( - val senderRecords: List = emptyList(), - val receiverRecords: List = emptyList() -) { - init { - require(senderRecords.isNotEmpty() || receiverRecords.isNotEmpty()) { "Must set senderRecords or receiverRecords or both." } - } - - val size = senderRecords.size + receiverRecords.size -} - -@CordaSerializable -abstract class DistributionRecord { - abstract val txId: SecureHash - abstract val timestamp: Instant -} - -@CordaSerializable -data class SenderDistributionRecord( - override val txId: SecureHash, - val peerPartyId: SecureHash, // CordaX500Name hashCode() - val senderStatesToRecord: StatesToRecord, - val receiverStatesToRecord: StatesToRecord, - override val timestamp: Instant -) : DistributionRecord() - -@CordaSerializable -data class ReceiverDistributionRecord( - override val txId: SecureHash, - val initiatorPartyId: SecureHash, // CordaX500Name hashCode() - val encryptedDistributionList: OpaqueBytes, - override val timestamp: Instant -) : DistributionRecord() - -@CordaSerializable -enum class DistributionRecordType { - SENDER, RECEIVER, ALL -} diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecoveryTests.kt b/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecoveryTests.kt index 60a48a073c..c4a98b1093 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecoveryTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecoveryTests.kt @@ -8,7 +8,10 @@ import net.corda.core.crypto.SignatureMetadata import net.corda.core.crypto.sign import net.corda.core.flows.DistributionList.ReceiverDistributionList import net.corda.core.flows.DistributionList.SenderDistributionList +import net.corda.core.flows.DistributionRecordType +import net.corda.core.flows.ReceiverDistributionRecord import net.corda.core.flows.RecoveryTimeWindow +import net.corda.core.flows.SenderDistributionRecord import net.corda.core.flows.TransactionMetadata import net.corda.core.node.NodeInfo import net.corda.core.node.StatesToRecord.ALL_VISIBLE @@ -18,7 +21,6 @@ import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.WireTransaction import net.corda.core.utilities.NetworkHostAndPort import net.corda.node.CordaClock -import net.corda.node.SimpleClock import net.corda.node.services.identity.InMemoryIdentityService import net.corda.node.services.network.PersistentNetworkMapCache import net.corda.node.services.network.PersistentPartyInfoCache @@ -40,6 +42,7 @@ import net.corda.testing.internal.TestingNamedCacheFactory import net.corda.testing.internal.configureDatabase import net.corda.testing.internal.createWireTransaction import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties +import net.corda.testing.node.TestClock import net.corda.testing.node.internal.MockEncryptionService import org.assertj.core.api.Assertions.assertThat import org.junit.After @@ -48,7 +51,8 @@ import org.junit.Rule import org.junit.Test import java.security.KeyPair import java.time.Clock -import java.time.Instant.now +import java.time.Duration +import java.time.Instant import java.time.temporal.ChronoUnit import kotlin.test.assertEquals import kotlin.test.assertFailsWith @@ -84,9 +88,13 @@ class DBTransactionStorageLedgerRecoveryTests { database.close() } + fun now(): Instant { + return transactionRecovery.clock.instant() + } + @Test(timeout = 300_000) fun `query local ledger for transactions with recovery peers within time window`() { - val beforeFirstTxn = now() + val beforeFirstTxn = now().truncatedTo(ChronoUnit.SECONDS) val txn = newTransaction() transactionRecovery.addUnnotarisedTransaction(txn) transactionRecovery.addSenderTransactionRecoveryMetadata(txn.id, TransactionMetadata(ALICE_NAME, SenderDistributionList(ALL_VISIBLE, mapOf(BOB_NAME to ONLY_RELEVANT)))) @@ -94,13 +102,14 @@ class DBTransactionStorageLedgerRecoveryTests { untilTime = beforeFirstTxn.plus(1, ChronoUnit.MINUTES)) val results = transactionRecovery.querySenderDistributionRecords(timeWindow) assertEquals(1, results.size) - - val afterFirstTxn = now() + (transactionRecovery.clock as TestClock).advanceBy(Duration.ofSeconds(1)) + val afterFirstTxn = now().truncatedTo(ChronoUnit.SECONDS) val txn2 = newTransaction() transactionRecovery.addUnnotarisedTransaction(txn2) transactionRecovery.addSenderTransactionRecoveryMetadata(txn2.id, TransactionMetadata(ALICE_NAME, SenderDistributionList(ONLY_RELEVANT, mapOf(CHARLIE_NAME to ONLY_RELEVANT)))) assertEquals(2, transactionRecovery.querySenderDistributionRecords(timeWindow).size) - assertEquals(1, transactionRecovery.querySenderDistributionRecords(RecoveryTimeWindow(fromTime = afterFirstTxn)).size) + assertEquals(1, transactionRecovery.querySenderDistributionRecords(RecoveryTimeWindow(fromTime = afterFirstTxn, + untilTime = afterFirstTxn.plus(1, ChronoUnit.MINUTES))).size) } @Test(timeout = 300_000) @@ -114,7 +123,7 @@ class DBTransactionStorageLedgerRecoveryTests { val timeWindow = RecoveryTimeWindow(fromTime = now().minus(1, ChronoUnit.DAYS)) val results = transactionRecovery.querySenderDistributionRecords(timeWindow, excludingTxnIds = setOf(transaction1.id)) assertEquals(1, results.size) - assertEquals(transaction2.id.toString(), results[0].compositeKey.txId) + assertEquals(transaction2.id, results[0].txId) } @Test(timeout = 300_000) @@ -128,7 +137,7 @@ class DBTransactionStorageLedgerRecoveryTests { val timeWindow = RecoveryTimeWindow(fromTime = now().minus(1, ChronoUnit.DAYS)) val results = transactionRecovery.querySenderDistributionRecords(timeWindow, peers = setOf(CHARLIE_NAME)) assertEquals(1, results.size) - assertEquals(transaction2.id.toString(), results[0].compositeKey.txId) + assertEquals(transaction2.id, results[0].txId) } @Test(timeout = 300_000) @@ -147,13 +156,13 @@ class DBTransactionStorageLedgerRecoveryTests { val timeWindow = RecoveryTimeWindow(fromTime = now().minus(1, ChronoUnit.DAYS)) transactionRecovery.queryDistributionRecords(timeWindow, recordType = DistributionRecordType.SENDER).let { assertEquals(2, it.size) - assertEquals(SecureHash.sha256(BOB_NAME.toString()).toString(), it.senderRecords[0].compositeKey.peerPartyId) + assertEquals(SecureHash.sha256(BOB_NAME.toString()), it.senderRecords[0].peerPartyId) assertEquals(ALL_VISIBLE, it.senderRecords[0].senderStatesToRecord) } transactionRecovery.queryDistributionRecords(timeWindow, recordType = DistributionRecordType.RECEIVER).let { assertEquals(1, it.size) - assertEquals(SecureHash.sha256(BOB_NAME.toString()).toString(), it.receiverRecords[0].compositeKey.peerPartyId) - assertEquals(ALL_VISIBLE, (HashedDistributionList.decrypt(it.receiverRecords[0].distributionList, encryptionService)).peerHashToStatesToRecord.map { it.value }[0]) + assertEquals(SecureHash.sha256(BOB_NAME.toString()), it.receiverRecords[0].peerPartyId) + assertEquals(ALL_VISIBLE, (HashedDistributionList.decrypt(it.receiverRecords[0].encryptedDistributionList.bytes, encryptionService)).peerHashToStatesToRecord.map { it.value }[0]) } val resultsAll = transactionRecovery.queryDistributionRecords(timeWindow, recordType = DistributionRecordType.ALL) assertEquals(3, resultsAll.size) @@ -224,9 +233,9 @@ class DBTransactionStorageLedgerRecoveryTests { val timeWindow = RecoveryTimeWindow(fromTime = now().minus(1, ChronoUnit.DAYS)) transactionRecovery.queryReceiverDistributionRecords(timeWindow, initiators = setOf(ALICE_NAME)).let { assertEquals(3, it.size) - assertEquals(HashedDistributionList.decrypt(it[0].distributionList, encryptionService).peerHashToStatesToRecord.map { it.value }[0], ALL_VISIBLE) - assertEquals(HashedDistributionList.decrypt(it[1].distributionList, encryptionService).peerHashToStatesToRecord.map { it.value }[0], ONLY_RELEVANT) - assertEquals(HashedDistributionList.decrypt(it[2].distributionList, encryptionService).peerHashToStatesToRecord.map { it.value }[0], NONE) + assertEquals(HashedDistributionList.decrypt(it[0].encryptedDistributionList.bytes, encryptionService).peerHashToStatesToRecord.map { it.value }[0], ALL_VISIBLE) + assertEquals(HashedDistributionList.decrypt(it[1].encryptedDistributionList.bytes, encryptionService).peerHashToStatesToRecord.map { it.value }[0], ONLY_RELEVANT) + assertEquals(HashedDistributionList.decrypt(it[2].encryptedDistributionList.bytes, encryptionService).peerHashToStatesToRecord.map { it.value }[0], NONE) } assertEquals(1, transactionRecovery.queryReceiverDistributionRecords(timeWindow, initiators = setOf(BOB_NAME)).size) assertEquals(1, transactionRecovery.queryReceiverDistributionRecords(timeWindow, initiators = setOf(CHARLIE_NAME)).size) @@ -266,7 +275,7 @@ class DBTransactionStorageLedgerRecoveryTests { val distList = transactionRecovery.decryptHashedDistributionList(record.encryptedDistributionList.bytes) assertEquals(ONLY_RELEVANT, distList.senderStatesToRecord) assertEquals(ALL_VISIBLE, distList.peerHashToStatesToRecord.values.first()) - assertEquals(ALICE_NAME, partyInfoCache.getCordaX500NameByPartyId(record.initiatorPartyId)) + assertEquals(ALICE_NAME, partyInfoCache.getCordaX500NameByPartyId(record.peerPartyId)) assertEquals(setOf(BOB_NAME), distList.peerHashToStatesToRecord.map { (peer) -> partyInfoCache.getCordaX500NameByPartyId(peer) }.toSet() ) } } @@ -364,7 +373,7 @@ class DBTransactionStorageLedgerRecoveryTests { return fromDb[0].toReceiverDistributionRecord() } - private fun newTransactionRecovery(cacheSizeBytesOverride: Long? = null, clock: CordaClock = SimpleClock(Clock.systemUTC())) { + private fun newTransactionRecovery(cacheSizeBytesOverride: Long? = null, clock: CordaClock = TestClock(Clock.systemUTC())) { val networkMapCache = PersistentNetworkMapCache(TestingNamedCacheFactory(), database, InMemoryIdentityService(trustRoot = DEV_ROOT_CA.certificate)) val alice = createNodeInfo(listOf(ALICE)) val bob = createNodeInfo(listOf(BOB)) From 39928563f61948ede7ceb6ee51579fe34ef1e560 Mon Sep 17 00:00:00 2001 From: Jose Coll Date: Thu, 19 Oct 2023 09:29:05 +0100 Subject: [PATCH 087/126] Set verboseLogging to false by default. Override toString() for ReceiverDistributionRecord --- .../src/main/kotlin/net/corda/core/flows/LedgerRecoverFlow.kt | 2 +- core/src/main/kotlin/net/corda/core/flows/RecoveryTypes.kt | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/core/src/main/kotlin/net/corda/core/flows/LedgerRecoverFlow.kt b/core/src/main/kotlin/net/corda/core/flows/LedgerRecoverFlow.kt index 32c0ff3866..f416fe1455 100644 --- a/core/src/main/kotlin/net/corda/core/flows/LedgerRecoverFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/LedgerRecoverFlow.kt @@ -36,7 +36,7 @@ data class LedgerRecoveryParameters( val useAllNetworkNodes: Boolean = false, val dryRun: Boolean = false, val useTimeWindowNarrowing: Boolean = true, - val verboseLogging: Boolean = true, + val verboseLogging: Boolean = false, val recoveryBatchSize: Int = 1000 ) diff --git a/core/src/main/kotlin/net/corda/core/flows/RecoveryTypes.kt b/core/src/main/kotlin/net/corda/core/flows/RecoveryTypes.kt index 2fbdb9c1fe..ae5d3ee4e6 100644 --- a/core/src/main/kotlin/net/corda/core/flows/RecoveryTypes.kt +++ b/core/src/main/kotlin/net/corda/core/flows/RecoveryTypes.kt @@ -94,6 +94,10 @@ data class ReceiverDistributionRecord( ) : DistributionRecord() { override val id: SecureHash get() = this.txId + + override fun toString(): String { + return "txId: $txId, peerPartyId: $peerPartyId, timestamp: $timestamp, timestampDiscriminator: $timestampDiscriminator, receiverStatesToRecord: $receiverStatesToRecord" + } } @CordaSerializable From bc718088febb5248a398a4e04d93c7bc919f9774 Mon Sep 17 00:00:00 2001 From: Jose Coll Date: Tue, 24 Oct 2023 10:38:46 +0100 Subject: [PATCH 088/126] ENT-10100 Changes required to support recovery of IN_FLIGHT transactions. (#7541) --- .../coretests/flows/FinalityFlowTests.kt | 50 +++++++++---------- .../corda/core/flows/SendTransactionFlow.kt | 1 + .../net/corda/core/internal/FetchDataFlow.kt | 14 ++++-- .../core/internal/ResolveTransactionsFlow.kt | 8 ++- .../core/internal/ServiceHubCoreInternal.kt | 2 +- .../core/node/services/TransactionStorage.kt | 17 +++++++ .../node/services/DbTransactionsResolver.kt | 10 ++-- .../node/services/api/ServiceHubInternal.kt | 7 --- .../persistence/DBTransactionStorage.kt | 12 ++--- .../node/messaging/TwoPartyTradeFlowTests.kt | 12 ++--- ...DBTransactionStorageLedgerRecoveryTests.kt | 4 +- .../persistence/DBTransactionStorageTests.kt | 2 +- .../node/internal/MockTransactionStorage.kt | 5 +- 13 files changed, 84 insertions(+), 60 deletions(-) diff --git a/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt b/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt index 8048c99dbd..562cc3a4b4 100644 --- a/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt +++ b/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt @@ -182,9 +182,9 @@ class FinalityFlowTests : WithFinality { val ref = aliceNode.startFlowAndRunNetwork(IssueFlow(notary)).resultFuture.getOrThrow() val stx = aliceNode.startFlowAndRunNetwork(SpendFlow(ref, bobNode.info.singleIdentity())).resultFuture.getOrThrow() - val (_, txnStatusAlice) = aliceNode.services.validatedTransactions.getTransactionInternal(stx.id) ?: fail() + val (_, txnStatusAlice) = aliceNode.services.validatedTransactions.getTransactionWithStatus(stx.id) ?: fail() assertEquals(TransactionStatus.VERIFIED, txnStatusAlice) - val (_, txnStatusBob) = bobNode.services.validatedTransactions.getTransactionInternal(stx.id) ?: fail() + val (_, txnStatusBob) = bobNode.services.validatedTransactions.getTransactionWithStatus(stx.id) ?: fail() assertEquals(TransactionStatus.VERIFIED, txnStatusBob) try { @@ -192,10 +192,10 @@ class FinalityFlowTests : WithFinality { } catch (e: NotaryException) { val stxId = (e.error as NotaryError.Conflict).txId - assertNull(aliceNode.services.validatedTransactions.getTransactionInternal(stxId)) + assertNull(aliceNode.services.validatedTransactions.getTransactionWithStatus(stxId)) // Note: double spend error not propagated to peers by default (corDapp PV = 3) // Un-notarised txn clean-up occurs in ReceiveFinalityFlow upon receipt of UnexpectedFlowEndException - assertNull(aliceNode.services.validatedTransactions.getTransactionInternal(stxId)) + assertNull(aliceNode.services.validatedTransactions.getTransactionWithStatus(stxId)) assertTxnRemovedFromDatabase(aliceNode, stxId) } } @@ -207,9 +207,9 @@ class FinalityFlowTests : WithFinality { val ref = aliceNode.startFlowAndRunNetwork(IssueFlow(notary)).resultFuture.getOrThrow() val stx = aliceNode.startFlowAndRunNetwork(SpendFlow(ref, bobNode.info.singleIdentity())).resultFuture.getOrThrow() - val (_, txnStatusAlice) = aliceNode.services.validatedTransactions.getTransactionInternal(stx.id) ?: fail() + val (_, txnStatusAlice) = aliceNode.services.validatedTransactions.getTransactionWithStatus(stx.id) ?: fail() assertEquals(TransactionStatus.VERIFIED, txnStatusAlice) - val (_, txnStatusBob) = bobNode.services.validatedTransactions.getTransactionInternal(stx.id) ?: fail() + val (_, txnStatusBob) = bobNode.services.validatedTransactions.getTransactionWithStatus(stx.id) ?: fail() assertEquals(TransactionStatus.VERIFIED, txnStatusBob) try { @@ -217,9 +217,9 @@ class FinalityFlowTests : WithFinality { } catch (e: NotaryException) { val stxId = (e.error as NotaryError.Conflict).txId - assertNull(aliceNode.services.validatedTransactions.getTransactionInternal(stxId)) + assertNull(aliceNode.services.validatedTransactions.getTransactionWithStatus(stxId)) assertTxnRemovedFromDatabase(aliceNode, stxId) - assertNull(bobNode.services.validatedTransactions.getTransactionInternal(stxId)) + assertNull(bobNode.services.validatedTransactions.getTransactionWithStatus(stxId)) assertTxnRemovedFromDatabase(bobNode, stxId) } @@ -228,9 +228,9 @@ class FinalityFlowTests : WithFinality { } catch (e: NotaryException) { val stxId = (e.error as NotaryError.Conflict).txId - assertNull(aliceNode.services.validatedTransactions.getTransactionInternal(stxId)) + assertNull(aliceNode.services.validatedTransactions.getTransactionWithStatus(stxId)) assertTxnRemovedFromDatabase(aliceNode, stxId) - val (_, txnStatus) = bobNode.services.validatedTransactions.getTransactionInternal(stxId) ?: fail() + val (_, txnStatus) = bobNode.services.validatedTransactions.getTransactionWithStatus(stxId) ?: fail() assertEquals(TransactionStatus.IN_FLIGHT, txnStatus) } } @@ -252,9 +252,9 @@ class FinalityFlowTests : WithFinality { val ref = bobNode.startFlowAndRunNetwork(IssueFlow(notary)).resultFuture.getOrThrow() val stx = bobNode.startFlowAndRunNetwork(SpendFlow(ref, aliceNode.info.singleIdentity())).resultFuture.getOrThrow() - val (_, txnStatusAlice) = aliceNode.services.validatedTransactions.getTransactionInternal(stx.id) ?: fail() + val (_, txnStatusAlice) = aliceNode.services.validatedTransactions.getTransactionWithStatus(stx.id) ?: fail() assertEquals(TransactionStatus.VERIFIED, txnStatusAlice) - val (_, txnStatusBob) = bobNode.services.validatedTransactions.getTransactionInternal(stx.id) ?: fail() + val (_, txnStatusBob) = bobNode.services.validatedTransactions.getTransactionWithStatus(stx.id) ?: fail() assertEquals(TransactionStatus.VERIFIED, txnStatusBob) try { @@ -262,9 +262,9 @@ class FinalityFlowTests : WithFinality { } catch (e: NotaryException) { val stxId = (e.error as NotaryError.Conflict).txId - assertNull(bobNode.services.validatedTransactions.getTransactionInternal(stxId)) + assertNull(bobNode.services.validatedTransactions.getTransactionWithStatus(stxId)) assertTxnRemovedFromDatabase(bobNode, stxId) - assertNull(aliceNode.services.validatedTransactions.getTransactionInternal(stxId)) + assertNull(aliceNode.services.validatedTransactions.getTransactionWithStatus(stxId)) assertTxnRemovedFromDatabase(aliceNode, stxId) } } @@ -276,9 +276,9 @@ class FinalityFlowTests : WithFinality { val ref = aliceNode.startFlowAndRunNetwork(IssueFlow(notary)).resultFuture.getOrThrow() val stx = aliceNode.startFlowAndRunNetwork(SpendFlow(ref, bobNode.info.singleIdentity())).resultFuture.getOrThrow() - val (_, txnStatusAlice) = aliceNode.services.validatedTransactions.getTransactionInternal(stx.id) ?: fail() + val (_, txnStatusAlice) = aliceNode.services.validatedTransactions.getTransactionWithStatus(stx.id) ?: fail() assertEquals(TransactionStatus.VERIFIED, txnStatusAlice) - val (_, txnStatusBob) = bobNode.services.validatedTransactions.getTransactionInternal(stx.id) ?: fail() + val (_, txnStatusBob) = bobNode.services.validatedTransactions.getTransactionWithStatus(stx.id) ?: fail() assertEquals(TransactionStatus.VERIFIED, txnStatusBob) try { @@ -286,9 +286,9 @@ class FinalityFlowTests : WithFinality { } catch (e: NotaryException) { val stxId = (e.error as NotaryError.Conflict).txId - assertNull(aliceNode.services.validatedTransactions.getTransactionInternal(stxId)) + assertNull(aliceNode.services.validatedTransactions.getTransactionWithStatus(stxId)) assertTxnRemovedFromDatabase(aliceNode, stxId) - assertNull(bobNode.services.validatedTransactions.getTransactionInternal(stxId)) + assertNull(bobNode.services.validatedTransactions.getTransactionWithStatus(stxId)) assertTxnRemovedFromDatabase(bobNode, stxId) } } @@ -300,9 +300,9 @@ class FinalityFlowTests : WithFinality { val ref = aliceNode.startFlowAndRunNetwork(IssueFlow(notary)).resultFuture.getOrThrow() val notarisedStxn1 = aliceNode.startFlowAndRunNetwork(SpeedySpendFlow(ref, bobNode.info.singleIdentity())).resultFuture.getOrThrow() - val (_, txnStatusAlice) = aliceNode.services.validatedTransactions.getTransactionInternal(notarisedStxn1.id) ?: fail() + val (_, txnStatusAlice) = aliceNode.services.validatedTransactions.getTransactionWithStatus(notarisedStxn1.id) ?: fail() assertEquals(TransactionStatus.VERIFIED, txnStatusAlice) - val (_, txnStatusBob) = bobNode.services.validatedTransactions.getTransactionInternal(notarisedStxn1.id) ?: fail() + val (_, txnStatusBob) = bobNode.services.validatedTransactions.getTransactionWithStatus(notarisedStxn1.id) ?: fail() assertEquals(TransactionStatus.IN_FLIGHT, txnStatusBob) // now lets attempt a new spend with the new output of the previous transaction @@ -311,17 +311,17 @@ class FinalityFlowTests : WithFinality { // the original transaction is now finalised at Bob (despite the original flow not completing) because Bob resolved the // original transaction from Alice in the second transaction (and Alice had already notarised and finalised the original transaction) - val (_, txnStatusBobAgain) = bobNode.services.validatedTransactions.getTransactionInternal(notarisedStxn1.id) ?: fail() + val (_, txnStatusBobAgain) = bobNode.services.validatedTransactions.getTransactionWithStatus(notarisedStxn1.id) ?: fail() assertEquals(TransactionStatus.VERIFIED, txnStatusBobAgain) - val (_, txnStatusAlice2) = aliceNode.services.validatedTransactions.getTransactionInternal(notarisedStxn2.id) ?: fail() + val (_, txnStatusAlice2) = aliceNode.services.validatedTransactions.getTransactionWithStatus(notarisedStxn2.id) ?: fail() assertEquals(TransactionStatus.VERIFIED, txnStatusAlice2) - val (_, txnStatusBob2) = bobNode.services.validatedTransactions.getTransactionInternal(notarisedStxn2.id) ?: fail() + val (_, txnStatusBob2) = bobNode.services.validatedTransactions.getTransactionWithStatus(notarisedStxn2.id) ?: fail() assertEquals(TransactionStatus.IN_FLIGHT, txnStatusBob2) // Validate attempt at flow finalisation by Bob has no effect on outcome. val finaliseStxn1 = bobNode.startFlowAndRunNetwork(FinaliseSpeedySpendFlow(notarisedStxn1.id, notarisedStxn1.sigs)).resultFuture.getOrThrow() - val (_, txnStatusBobYetAgain) = bobNode.services.validatedTransactions.getTransactionInternal(finaliseStxn1.id) ?: fail() + val (_, txnStatusBobYetAgain) = bobNode.services.validatedTransactions.getTransactionWithStatus(finaliseStxn1.id) ?: fail() assertEquals(TransactionStatus.VERIFIED, txnStatusBobYetAgain) } @@ -335,7 +335,7 @@ class FinalityFlowTests : WithFinality { } catch (e: UnexpectedFlowEndException) { val stxId = SecureHash.parse(e.message) - val (_, txnStatusBob) = bobNode.services.validatedTransactions.getTransactionInternal(stxId) ?: fail() + val (_, txnStatusBob) = bobNode.services.validatedTransactions.getTransactionWithStatus(stxId) ?: fail() assertEquals(TransactionStatus.IN_FLIGHT, txnStatusBob) } } diff --git a/core/src/main/kotlin/net/corda/core/flows/SendTransactionFlow.kt b/core/src/main/kotlin/net/corda/core/flows/SendTransactionFlow.kt index d10bbe0ebb..9f57de9580 100644 --- a/core/src/main/kotlin/net/corda/core/flows/SendTransactionFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/SendTransactionFlow.kt @@ -221,6 +221,7 @@ open class DataVendingFlow(val otherSessions: Set, val payload: Any numSent++ tx } + FetchDataFlow.DataType.TRANSACTION_RECOVERY -> NotImplementedError("Enterprise only feature") // Loop on all items returned using dataRequest.hashes.map: FetchDataFlow.DataType.BATCH_TRANSACTION -> dataRequest.hashes.map { txId -> if (!authorisedTransactions.isAuthorised(txId)) { diff --git a/core/src/main/kotlin/net/corda/core/internal/FetchDataFlow.kt b/core/src/main/kotlin/net/corda/core/internal/FetchDataFlow.kt index 8d69db366f..1479ab4896 100644 --- a/core/src/main/kotlin/net/corda/core/internal/FetchDataFlow.kt +++ b/core/src/main/kotlin/net/corda/core/internal/FetchDataFlow.kt @@ -12,6 +12,7 @@ import net.corda.core.flows.MaybeSerializedSignedTransaction import net.corda.core.internal.FetchDataFlow.DownloadedVsRequestedDataMismatch import net.corda.core.internal.FetchDataFlow.HashNotFound import net.corda.core.node.NetworkParameters +import net.corda.core.node.services.SignedTransactionWithStatus import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.CordaSerializationTransformEnumDefault import net.corda.core.serialization.CordaSerializationTransformEnumDefaults @@ -82,7 +83,7 @@ sealed class FetchDataFlow( ) @CordaSerializable enum class DataType { - TRANSACTION, ATTACHMENT, PARAMETERS, BATCH_TRANSACTION, UNKNOWN + TRANSACTION, ATTACHMENT, PARAMETERS, BATCH_TRANSACTION, UNKNOWN, TRANSACTION_RECOVERY } @Suspendable @@ -267,12 +268,19 @@ class FetchAttachmentsFlow(requests: Set, * Authorisation is accorded only on valid ancestors of the root transaction. * Note that returned transactions are not inserted into the database, because it's up to the caller to actually verify the transactions are valid. */ -class FetchTransactionsFlow(requests: Set, otherSide: FlowSession) : - FetchDataFlow(requests, otherSide, DataType.TRANSACTION) { +class FetchTransactionsFlow @JvmOverloads constructor(requests: Set, otherSide: FlowSession, dataType: DataType = DataType.TRANSACTION) : + FetchDataFlow(requests, otherSide, dataType) { override fun load(txid: SecureHash): SignedTransaction? = serviceHub.validatedTransactions.getTransaction(txid) } +// Used by Enterprise Ledger Recovery +class FetchRecoverableTransactionsFlow @JvmOverloads constructor(requests: Set, otherSide: FlowSession, dataType: DataType = DataType.TRANSACTION_RECOVERY) : + FetchDataFlow(requests, otherSide, dataType) { + + override fun load(txid: SecureHash): SignedTransactionWithStatus? = serviceHub.validatedTransactions.getTransactionWithStatus(txid) +} + class FetchBatchTransactionsFlow(requests: Set, otherSide: FlowSession) : FetchDataFlow(requests, otherSide, DataType.BATCH_TRANSACTION) { diff --git a/core/src/main/kotlin/net/corda/core/internal/ResolveTransactionsFlow.kt b/core/src/main/kotlin/net/corda/core/internal/ResolveTransactionsFlow.kt index 053015fef9..7b8f248667 100644 --- a/core/src/main/kotlin/net/corda/core/internal/ResolveTransactionsFlow.kt +++ b/core/src/main/kotlin/net/corda/core/internal/ResolveTransactionsFlow.kt @@ -20,7 +20,8 @@ class ResolveTransactionsFlow private constructor( val txHashes: Set, val otherSide: FlowSession, val statesToRecord: StatesToRecord, - val deferredAck: Boolean = false + val deferredAck: Boolean = false, + val recoveryMode: Boolean = false ) : FlowLogic() { constructor(txHashes: Set, otherSide: FlowSession, statesToRecord: StatesToRecord = StatesToRecord.NONE) @@ -29,6 +30,9 @@ class ResolveTransactionsFlow private constructor( constructor(txHashes: Set, otherSide: FlowSession, statesToRecord: StatesToRecord, deferredAck: Boolean) : this(null, txHashes, otherSide, statesToRecord, deferredAck) + constructor(txHashes: Set, otherSide: FlowSession, statesToRecord: StatesToRecord, deferredAck: Boolean, recoveryMode: Boolean ) + : this(null, txHashes, otherSide, statesToRecord, deferredAck, recoveryMode) + /** * Resolves and validates the dependencies of the specified [SignedTransaction]. Fetches the attachments, but does * *not* validate or store the [SignedTransaction] itself. @@ -63,7 +67,7 @@ class ResolveTransactionsFlow private constructor( } val resolver = (serviceHub as ServiceHubCoreInternal).createTransactionsResolver(this) - resolver.downloadDependencies(batchMode) + resolver.downloadDependencies(batchMode, recoveryMode) if (!deferredAck) { logger.trace { "ResolveTransactionsFlow: Sending END." } diff --git a/core/src/main/kotlin/net/corda/core/internal/ServiceHubCoreInternal.kt b/core/src/main/kotlin/net/corda/core/internal/ServiceHubCoreInternal.kt index d752eb3b15..bd7c1142ac 100644 --- a/core/src/main/kotlin/net/corda/core/internal/ServiceHubCoreInternal.kt +++ b/core/src/main/kotlin/net/corda/core/internal/ServiceHubCoreInternal.kt @@ -85,7 +85,7 @@ interface ServiceHubCoreInternal : ServiceHub { interface TransactionsResolver { @Suspendable - fun downloadDependencies(batchMode: Boolean) + fun downloadDependencies(batchMode: Boolean, recoveryMode: Boolean) @Suspendable fun recordDependencies(usedStatesToRecord: StatesToRecord) diff --git a/core/src/main/kotlin/net/corda/core/node/services/TransactionStorage.kt b/core/src/main/kotlin/net/corda/core/node/services/TransactionStorage.kt index b04c96729f..b4875040e0 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/TransactionStorage.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/TransactionStorage.kt @@ -2,8 +2,11 @@ package net.corda.core.node.services import net.corda.core.DoNotImplement import net.corda.core.concurrent.CordaFuture +import net.corda.core.contracts.NamedByHash import net.corda.core.crypto.SecureHash +import net.corda.core.flows.TransactionStatus import net.corda.core.messaging.DataFeed +import net.corda.core.serialization.CordaSerializable import net.corda.core.transactions.SignedTransaction import rx.Observable @@ -17,6 +20,11 @@ interface TransactionStorage { */ fun getTransaction(id: SecureHash): SignedTransaction? + /** + * Return the transaction with its status for the given [id], or null if no such transaction exists. + */ + fun getTransactionWithStatus(id: SecureHash): SignedTransactionWithStatus? + /** * Get a synchronous Observable of updates. When observations are pushed to the Observer, the vault will already * incorporate the update. @@ -32,4 +40,13 @@ interface TransactionStorage { * Returns a future that completes with the transaction corresponding to [id] once it has been committed */ fun trackTransaction(id: SecureHash): CordaFuture +} + +@CordaSerializable +data class SignedTransactionWithStatus( + val stx: SignedTransaction, + val status: TransactionStatus +) : NamedByHash { + override val id: SecureHash + get() = stx.id } \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/services/DbTransactionsResolver.kt b/node/src/main/kotlin/net/corda/node/services/DbTransactionsResolver.kt index 3d737ea422..e82549d8e3 100644 --- a/node/src/main/kotlin/net/corda/node/services/DbTransactionsResolver.kt +++ b/node/src/main/kotlin/net/corda/node/services/DbTransactionsResolver.kt @@ -11,8 +11,8 @@ import net.corda.core.internal.dependencies import net.corda.core.node.StatesToRecord import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.debug -import net.corda.core.utilities.trace import net.corda.core.utilities.seconds +import net.corda.core.utilities.trace import net.corda.node.services.api.WritableTransactionStorage import java.util.* @@ -21,7 +21,8 @@ class DbTransactionsResolver(private val flow: ResolveTransactionsFlow) : Transa private val logger = flow.logger @Suspendable - override fun downloadDependencies(batchMode: Boolean) { + override fun downloadDependencies(batchMode: Boolean, recoveryMode: Boolean) { + if (recoveryMode) throw NotImplementedError("Enterprise only Ledger Recovery feature") logger.debug { "Downloading dependencies for transactions ${flow.txHashes}" } val transactionStorage = flow.serviceHub.validatedTransactions as WritableTransactionStorage @@ -99,13 +100,12 @@ class DbTransactionsResolver(private val flow: ResolveTransactionsFlow) : Transa override fun recordDependencies(usedStatesToRecord: StatesToRecord) { val sortedDependencies = checkNotNull(this.sortedDependencies) logger.trace { "Recording ${sortedDependencies.size} dependencies for ${flow.txHashes.size} transactions" } - val transactionStorage = flow.serviceHub.validatedTransactions as WritableTransactionStorage for (txId in sortedDependencies) { // Retrieve and delete the transaction from the unverified store. - val (tx, txStatus) = checkNotNull(transactionStorage.getTransactionInternal(txId)) { + val (tx, txStatus) = checkNotNull(flow.serviceHub.validatedTransactions.getTransactionWithStatus(txId)) { "Somehow the unverified transaction ($txId) that we stored previously is no longer there." } - if (txStatus != TransactionStatus.VERIFIED) { + if (txStatus == TransactionStatus.UNVERIFIED) { tx.verify(flow.serviceHub) flow.serviceHub.recordTransactions(usedStatesToRecord, listOf(tx)) } else { diff --git a/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt b/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt index 9ed76b15c8..0ee732b210 100644 --- a/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt +++ b/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt @@ -7,7 +7,6 @@ import net.corda.core.crypto.TransactionSignature import net.corda.core.flows.FlowLogic import net.corda.core.flows.TransactionMetadata import net.corda.core.flows.StateMachineRunId -import net.corda.core.flows.TransactionStatus import net.corda.core.identity.CordaX500Name import net.corda.core.internal.FlowStateMachineHandle import net.corda.core.internal.NamedCacheFactory @@ -416,12 +415,6 @@ interface WritableTransactionStorage : TransactionStorage { */ fun addUnverifiedTransaction(transaction: SignedTransaction) - /** - * Return the transaction with the given ID from the store, and its associated [TransactionStatus]. - * Returns null if no transaction with the ID exists. - */ - fun getTransactionInternal(id: SecureHash): Pair? - /** * Returns a future that completes with the transaction corresponding to [id] once it has been committed. Do not warn when run inside * a DB transaction. diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt index 6905d6f7c1..8577afe934 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt @@ -11,6 +11,7 @@ import net.corda.core.internal.VisibleForTesting import net.corda.core.internal.bufferUntilSubscribed import net.corda.core.internal.concurrent.doneFuture import net.corda.core.messaging.DataFeed +import net.corda.core.node.services.SignedTransactionWithStatus import net.corda.core.serialization.SerializationContext import net.corda.core.serialization.SerializationDefaults import net.corda.core.serialization.SerializedBytes @@ -314,6 +315,11 @@ open class DBTransactionStorage(private val database: CordaPersistence, cacheFac } } + override fun getTransactionWithStatus(id: SecureHash): SignedTransactionWithStatus? = + database.transaction { + txStorage.content[id]?.let { SignedTransactionWithStatus(it.toSignedTx(), it.status.toTransactionStatus()) } + } + override fun addUnverifiedTransaction(transaction: SignedTransaction) { if (transaction.coreTransaction is WireTransaction) transaction.verifyRequiredSignatures() @@ -335,12 +341,6 @@ open class DBTransactionStorage(private val database: CordaPersistence, cacheFac } } - override fun getTransactionInternal(id: SecureHash): Pair? { - return database.transaction { - txStorage.content[id]?.let { it.toSignedTx() to it.status.toTransactionStatus() } - } - } - private val updatesPublisher = PublishSubject.create().toSerialized() override val updates: Observable = updatesPublisher.wrapWithDatabaseTransaction() diff --git a/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt b/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt index 451bc813a9..2bdfc69e5f 100644 --- a/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt +++ b/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt @@ -17,11 +17,10 @@ import net.corda.core.crypto.SignatureMetadata import net.corda.core.crypto.TransactionSignature import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowSession -import net.corda.core.flows.TransactionMetadata import net.corda.core.flows.InitiatedBy import net.corda.core.flows.InitiatingFlow import net.corda.core.flows.StateMachineRunId -import net.corda.core.flows.TransactionStatus +import net.corda.core.flows.TransactionMetadata import net.corda.core.identity.AbstractParty import net.corda.core.identity.AnonymousParty import net.corda.core.identity.CordaX500Name @@ -31,6 +30,7 @@ import net.corda.core.internal.concurrent.map import net.corda.core.internal.rootCause import net.corda.core.messaging.DataFeed import net.corda.core.messaging.StateMachineTransactionMapping +import net.corda.core.node.services.SignedTransactionWithStatus import net.corda.core.node.services.Vault import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.SingletonSerializeAsToken @@ -44,12 +44,12 @@ import net.corda.core.utilities.toNonEmptySet import net.corda.core.utilities.unwrap import net.corda.coretesting.internal.TEST_TX_TIME import net.corda.finance.DOLLARS -import net.corda.finance.`issued by` import net.corda.finance.contracts.CommercialPaper import net.corda.finance.contracts.asset.CASH import net.corda.finance.contracts.asset.Cash import net.corda.finance.flows.TwoPartyTradeFlow.Buyer import net.corda.finance.flows.TwoPartyTradeFlow.Seller +import net.corda.finance.`issued by` import net.corda.node.services.api.CheckpointStorage import net.corda.node.services.api.WritableTransactionStorage import net.corda.node.services.persistence.DBTransactionStorage @@ -857,12 +857,12 @@ class TwoPartyTradeFlowTests(private val anonymous: Boolean) { } } - override fun getTransactionInternal(id: SecureHash): Pair? { + override fun getTransactionWithStatus(id: SecureHash): SignedTransactionWithStatus? { return database.transaction { - delegate.getTransactionInternal(id) + records.add(TxRecord.Get(id)) + delegate.getTransactionWithStatus(id) } } - } interface TxRecord { diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecoveryTests.kt b/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecoveryTests.kt index c4a98b1093..9cdf700611 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecoveryTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecoveryTests.kt @@ -308,7 +308,7 @@ class DBTransactionStorageLedgerRecoveryTests { assertEquals(true, transactionRecovery.removeUnnotarisedTransaction(senderTransaction.id)) assertFailsWith { readTransactionFromDB(senderTransaction.id).status } assertEquals(0, readSenderDistributionRecordFromDB(senderTransaction.id).size) - assertNull(transactionRecovery.getTransactionInternal(senderTransaction.id)) + assertNull(transactionRecovery.getTransactionWithStatus(senderTransaction.id)) val receiverTransaction = newTransaction(notarySig = false) transactionRecovery.addUnnotarisedTransaction(receiverTransaction) @@ -322,7 +322,7 @@ class DBTransactionStorageLedgerRecoveryTests { assertEquals(true, transactionRecovery.removeUnnotarisedTransaction(receiverTransaction.id)) assertFailsWith { readTransactionFromDB(receiverTransaction.id).status } assertFailsWith { readReceiverDistributionRecordFromDB(receiverTransaction.id) } - assertNull(transactionRecovery.getTransactionInternal(receiverTransaction.id)) + assertNull(transactionRecovery.getTransactionWithStatus(receiverTransaction.id)) } @Test(timeout = 300_000) diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageTests.kt b/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageTests.kt index 0d4b380375..2d54442556 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageTests.kt @@ -170,7 +170,7 @@ class DBTransactionStorageTests { assertEquals(true, transactionStorage.removeUnnotarisedTransaction(transaction.id)) assertFailsWith { readTransactionFromDB(transaction.id).status } - assertNull(transactionStorage.getTransactionInternal(transaction.id)) + assertNull(transactionStorage.getTransactionWithStatus(transaction.id)) } @Test(timeout = 300_000) diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/MockTransactionStorage.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/MockTransactionStorage.kt index 0a52c09f56..d09af76ca2 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/MockTransactionStorage.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/MockTransactionStorage.kt @@ -12,6 +12,7 @@ import net.corda.node.services.api.WritableTransactionStorage import net.corda.core.flows.TransactionMetadata import net.corda.core.flows.TransactionStatus import net.corda.core.identity.CordaX500Name +import net.corda.core.node.services.SignedTransactionWithStatus import net.corda.testing.node.MockServices import rx.Observable import rx.subjects.PublishSubject @@ -86,9 +87,9 @@ open class MockTransactionStorage : WritableTransactionStorage, SingletonSeriali txns.putIfAbsent(transaction.id, TxHolder(transaction, status = TransactionStatus.UNVERIFIED)) } - override fun getTransaction(id: SecureHash): SignedTransaction? = txns[id]?.let { if (it.status == TransactionStatus.VERIFIED) it.stx else null } + override fun getTransaction(id: SecureHash): SignedTransaction? = txns[id]?.let { if (it.isVerified) it.stx else null } - override fun getTransactionInternal(id: SecureHash): Pair? = txns[id]?.let { Pair(it.stx, it.status) } + override fun getTransactionWithStatus(id: SecureHash): SignedTransactionWithStatus? = txns[id]?.let { SignedTransactionWithStatus(it.stx, it.status) } private class TxHolder(val stx: SignedTransaction, var status: TransactionStatus) { val isVerified = status == TransactionStatus.VERIFIED From be433c1fd092f34b3323eb59685ab945f0b59c53 Mon Sep 17 00:00:00 2001 From: Jose Coll Date: Tue, 24 Oct 2023 17:01:37 +0100 Subject: [PATCH 089/126] ENT-10100 Filter out UNVERIFIED txns + move TransactionStatus package. (#7543) --- .ci/api-current.txt | 192 +----------------- .../coretests/flows/FinalityFlowTests.kt | 2 +- .../net/corda/core/flows/RecoveryTypes.kt | 11 +- .../corda/core/flows/SendTransactionFlow.kt | 3 + .../net/corda/core/internal/FetchDataFlow.kt | 5 +- .../core/node/services/TransactionStorage.kt | 8 +- .../node/services/DbTransactionsResolver.kt | 2 +- .../persistence/DBTransactionStorage.kt | 8 +- .../node/internal/MockTransactionStorage.kt | 2 +- 9 files changed, 29 insertions(+), 204 deletions(-) diff --git a/.ci/api-current.txt b/.ci/api-current.txt index a7f5449ff7..79812632d4 100644 --- a/.ci/api-current.txt +++ b/.ci/api-current.txt @@ -1798,8 +1798,6 @@ public final class net.corda.core.crypto.Crypto extends java.lang.Object public static final boolean doVerify(net.corda.core.crypto.SecureHash, net.corda.core.crypto.TransactionSignature) public static final boolean doVerify(net.corda.core.crypto.SignatureScheme, java.security.PublicKey, byte[], byte[]) @NotNull - public static final byte[] encodePublicKey(java.security.PublicKey) - @NotNull public static final java.security.Provider findProvider(String) @NotNull public static final net.corda.core.crypto.SignatureScheme findSignatureScheme(int) @@ -2520,17 +2518,15 @@ public static final class net.corda.core.flows.ContractUpgradeFlow$Initiate exte protected net.corda.core.flows.AbstractStateReplacementFlow$UpgradeTx assembleTx() ## public class net.corda.core.flows.DataVendingFlow extends net.corda.core.flows.FlowLogic - public (java.util.Set, Object, net.corda.core.flows.TransactionMetadata) - public (java.util.Set, Object, net.corda.core.flows.TransactionMetadata, int, kotlin.jvm.internal.DefaultConstructorMarker) public (net.corda.core.flows.FlowSession, Object) - public (net.corda.core.flows.FlowSession, Object, net.corda.core.flows.TransactionMetadata) - public (net.corda.core.flows.FlowSession, Object, net.corda.core.flows.TransactionMetadata, int, kotlin.jvm.internal.DefaultConstructorMarker) @Suspendable @Nullable public Void call() @NotNull public final java.util.Set getOtherSessions() @NotNull + public final net.corda.core.flows.FlowSession getOtherSideSession() + @NotNull public final Object getPayload() @Suspendable @NotNull @@ -2541,61 +2537,10 @@ public class net.corda.core.flows.DataVendingFlow extends net.corda.core.flows.F @DoNotImplement public interface net.corda.core.flows.Destination ## -@CordaSerializable -public abstract class net.corda.core.flows.DistributionList extends java.lang.Object - public (kotlin.jvm.internal.DefaultConstructorMarker) -## -@CordaSerializable -public static final class net.corda.core.flows.DistributionList$ReceiverDistributionList extends net.corda.core.flows.DistributionList - public (byte[], net.corda.core.node.StatesToRecord) - @NotNull - public final byte[] component1() - @NotNull - public final net.corda.core.node.StatesToRecord component2() - @NotNull - public final net.corda.core.flows.DistributionList$ReceiverDistributionList copy(byte[], net.corda.core.node.StatesToRecord) - public boolean equals(Object) - @NotNull - public final byte[] getOpaqueData() - @NotNull - public final net.corda.core.node.StatesToRecord getReceiverStatesToRecord() - public int hashCode() - @NotNull - public String toString() -## -@CordaSerializable -public static final class net.corda.core.flows.DistributionList$SenderDistributionList extends net.corda.core.flows.DistributionList - public (net.corda.core.node.StatesToRecord, java.util.Map) - @NotNull - public final net.corda.core.node.StatesToRecord component1() - @NotNull - public final java.util.Map component2() - @NotNull - public final net.corda.core.flows.DistributionList$SenderDistributionList copy(net.corda.core.node.StatesToRecord, java.util.Map) - public boolean equals(Object) - @NotNull - public final java.util.Map getPeersToStatesToRecord() - @NotNull - public final net.corda.core.node.StatesToRecord getSenderStatesToRecord() - public int hashCode() - @NotNull - public String toString() -@CordaSerializable -public abstract class net.corda.core.flows.DistributionRecord extends java.lang.Object implements net.corda.core.contracts.NamedByHash - public () - @NotNull - public abstract net.corda.core.crypto.SecureHash getPeerPartyId() - @NotNull - public abstract java.time.Instant getTimestamp() - public abstract int getTimestampDiscriminator() - @NotNull - public abstract net.corda.core.crypto.SecureHash getTxId() -## @InitiatingFlow public final class net.corda.core.flows.FinalityFlow extends net.corda.core.flows.FlowLogic public (net.corda.core.transactions.SignedTransaction) public (net.corda.core.transactions.SignedTransaction, java.util.Collection) - public (net.corda.core.transactions.SignedTransaction, java.util.Collection, java.util.Collection) public (net.corda.core.transactions.SignedTransaction, java.util.Collection, java.util.Collection, net.corda.core.utilities.ProgressTracker) public (net.corda.core.transactions.SignedTransaction, java.util.Collection, net.corda.core.node.StatesToRecord) public (net.corda.core.transactions.SignedTransaction, java.util.Collection, net.corda.core.node.StatesToRecord, net.corda.core.utilities.ProgressTracker) @@ -2627,32 +2572,12 @@ public static final class net.corda.core.flows.FinalityFlow$Companion$BROADCASTI public static final net.corda.core.flows.FinalityFlow$Companion$BROADCASTING INSTANCE ## @CordaSerializable -public static final class net.corda.core.flows.FinalityFlow$Companion$BROADCASTING_NOTARY_ERROR extends net.corda.core.utilities.ProgressTracker$Step - public static final net.corda.core.flows.FinalityFlow$Companion$BROADCASTING_NOTARY_ERROR INSTANCE -## -@CordaSerializable -public static final class net.corda.core.flows.FinalityFlow$Companion$BROADCASTING_POST_NOTARISATION extends net.corda.core.utilities.ProgressTracker$Step - public static final net.corda.core.flows.FinalityFlow$Companion$BROADCASTING_POST_NOTARISATION INSTANCE -## -@CordaSerializable -public static final class net.corda.core.flows.FinalityFlow$Companion$BROADCASTING_PRE_NOTARISATION extends net.corda.core.utilities.ProgressTracker$Step - public static final net.corda.core.flows.FinalityFlow$Companion$BROADCASTING_PRE_NOTARISATION INSTANCE -## -@CordaSerializable -public static final class net.corda.core.flows.FinalityFlow$Companion$FINALISING_TRANSACTION extends net.corda.core.utilities.ProgressTracker$Step - public static final net.corda.core.flows.FinalityFlow$Companion$FINALISING_TRANSACTION INSTANCE -## -@CordaSerializable public static final class net.corda.core.flows.FinalityFlow$Companion$NOTARISING extends net.corda.core.utilities.ProgressTracker$Step @NotNull public net.corda.core.utilities.ProgressTracker childProgressTracker() public static final net.corda.core.flows.FinalityFlow$Companion$NOTARISING INSTANCE ## @CordaSerializable -public static final class net.corda.core.flows.FinalityFlow$Companion$RECORD_UNNOTARISED extends net.corda.core.utilities.ProgressTracker$Step - public static final net.corda.core.flows.FinalityFlow$Companion$RECORD_UNNOTARISED INSTANCE -## -@CordaSerializable public class net.corda.core.flows.FlowException extends net.corda.core.CordaException implements net.corda.core.flows.IdentifiableException public () public (String) @@ -2954,37 +2879,6 @@ public static final class net.corda.core.flows.FlowStackSnapshot$Frame extends j public String toString() ## @CordaSerializable -public final class net.corda.core.flows.FlowTransactionInfo extends java.lang.Object - public (net.corda.core.flows.StateMachineRunId, String, net.corda.core.flows.TransactionStatus, java.time.Instant, net.corda.core.flows.TransactionMetadata) - @NotNull - public final net.corda.core.flows.StateMachineRunId component1() - @NotNull - public final String component2() - @NotNull - public final net.corda.core.flows.TransactionStatus component3() - @NotNull - public final java.time.Instant component4() - @Nullable - public final net.corda.core.flows.TransactionMetadata component5() - @NotNull - public final net.corda.core.flows.FlowTransactionInfo copy(net.corda.core.flows.StateMachineRunId, String, net.corda.core.flows.TransactionStatus, java.time.Instant, net.corda.core.flows.TransactionMetadata) - public boolean equals(Object) - @Nullable - public final net.corda.core.flows.TransactionMetadata getMetadata() - @NotNull - public final net.corda.core.flows.StateMachineRunId getStateMachineRunId() - @NotNull - public final net.corda.core.flows.TransactionStatus getStatus() - @NotNull - public final java.time.Instant getTimestamp() - @NotNull - public final String getTxId() - public int hashCode() - public final boolean isInitiator(net.corda.core.identity.CordaX500Name) - @NotNull - public String toString() -## -@CordaSerializable public class net.corda.core.flows.HospitalizeFlowException extends net.corda.core.CordaRuntimeException public () public (String) @@ -3246,10 +3140,6 @@ public static final class net.corda.core.flows.NotaryFlow$Client$Companion$REQUE public static final class net.corda.core.flows.NotaryFlow$Client$Companion$VALIDATING extends net.corda.core.utilities.ProgressTracker$Step public static final net.corda.core.flows.NotaryFlow$Client$Companion$VALIDATING INSTANCE ## -public final class net.corda.core.flows.NotarySigCheck extends java.lang.Object - public final boolean needsNotarySignature(net.corda.core.transactions.SignedTransaction) - public static final net.corda.core.flows.NotarySigCheck INSTANCE -## public final class net.corda.core.flows.ReceiveFinalityFlow extends net.corda.core.flows.FlowLogic public (net.corda.core.flows.FlowSession) public (net.corda.core.flows.FlowSession, net.corda.core.crypto.SecureHash) @@ -3277,41 +3167,6 @@ public class net.corda.core.flows.ReceiveTransactionFlow extends net.corda.core. protected void checkBeforeRecording(net.corda.core.transactions.SignedTransaction) ## @CordaSerializable -public final class net.corda.core.flows.RecoveryTimeWindow extends java.lang.Object - public (java.time.Instant, java.time.Instant) - public (java.time.Instant, java.time.Instant, int, kotlin.jvm.internal.DefaultConstructorMarker) - @NotNull - public static final net.corda.core.flows.RecoveryTimeWindow between(java.time.Instant, java.time.Instant) - @NotNull - public final java.time.Instant component1() - @NotNull - public final java.time.Instant component2() - @NotNull - public final net.corda.core.flows.RecoveryTimeWindow copy(java.time.Instant, java.time.Instant) - public boolean equals(Object) - @NotNull - public static final net.corda.core.flows.RecoveryTimeWindow fromOnly(java.time.Instant) - @NotNull - public final java.time.Instant getFromTime() - @NotNull - public final java.time.Instant getUntilTime() - public int hashCode() - @NotNull - public String toString() - @NotNull - public static final net.corda.core.flows.RecoveryTimeWindow untilOnly(java.time.Instant) - public static final net.corda.core.flows.RecoveryTimeWindow$Companion Companion -## -public static final class net.corda.core.flows.RecoveryTimeWindow$Companion extends java.lang.Object - public (kotlin.jvm.internal.DefaultConstructorMarker) - @NotNull - public final net.corda.core.flows.RecoveryTimeWindow between(java.time.Instant, java.time.Instant) - @NotNull - public final net.corda.core.flows.RecoveryTimeWindow fromOnly(java.time.Instant) - @NotNull - public final net.corda.core.flows.RecoveryTimeWindow untilOnly(java.time.Instant) -## -@CordaSerializable public final class net.corda.core.flows.ResultSerializationException extends net.corda.core.CordaRuntimeException public (net.corda.core.serialization.internal.MissingSerializerException) ## @@ -3322,12 +3177,6 @@ public class net.corda.core.flows.SendStateAndRefFlow extends net.corda.core.flo ## public class net.corda.core.flows.SendTransactionFlow extends net.corda.core.flows.DataVendingFlow public (net.corda.core.flows.FlowSession, net.corda.core.transactions.SignedTransaction) - public static final net.corda.core.flows.SendTransactionFlow$Companion Companion -## -public static final class net.corda.core.flows.SendTransactionFlow$Companion extends java.lang.Object - public (kotlin.jvm.internal.DefaultConstructorMarker) - @NotNull - public final net.corda.core.identity.CordaX500Name getDUMMY_PARTICIPANT_NAME() ## public abstract class net.corda.core.flows.SignTransactionFlow extends net.corda.core.flows.FlowLogic public (net.corda.core.flows.FlowSession) @@ -3435,29 +3284,6 @@ public class net.corda.core.flows.StateReplacementException extends net.corda.co public (String, Throwable, int, kotlin.jvm.internal.DefaultConstructorMarker) ## @CordaSerializable -public final class net.corda.core.flows.TransactionMetadata extends java.lang.Object - public (net.corda.core.identity.CordaX500Name, net.corda.core.flows.DistributionList) - @NotNull - public final net.corda.core.identity.CordaX500Name component1() - @NotNull - public final net.corda.core.flows.DistributionList component2() - @NotNull - public final net.corda.core.flows.TransactionMetadata copy(net.corda.core.identity.CordaX500Name, net.corda.core.flows.DistributionList) - public boolean equals(Object) - @NotNull - public final net.corda.core.flows.DistributionList getDistributionList() - @NotNull - public final net.corda.core.identity.CordaX500Name getInitiator() - public int hashCode() - @NotNull - public String toString() -## -@CordaSerializable -public final class net.corda.core.flows.TransactionStatus extends java.lang.Enum - public static net.corda.core.flows.TransactionStatus valueOf(String) - public static net.corda.core.flows.TransactionStatus[] values() -## -@CordaSerializable public final class net.corda.core.flows.UnexpectedFlowEndException extends net.corda.core.CordaRuntimeException implements net.corda.core.flows.IdentifiableException public (String) public (String, Throwable) @@ -4317,7 +4143,6 @@ public interface net.corda.core.node.ServicesForResolution @NotNull public net.corda.core.transactions.LedgerTransaction specialise(net.corda.core.transactions.LedgerTransaction) ## -@CordaSerializable public final class net.corda.core.node.StatesToRecord extends java.lang.Enum public static net.corda.core.node.StatesToRecord valueOf(String) public static net.corda.core.node.StatesToRecord[] values() @@ -4651,8 +4476,6 @@ public static final class net.corda.core.node.services.Vault$ConstraintInfo$Type @CordaSerializable public static final class net.corda.core.node.services.Vault$Page extends java.lang.Object public (java.util.List>, java.util.List, long, net.corda.core.node.services.Vault$StateStatus, java.util.List) - public (java.util.List>, java.util.List, long, net.corda.core.node.services.Vault$StateStatus, java.util.List, net.corda.core.contracts.StateRef) - public (java.util.List, java.util.List, long, net.corda.core.node.services.Vault$StateStatus, java.util.List, net.corda.core.contracts.StateRef, int, kotlin.jvm.internal.DefaultConstructorMarker) @NotNull public final java.util.List> component1() @NotNull @@ -4662,17 +4485,11 @@ public static final class net.corda.core.node.services.Vault$Page extends java.l public final net.corda.core.node.services.Vault$StateStatus component4() @NotNull public final java.util.List component5() - @Nullable - public final net.corda.core.contracts.StateRef component6() @NotNull public final net.corda.core.node.services.Vault$Page copy(java.util.List>, java.util.List, long, net.corda.core.node.services.Vault$StateStatus, java.util.List) - @NotNull - public final net.corda.core.node.services.Vault$Page copy(java.util.List>, java.util.List, long, net.corda.core.node.services.Vault$StateStatus, java.util.List, net.corda.core.contracts.StateRef) public boolean equals(Object) @NotNull public final java.util.List getOtherResults() - @Nullable - public final net.corda.core.contracts.StateRef getPreviousPageAnchor() @NotNull public final net.corda.core.node.services.Vault$StateStatus getStateTypes() @NotNull @@ -8233,6 +8050,8 @@ public static final class net.corda.testing.core.TestIdentity$Companion extends public final net.corda.testing.core.TestIdentity fresh(String, net.corda.core.crypto.SignatureScheme) ## public final class net.corda.testing.core.TestUtils extends java.lang.Object + @NotNull + public static final java.security.cert.X509CRL createCRL(net.corda.nodeapi.internal.crypto.CertificateAndKeyPair, java.util.List, java.net.URI, java.time.Instant, java.time.Instant, boolean, java.time.Instant, int, String) public static final T executeTest(java.time.Duration, kotlin.jvm.functions.Function0, java.time.Duration, kotlin.jvm.functions.Function0) @NotNull public static final net.corda.core.utilities.NetworkHostAndPort freeLocalHostAndPort() @@ -8907,7 +8726,6 @@ public final class net.corda.testing.flows.FlowTestsUtilsKt extends java.lang.Ob public static final java.util.Map> receiveAll(net.corda.core.flows.FlowLogic, kotlin.Pair>, kotlin.Pair>...) @NotNull public static final rx.Observable registerCoreFlowFactory(net.corda.testing.node.internal.TestStartedNode, Class>, Class, kotlin.jvm.functions.Function1, boolean) - public static final void waitForAllFlowsToComplete(net.corda.testing.driver.NodeHandle, int, long) ## @DoNotImplement public abstract class net.corda.testing.node.ClusterSpec extends java.lang.Object @@ -9329,9 +9147,7 @@ public class net.corda.testing.node.MockServices extends java.lang.Object implem @NotNull public static final kotlin.Pair makeTestDatabaseAndPersistentServices(java.util.List, net.corda.testing.core.TestIdentity, net.corda.core.node.NetworkParameters, java.util.Set, java.util.Set, net.corda.testing.internal.TestingNamedCacheFactory) public void recordTransactions(Iterable) - public final void recordTransactions(Iterable, boolean) public void recordTransactions(net.corda.core.node.StatesToRecord, Iterable) - public final void recordTransactions(net.corda.core.transactions.SignedTransaction, boolean) public void recordTransactions(net.corda.core.transactions.SignedTransaction, net.corda.core.transactions.SignedTransaction...) public void recordTransactions(boolean, Iterable) public void recordTransactions(boolean, net.corda.core.transactions.SignedTransaction, net.corda.core.transactions.SignedTransaction...) diff --git a/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt b/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt index 562cc3a4b4..4eb85ec26b 100644 --- a/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt +++ b/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt @@ -25,7 +25,6 @@ import net.corda.core.flows.ReceiverDistributionRecord import net.corda.core.flows.SendTransactionFlow import net.corda.core.flows.SenderDistributionRecord import net.corda.core.flows.StartableByRPC -import net.corda.core.flows.TransactionStatus import net.corda.core.flows.UnexpectedFlowEndException import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party @@ -34,6 +33,7 @@ import net.corda.core.internal.PLATFORM_VERSION import net.corda.core.internal.PlatformVersionSwitches import net.corda.core.internal.ServiceHubCoreInternal import net.corda.core.node.StatesToRecord +import net.corda.core.node.services.TransactionStatus import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.OpaqueBytes diff --git a/core/src/main/kotlin/net/corda/core/flows/RecoveryTypes.kt b/core/src/main/kotlin/net/corda/core/flows/RecoveryTypes.kt index ae5d3ee4e6..b04de15187 100644 --- a/core/src/main/kotlin/net/corda/core/flows/RecoveryTypes.kt +++ b/core/src/main/kotlin/net/corda/core/flows/RecoveryTypes.kt @@ -1,9 +1,11 @@ package net.corda.core.flows +import net.corda.core.DoNotImplement import net.corda.core.contracts.NamedByHash import net.corda.core.crypto.SecureHash import net.corda.core.identity.CordaX500Name import net.corda.core.node.StatesToRecord +import net.corda.core.node.services.TransactionStatus import net.corda.core.serialization.CordaSerializable import net.corda.core.utilities.OpaqueBytes import java.time.Instant @@ -32,6 +34,7 @@ data class TransactionMetadata( ) @CordaSerializable +@DoNotImplement sealed class DistributionList { @CordaSerializable @@ -47,13 +50,6 @@ sealed class DistributionList { ) : DistributionList() } -@CordaSerializable -enum class TransactionStatus { - UNVERIFIED, - VERIFIED, - IN_FLIGHT; -} - @CordaSerializable class DistributionRecords( val senderRecords: List = emptyList(), @@ -63,6 +59,7 @@ class DistributionRecords( } @CordaSerializable +@DoNotImplement abstract class DistributionRecord : NamedByHash { abstract val txId: SecureHash abstract val peerPartyId: SecureHash diff --git a/core/src/main/kotlin/net/corda/core/flows/SendTransactionFlow.kt b/core/src/main/kotlin/net/corda/core/flows/SendTransactionFlow.kt index 9f57de9580..92811e5965 100644 --- a/core/src/main/kotlin/net/corda/core/flows/SendTransactionFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/SendTransactionFlow.kt @@ -122,6 +122,9 @@ open class DataVendingFlow(val otherSessions: Set, val payload: Any constructor(otherSideSession: FlowSession, payload: Any, txnMetadata: TransactionMetadata? = null) : this(setOf(otherSideSession), payload, txnMetadata) constructor(otherSideSession: FlowSession, payload: Any) : this(otherSideSession, payload, null) + @Deprecated("Use otherSessions: Set", replaceWith = ReplaceWith("otherSessions.single()")) + val otherSideSession: FlowSession get() = otherSessions.single() + @Suspendable protected open fun sendPayloadAndReceiveDataRequest(otherSideSession: FlowSession, payload: Any) = otherSideSession.sendAndReceive(payload) diff --git a/core/src/main/kotlin/net/corda/core/internal/FetchDataFlow.kt b/core/src/main/kotlin/net/corda/core/internal/FetchDataFlow.kt index 1479ab4896..44367159f6 100644 --- a/core/src/main/kotlin/net/corda/core/internal/FetchDataFlow.kt +++ b/core/src/main/kotlin/net/corda/core/internal/FetchDataFlow.kt @@ -13,6 +13,7 @@ import net.corda.core.internal.FetchDataFlow.DownloadedVsRequestedDataMismatch import net.corda.core.internal.FetchDataFlow.HashNotFound import net.corda.core.node.NetworkParameters import net.corda.core.node.services.SignedTransactionWithStatus +import net.corda.core.node.services.TransactionStatus import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.CordaSerializationTransformEnumDefault import net.corda.core.serialization.CordaSerializationTransformEnumDefaults @@ -278,7 +279,9 @@ class FetchTransactionsFlow @JvmOverloads constructor(requests: Set, class FetchRecoverableTransactionsFlow @JvmOverloads constructor(requests: Set, otherSide: FlowSession, dataType: DataType = DataType.TRANSACTION_RECOVERY) : FetchDataFlow(requests, otherSide, dataType) { - override fun load(txid: SecureHash): SignedTransactionWithStatus? = serviceHub.validatedTransactions.getTransactionWithStatus(txid) + override fun load(txid: SecureHash): SignedTransactionWithStatus? = serviceHub.validatedTransactions.getTransactionWithStatus(txid)?.let { + if (it.status != TransactionStatus.UNVERIFIED) it else null + } } class FetchBatchTransactionsFlow(requests: Set, otherSide: FlowSession) : diff --git a/core/src/main/kotlin/net/corda/core/node/services/TransactionStorage.kt b/core/src/main/kotlin/net/corda/core/node/services/TransactionStorage.kt index b4875040e0..a68716a6d3 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/TransactionStorage.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/TransactionStorage.kt @@ -4,7 +4,6 @@ import net.corda.core.DoNotImplement import net.corda.core.concurrent.CordaFuture import net.corda.core.contracts.NamedByHash import net.corda.core.crypto.SecureHash -import net.corda.core.flows.TransactionStatus import net.corda.core.messaging.DataFeed import net.corda.core.serialization.CordaSerializable import net.corda.core.transactions.SignedTransaction @@ -49,4 +48,11 @@ data class SignedTransactionWithStatus( ) : NamedByHash { override val id: SecureHash get() = stx.id +} + +@CordaSerializable +enum class TransactionStatus { + UNVERIFIED, + VERIFIED, + IN_FLIGHT; } \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/services/DbTransactionsResolver.kt b/node/src/main/kotlin/net/corda/node/services/DbTransactionsResolver.kt index e82549d8e3..668b95ff92 100644 --- a/node/src/main/kotlin/net/corda/node/services/DbTransactionsResolver.kt +++ b/node/src/main/kotlin/net/corda/node/services/DbTransactionsResolver.kt @@ -3,12 +3,12 @@ package net.corda.node.services import co.paralleluniverse.fibers.Suspendable import net.corda.core.crypto.SecureHash import net.corda.core.flows.FlowLogic -import net.corda.core.flows.TransactionStatus import net.corda.core.internal.FetchTransactionsFlow import net.corda.core.internal.ResolveTransactionsFlow import net.corda.core.internal.TransactionsResolver import net.corda.core.internal.dependencies import net.corda.core.node.StatesToRecord +import net.corda.core.node.services.TransactionStatus import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.debug import net.corda.core.utilities.seconds diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt index 8577afe934..b66dedb22b 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt @@ -98,11 +98,11 @@ open class DBTransactionStorage(private val database: CordaPersistence, cacheFac return this == VERIFIED } - fun toTransactionStatus(): net.corda.core.flows.TransactionStatus { + fun toTransactionStatus(): net.corda.core.node.services.TransactionStatus { return when(this) { - UNVERIFIED -> net.corda.core.flows.TransactionStatus.UNVERIFIED - VERIFIED -> net.corda.core.flows.TransactionStatus.VERIFIED - IN_FLIGHT -> net.corda.core.flows.TransactionStatus.IN_FLIGHT + UNVERIFIED -> net.corda.core.node.services.TransactionStatus.UNVERIFIED + VERIFIED -> net.corda.core.node.services.TransactionStatus.VERIFIED + IN_FLIGHT -> net.corda.core.node.services.TransactionStatus.IN_FLIGHT } } diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/MockTransactionStorage.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/MockTransactionStorage.kt index d09af76ca2..ee4a4a91f5 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/MockTransactionStorage.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/MockTransactionStorage.kt @@ -10,9 +10,9 @@ import net.corda.core.toFuture import net.corda.core.transactions.SignedTransaction import net.corda.node.services.api.WritableTransactionStorage import net.corda.core.flows.TransactionMetadata -import net.corda.core.flows.TransactionStatus import net.corda.core.identity.CordaX500Name import net.corda.core.node.services.SignedTransactionWithStatus +import net.corda.core.node.services.TransactionStatus import net.corda.testing.node.MockServices import rx.Observable import rx.subjects.PublishSubject From be515abd0892ea3ef08db1b38881d14e9fa62d37 Mon Sep 17 00:00:00 2001 From: Jose Coll Date: Tue, 24 Oct 2023 17:02:17 +0100 Subject: [PATCH 090/126] ENT-11004 Store Ledger Recovery records only if the transaction was locally stored in the first place. (#7544) --- .../main/kotlin/net/corda/core/flows/SendTransactionFlow.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/core/src/main/kotlin/net/corda/core/flows/SendTransactionFlow.kt b/core/src/main/kotlin/net/corda/core/flows/SendTransactionFlow.kt index 92811e5965..93a0f59852 100644 --- a/core/src/main/kotlin/net/corda/core/flows/SendTransactionFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/SendTransactionFlow.kt @@ -90,7 +90,7 @@ open class SendTransactionFlow(val stx: SignedTransaction, val DUMMY_PARTICIPANT_NAME = CordaX500Name("Transaction Participant", "London", "GB") fun makeMetaData(stx: SignedTransaction, recordMetaDataEvenIfNotFullySigned: Boolean, senderStatesToRecord: StatesToRecord, participantSessions: Set, observerSessions: Set): TransactionMetadata? { - return if (recordMetaDataEvenIfNotFullySigned || isFullySigned(stx)) + return if (recordMetaDataEvenIfNotFullySigned || isFullySignedAndStoredLocally(stx)) TransactionMetadata(DUMMY_PARTICIPANT_NAME, SenderDistributionList(senderStatesToRecord, (participantSessions.map { it.counterparty.name to StatesToRecord.ONLY_RELEVANT }).toMap() + @@ -104,6 +104,9 @@ open class SendTransactionFlow(val stx: SignedTransaction, stx.resolveTransactionWithSignatures(serviceHub).getMissingSigners().isEmpty() else false } + + private fun isFullySignedAndStoredLocally(stx: SignedTransaction) = isFullySigned(stx) + && (currentTopLevel?.serviceHub?.validatedTransactions?.getTransaction(stx.id) != null) } } From e52f086d1179b56bd2c505e6cff04842ade9a420 Mon Sep 17 00:00:00 2001 From: Jose Coll Date: Thu, 26 Oct 2023 09:38:52 +0100 Subject: [PATCH 091/126] ENT-11036 Additional parameter to enable recovery of IN_FLIGHT transactions (post ledger peer recovery) (#7546) --- .../src/main/kotlin/net/corda/core/flows/LedgerRecoverFlow.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/src/main/kotlin/net/corda/core/flows/LedgerRecoverFlow.kt b/core/src/main/kotlin/net/corda/core/flows/LedgerRecoverFlow.kt index f416fe1455..5153f98e91 100644 --- a/core/src/main/kotlin/net/corda/core/flows/LedgerRecoverFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/LedgerRecoverFlow.kt @@ -37,12 +37,14 @@ data class LedgerRecoveryParameters( val dryRun: Boolean = false, val useTimeWindowNarrowing: Boolean = true, val verboseLogging: Boolean = false, - val recoveryBatchSize: Int = 1000 + val recoveryBatchSize: Int = 1000, + val alsoFinalize: Boolean = false ) @CordaSerializable data class LedgerRecoveryResult( val totalRecoveredRecords: Long, val totalRecoveredTransactions: Long, + val totalRecoveredInFlightTransactions: Long, val totalErrors: Long ) From c626d3a43562f721c9a38ab796d21930def8ab93 Mon Sep 17 00:00:00 2001 From: Rick Parker Date: Fri, 27 Oct 2023 13:26:07 +0100 Subject: [PATCH 092/126] ENT-10100: fix batch support during recovery of in flight transactions (#7549) --- .../corda/core/flows/SendTransactionFlow.kt | 19 +++++++++--- .../net/corda/core/internal/FetchDataFlow.kt | 27 +++++++----------- .../internal/amqp/EvolvabilityTests.kt | 17 +++++++++++ ...olvabilityTests.maybeSerializedTransaction | Bin 0 -> 4019 bytes 4 files changed, 42 insertions(+), 21 deletions(-) create mode 100644 serialization/src/test/resources/net/corda/serialization/internal/amqp/EvolvabilityTests.maybeSerializedTransaction diff --git a/core/src/main/kotlin/net/corda/core/flows/SendTransactionFlow.kt b/core/src/main/kotlin/net/corda/core/flows/SendTransactionFlow.kt index 93a0f59852..173107dd5f 100644 --- a/core/src/main/kotlin/net/corda/core/flows/SendTransactionFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/SendTransactionFlow.kt @@ -4,11 +4,18 @@ import co.paralleluniverse.fibers.Suspendable import net.corda.core.contracts.NamedByHash import net.corda.core.contracts.StateAndRef import net.corda.core.crypto.SecureHash +import net.corda.core.flows.DistributionList.SenderDistributionList import net.corda.core.identity.CordaX500Name -import net.corda.core.internal.* +import net.corda.core.internal.FetchDataFlow +import net.corda.core.internal.NetworkParametersStorage +import net.corda.core.internal.PlatformVersionSwitches +import net.corda.core.internal.RetrieveAnyTransactionPayload +import net.corda.core.internal.ServiceHubCoreInternal +import net.corda.core.internal.readFully import net.corda.core.node.ServicesForResolution import net.corda.core.node.StatesToRecord import net.corda.core.serialization.CordaSerializable +import net.corda.core.serialization.DeprecatedConstructorForDeserialization import net.corda.core.serialization.SerializedBytes import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize @@ -16,7 +23,6 @@ import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.trace import net.corda.core.utilities.unwrap import kotlin.collections.toSet -import net.corda.core.flows.DistributionList.SenderDistributionList /** * In the words of Matt working code is more important then pretty code. This class that contains code that may @@ -27,7 +33,12 @@ import net.corda.core.flows.DistributionList.SenderDistributionList */ @CordaSerializable class MaybeSerializedSignedTransaction(override val id: SecureHash, val serialized: SerializedBytes?, - val nonSerialised: SignedTransaction?) : NamedByHash { + val nonSerialised: SignedTransaction?, + val inFlight: Boolean) : NamedByHash { + + @DeprecatedConstructorForDeserialization(version = 1) + constructor(id: SecureHash, serialized: SerializedBytes?, nonSerialised: SignedTransaction?) : this(id, serialized, nonSerialised, false) + init { check(serialized == null || nonSerialised == null) { "MaybeSerializedSignedTransaction: Serialized and non-serialized may not both be non-null." @@ -227,7 +238,7 @@ open class DataVendingFlow(val otherSessions: Set, val payload: Any numSent++ tx } - FetchDataFlow.DataType.TRANSACTION_RECOVERY -> NotImplementedError("Enterprise only feature") + FetchDataFlow.DataType.TRANSACTION_RECOVERY -> throw NotImplementedError("Enterprise only feature") // Loop on all items returned using dataRequest.hashes.map: FetchDataFlow.DataType.BATCH_TRANSACTION -> dataRequest.hashes.map { txId -> if (!authorisedTransactions.isAuthorised(txId)) { diff --git a/core/src/main/kotlin/net/corda/core/internal/FetchDataFlow.kt b/core/src/main/kotlin/net/corda/core/internal/FetchDataFlow.kt index 44367159f6..07564bcbf4 100644 --- a/core/src/main/kotlin/net/corda/core/internal/FetchDataFlow.kt +++ b/core/src/main/kotlin/net/corda/core/internal/FetchDataFlow.kt @@ -12,7 +12,6 @@ import net.corda.core.flows.MaybeSerializedSignedTransaction import net.corda.core.internal.FetchDataFlow.DownloadedVsRequestedDataMismatch import net.corda.core.internal.FetchDataFlow.HashNotFound import net.corda.core.node.NetworkParameters -import net.corda.core.node.services.SignedTransactionWithStatus import net.corda.core.node.services.TransactionStatus import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.CordaSerializationTransformEnumDefault @@ -24,10 +23,11 @@ import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.NonEmptySet import net.corda.core.utilities.UntrustworthyData import net.corda.core.utilities.debug -import net.corda.core.utilities.unwrap import net.corda.core.utilities.trace +import net.corda.core.utilities.unwrap import java.nio.file.FileAlreadyExistsException -import java.util.* +import java.util.ArrayList +import java.util.LinkedHashSet /** * An abstract flow for fetching typed data from a remote peer. @@ -275,24 +275,17 @@ class FetchTransactionsFlow @JvmOverloads constructor(requests: Set, override fun load(txid: SecureHash): SignedTransaction? = serviceHub.validatedTransactions.getTransaction(txid) } -// Used by Enterprise Ledger Recovery -class FetchRecoverableTransactionsFlow @JvmOverloads constructor(requests: Set, otherSide: FlowSession, dataType: DataType = DataType.TRANSACTION_RECOVERY) : - FetchDataFlow(requests, otherSide, dataType) { - - override fun load(txid: SecureHash): SignedTransactionWithStatus? = serviceHub.validatedTransactions.getTransactionWithStatus(txid)?.let { - if (it.status != TransactionStatus.UNVERIFIED) it else null - } -} - -class FetchBatchTransactionsFlow(requests: Set, otherSide: FlowSession) : - FetchDataFlow(requests, otherSide, DataType.BATCH_TRANSACTION) { +class FetchBatchTransactionsFlow(requests: Set, otherSide: FlowSession, private val recoveryMode: Boolean = false) : + FetchDataFlow(requests, otherSide, + if (recoveryMode) DataType.TRANSACTION_RECOVERY else DataType.BATCH_TRANSACTION) { override fun load(txid: SecureHash): MaybeSerializedSignedTransaction? { - val tran = serviceHub.validatedTransactions.getTransaction(txid) - return if (tran == null) { + val tranAndStatus = serviceHub.validatedTransactions.getTransactionWithStatus(txid) + @Suppress("ComplexCondition") + return if (tranAndStatus == null || tranAndStatus.status == TransactionStatus.UNVERIFIED || (!recoveryMode && tranAndStatus.status == TransactionStatus.IN_FLIGHT)) { null } else { - MaybeSerializedSignedTransaction(txid, null, tran) + MaybeSerializedSignedTransaction(txid, null, tranAndStatus.stx, tranAndStatus.status == TransactionStatus.IN_FLIGHT) } } } diff --git a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/EvolvabilityTests.kt b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/EvolvabilityTests.kt index 9514a65626..0c2ecdb8c9 100644 --- a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/EvolvabilityTests.kt +++ b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/EvolvabilityTests.kt @@ -4,6 +4,7 @@ import net.corda.core.crypto.Crypto import net.corda.core.crypto.Crypto.generateKeyPair import net.corda.core.crypto.SignedData import net.corda.core.crypto.sign +import net.corda.core.flows.MaybeSerializedSignedTransaction import net.corda.core.flows.NotarisationRequest import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party @@ -21,6 +22,7 @@ import net.corda.serialization.internal.amqp.testutils.serialize import net.corda.serialization.internal.amqp.testutils.serializeAndReturnSchema import net.corda.serialization.internal.amqp.testutils.testDefaultFactory import net.corda.serialization.internal.amqp.testutils.testName +import org.assertj.core.api.Assertions.assertThat import org.junit.Ignore import org.junit.Test import org.junit.jupiter.api.Assertions.assertNotSame @@ -921,4 +923,19 @@ class EvolvabilityTests { assertNotSame(deserialized2.statesToConsume[0].txhash, deserialized2.statesToConsume[1].txhash) assertNotSame(deserialized2.statesToConsume[2].txhash, deserialized2.statesToConsume[3].txhash) } + + @Test(timeout = 300_000) + fun maybeSerializedTransaction() { + val sf = testDefaultFactory() + val resource = "EvolvabilityTests.maybeSerializedTransaction" + + //val A = MaybeSerializedSignedTransaction(SecureHash.randomSHA256(), null, null) + //File(URI("$localPath/$resource")).writeBytes(SerializationOutput(sf).serialize(A).bytes) + + val url = EvolvabilityTests::class.java.getResource(resource) + val sc2 = url.readBytes() + val deserializedA = DeserializationInput(sf).deserialize(SerializedBytes(sc2)) + + assertThat(deserializedA).isInstanceOf(MaybeSerializedSignedTransaction::class.java) + } } diff --git a/serialization/src/test/resources/net/corda/serialization/internal/amqp/EvolvabilityTests.maybeSerializedTransaction b/serialization/src/test/resources/net/corda/serialization/internal/amqp/EvolvabilityTests.maybeSerializedTransaction new file mode 100644 index 0000000000000000000000000000000000000000..8da04d34d38ad943ec2ae4a9a91db77f63b76a6c GIT binary patch literal 4019 zcmc&%&2Jk;6!$vbm?F`tq9{@&P!$42F*tD&J5C{yKjOrWIDh@10*EQNFXHO0=sU!wbyp)v{+m$Yv=8IzxR8; z_uecoD`mjgY_<;{a{8~$#{6irUHL-)GiSTIC_(Jz4U1a|DV`~KVwpWY+|1R>UX&GF zt<3f|O$^(ql~PLfX12IeIGaNJCdh~L5yS3v=F1zuCcb^+&r1*Q{q^Yn*X+XLcfkAc zPuH&B`C0htQ|*@zgwF#36NcS@@xmFN%m3(qu9|18e#YF@AF;%1A~bM9cSV#t$ejQh z2XjynfGBifIVV&lSk5azLV(ADECuLIhAdfLEGPJvqlQq>)n8%?0Q6QPxi zHWowy3lO;tu5z%|h7u2V%y?GyWTckj4~Mh3)$*u~?x`FTg?$4e<>t(n9P||@VL5|* zGS3MTP&BJRbc1=hQb7=tIYGh^!mJJ}=w&ByAiW319<%ft_PO$#9YLE7DbmaVX{%c} zY5P5L(RW*wI>R+II_TT?)wo;{L-m{ij^&VTM*pm)ewv_W6 zQOvGscUSyMC)4o-xnjF^eCW^Aa;0cb-oH7_JRS5ceH(I`A@1u8S?Gxy87ol0nmgU* zM1hY(ZAe5@Ou~?pT-Ap&)>nQ4Dkq{YNeIc!@wB9^hQr&+K@D%H-}MQIK^b5$$(z}3 zeM4&pe8NsnlZNLe9+IoW6l4})_wuw4nHNCgqc7;GRn+z2b z)f0<|C}zh53eaWyn8z24&ld}l+~8aXLOt75HkyZP(ynSKVsI6g+3{Wjbb%RvGBID? zvtEvFcH`}%`M)yEe)oE8|kDg?Jx8cb1?!QOxCaY>9v%L2G_j4*(EdS!#F@M KlFX$CjO}0SepMy_ literal 0 HcmV?d00001 From 04b1e8fb3db2a1f18932c3999dbb53be3b4fad02 Mon Sep 17 00:00:00 2001 From: Jose Coll Date: Fri, 27 Oct 2023 13:45:19 +0100 Subject: [PATCH 093/126] Add "forceRecoverFlowIds" parameter to FinalityRecoveryFlow --- .../corda/core/flows/FinalityRecoveryFlow.kt | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/flows/FinalityRecoveryFlow.kt b/core/src/main/kotlin/net/corda/core/flows/FinalityRecoveryFlow.kt index e31f5b4fa8..02acd40a8e 100644 --- a/core/src/main/kotlin/net/corda/core/flows/FinalityRecoveryFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/FinalityRecoveryFlow.kt @@ -21,6 +21,7 @@ class FinalityRecoveryFlow( private val matchingCriteria: FlowRecoveryQuery? = null, private val forceRecover: Boolean = false, private val recoverAll: Boolean = false, + private val forceRecoverFlowIds: Collection = emptySet(), override val progressTracker: ProgressTracker = ProgressTracker()) : FlowLogic>() { @CordaInternal @@ -28,16 +29,17 @@ class FinalityRecoveryFlow( val flowIds: Collection, val matchingCriteria: FlowRecoveryQuery?, val forceRecover: Boolean, - val recoverAll: Boolean) + val recoverAll: Boolean, + val forceRecoverFlowIds: Collection) @CordaInternal - fun getExtraConstructorArgs() = ExtraConstructorArgs(txIds, flowIds, matchingCriteria, forceRecover, recoverAll) + fun getExtraConstructorArgs() = ExtraConstructorArgs(txIds, flowIds, matchingCriteria, forceRecover, recoverAll, forceRecoverFlowIds) constructor(txId: SecureHash, forceRecover: Boolean = false) : this(setOf(txId), forceRecover) - constructor(txIds: Collection, forceRecover: Boolean = false, recoverAll: Boolean = false) : this(txIds, emptySet(), null, forceRecover, recoverAll, tracker()) + constructor(txIds: Collection, forceRecover: Boolean = false, recoverAll: Boolean = false) : this(txIds, emptySet(), null, forceRecover, recoverAll, emptySet(), tracker()) constructor(flowId: StateMachineRunId, forceRecover: Boolean = false) : this(emptySet(), setOf(flowId), null, forceRecover) - constructor(flowIds: Collection, forceRecover: Boolean = false) : this(emptySet(), flowIds, null, forceRecover, false, tracker()) - constructor(recoverAll: Boolean, forceRecover: Boolean = false) : this(emptySet(), emptySet(), null, forceRecover, recoverAll, tracker()) - constructor(matchingCriteria: FlowRecoveryQuery, forceRecover: Boolean = false) : this(emptySet(), emptySet(), matchingCriteria, forceRecover, false, tracker()) + constructor(flowIds: Collection, forceRecover: Boolean = false) : this(emptySet(), flowIds, null, forceRecover, false, emptySet(), tracker()) + constructor(recoverAll: Boolean, forceRecover: Boolean = false) : this(emptySet(), emptySet(), null, forceRecover, recoverAll, emptySet(), tracker()) + constructor(matchingCriteria: FlowRecoveryQuery, forceRecover: Boolean = false) : this(emptySet(), emptySet(), matchingCriteria, forceRecover, false, emptySet(), tracker()) @Suspendable @Throws(FlowRecoveryException::class) @@ -55,9 +57,10 @@ class FlowRecoveryException(message: String, cause: Throwable? = null) : FlowExc data class FlowRecoveryQuery( val timeframe: FlowTimeWindow? = null, val initiatedBy: CordaX500Name? = null, - val counterParties: List? = null) { + val counterParties: List? = null, + val excludeFlowIds: List? = null) { init { - require(timeframe != null || initiatedBy != null || counterParties != null) { + require(timeframe != null || initiatedBy != null || counterParties != null || excludeFlowIds != null) { "Must specify at least one recovery criteria" } } From 5e017dfbc8af467b943dc559738985bbe1c1e09c Mon Sep 17 00:00:00 2001 From: Jose Coll Date: Fri, 27 Oct 2023 14:13:29 +0100 Subject: [PATCH 094/126] Drop FlowRecoveryQuery criteria: excludeFlowIds --- .../main/kotlin/net/corda/core/flows/FinalityRecoveryFlow.kt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/flows/FinalityRecoveryFlow.kt b/core/src/main/kotlin/net/corda/core/flows/FinalityRecoveryFlow.kt index 02acd40a8e..2e0adf9eb2 100644 --- a/core/src/main/kotlin/net/corda/core/flows/FinalityRecoveryFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/FinalityRecoveryFlow.kt @@ -57,10 +57,9 @@ class FlowRecoveryException(message: String, cause: Throwable? = null) : FlowExc data class FlowRecoveryQuery( val timeframe: FlowTimeWindow? = null, val initiatedBy: CordaX500Name? = null, - val counterParties: List? = null, - val excludeFlowIds: List? = null) { + val counterParties: List? = null) { init { - require(timeframe != null || initiatedBy != null || counterParties != null || excludeFlowIds != null) { + require(timeframe != null || initiatedBy != null || counterParties != null) { "Must specify at least one recovery criteria" } } From 7f867a0a4969017f069de0c4870a9be95d0e1291 Mon Sep 17 00:00:00 2001 From: Chris Cochrane <78791827+chriscochrane@users.noreply.github.com> Date: Mon, 30 Oct 2023 15:41:21 +0000 Subject: [PATCH 095/126] ENT-11038 - Redirect Liquibase logging via Slf4j (#7552) --- .../persistence/Slf4jLiquibaseLogService.kt | 13 ++++ .../persistence/Slf4jLiquibaseLogger.kt | 74 +++++++++++++++++++ .../services/liquibase.logging.LogService | 1 + 3 files changed, 88 insertions(+) create mode 100644 node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/Slf4jLiquibaseLogService.kt create mode 100644 node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/Slf4jLiquibaseLogger.kt create mode 100644 node-api/src/main/resources/META-INF/services/liquibase.logging.LogService diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/Slf4jLiquibaseLogService.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/Slf4jLiquibaseLogService.kt new file mode 100644 index 0000000000..1ec830d0e5 --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/Slf4jLiquibaseLogService.kt @@ -0,0 +1,13 @@ +package net.corda.nodeapi.internal.persistence + +import liquibase.logging.core.AbstractLogService +import org.slf4j.LoggerFactory + +class Slf4jLiquibaseLogService : AbstractLogService() { + + override fun getPriority() = Integer.MAX_VALUE + override fun getLog(clazz: Class<*>?) : liquibase.logging.Logger { + return Slf4jLiquibaseLogger(LoggerFactory.getLogger(clazz)) + } + +} diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/Slf4jLiquibaseLogger.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/Slf4jLiquibaseLogger.kt new file mode 100644 index 0000000000..d9afda1e38 --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/Slf4jLiquibaseLogger.kt @@ -0,0 +1,74 @@ +package net.corda.nodeapi.internal.persistence + +import org.slf4j.Logger +import java.util.logging.Level + +/** + * Slf4j logger for Liquibase + * Captures logging from Liquibase that would otherwise go to stderr. + */ +class Slf4jLiquibaseLogger(private val logger: Logger) : liquibase.logging.core.AbstractLogger() { + + companion object { + private val TRACE_THRESHOLD = Level.FINEST.intValue() + private val DEBUG_THRESHOLD = Level.FINE.intValue() + private val INFO_THRESHOLD = Level.INFO.intValue() + private val WARN_THRESHOLD = Level.WARNING.intValue() + } + + override fun log(level: Level, message: String?, e: Throwable?) { + val levelValue = level.intValue() + if (levelValue <= TRACE_THRESHOLD) { + logger.trace(message, e) + } else if (levelValue <= DEBUG_THRESHOLD) { + logger.debug(message, e) + } else if (levelValue <= INFO_THRESHOLD) { + logger.info(message, e) + } else if (levelValue <= WARN_THRESHOLD) { + logger.warn(message, e) + } else { + logger.error(message, e) + } + } + + override fun severe(message: String?) { + logger.error(message) + } + + override fun severe(message: String?, e: Throwable?) { + logger.error(message, e) + } + + override fun warning(message: String?) { + logger.warn(message) + } + + override fun warning(message: String?, e: Throwable?) { + logger.warn(message, e) + } + + override fun info(message: String?) { + logger.info(message) + } + + override fun info(message: String?, e: Throwable?) { + logger.info(message, e) + } + + override fun config(message: String?) { + logger.info(message) + } + + override fun config(message: String?, e: Throwable?) { + logger.info(message, e) + } + + override fun fine(message: String?) { + logger.debug(message) + } + + override fun fine(message: String?, e: Throwable?) { + logger.debug(message, e) + } + +} \ No newline at end of file diff --git a/node-api/src/main/resources/META-INF/services/liquibase.logging.LogService b/node-api/src/main/resources/META-INF/services/liquibase.logging.LogService new file mode 100644 index 0000000000..74002d445f --- /dev/null +++ b/node-api/src/main/resources/META-INF/services/liquibase.logging.LogService @@ -0,0 +1 @@ +net.corda.nodeapi.internal.persistence.Slf4jLiquibaseLogService \ No newline at end of file From 60bcc991549bc63d8028e68b70334d1f49e2f6b1 Mon Sep 17 00:00:00 2001 From: Jose Coll Date: Tue, 31 Oct 2023 15:55:55 +0000 Subject: [PATCH 096/126] ENT-11037 Expose and use additional Ledger Recovery statistics (#7553) --- .../net/corda/core/internal/ResolveTransactionsFlow.kt | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/core/src/main/kotlin/net/corda/core/internal/ResolveTransactionsFlow.kt b/core/src/main/kotlin/net/corda/core/internal/ResolveTransactionsFlow.kt index 7b8f248667..3896d5648c 100644 --- a/core/src/main/kotlin/net/corda/core/internal/ResolveTransactionsFlow.kt +++ b/core/src/main/kotlin/net/corda/core/internal/ResolveTransactionsFlow.kt @@ -46,6 +46,8 @@ class ResolveTransactionsFlow private constructor( : this(transaction, transaction.dependencies, otherSide, statesToRecord, deferredAck) private var fetchNetParamsFromCounterpart = false + // statistics set in DbResolver (Enterprise only) after exiting recordDependencies() + var statistics: ResolveTransactionsFlowStats = ResolveTransactionsFlowStats() @Suppress("MagicNumber") @Suspendable @@ -120,3 +122,10 @@ class ResolveTransactionsFlow private constructor( } } } + +data class ResolveTransactionsFlowStats( + val resolvableTransactionHashesCount: Long = 0, + val downloadedDependencyCount: Long = 0, + val recordedTransactionCount: Long = 0, + val skippedRecordingTransactionCount: Long = 0 +) From 5c32d3dec4519d949e8206be2613902ded692f33 Mon Sep 17 00:00:00 2001 From: Jose Coll Date: Thu, 2 Nov 2023 17:46:29 +0000 Subject: [PATCH 097/126] ENT-11073 Checkpoint agent: update java assist version. (#7554) --- build.gradle | 2 ++ constants.properties | 1 + experimental/quasar-hook/build.gradle | 6 +----- tools/checkpoint-agent/build.gradle | 27 +-------------------------- 4 files changed, 5 insertions(+), 31 deletions(-) diff --git a/build.gradle b/build.gradle index ac0995e4ea..aeccb9ada0 100644 --- a/build.gradle +++ b/build.gradle @@ -124,6 +124,8 @@ buildscript { ext.commons_configuration2_version = constants.getProperty("commonsConfiguration2Version") ext.commons_text_version = constants.getProperty("commonsTextVersion") ext.snake_yaml_version = constants.getProperty("snakeYamlVersion") + ext.javaassist_version = constants.getProperty("javaassistVersion") + if (JavaVersion.current().isJava8()) { ext.fontawesomefx_commons_version = constants.getProperty("fontawesomefxCommonsJava8Version") ext.fontawesomefx_fontawesome_version = constants.getProperty("fontawesomefxFontawesomeJava8Version") diff --git a/constants.properties b/constants.properties index 6c78be4da5..5c01bc82b7 100644 --- a/constants.properties +++ b/constants.properties @@ -106,3 +106,4 @@ fontawesomefxFontawesomeJava8Version=4.7.0-5 # FontAwesomeFX for a more recent version of the Java Runtime (class file version 55.0) fontawesomefxCommonsVersion=11.0 fontawesomefxFontawesomeVersion=4.7.0-11 +javaassistVersion=3.27.0-GA diff --git a/experimental/quasar-hook/build.gradle b/experimental/quasar-hook/build.gradle index dfe49fa970..df09b426d7 100644 --- a/experimental/quasar-hook/build.gradle +++ b/experimental/quasar-hook/build.gradle @@ -1,7 +1,3 @@ -ext { - javaassist_version = "3.12.1.GA" -} - apply plugin: 'kotlin' apply plugin: 'idea' @@ -10,7 +6,7 @@ description 'A javaagent to allow hooking into the instrumentation by Quasar' dependencies { compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" - compile "javassist:javassist:$javaassist_version" + compile "org.javassist:javassist:$javaassist_version" } jar { diff --git a/tools/checkpoint-agent/build.gradle b/tools/checkpoint-agent/build.gradle index 228a6de595..838d70c652 100644 --- a/tools/checkpoint-agent/build.gradle +++ b/tools/checkpoint-agent/build.gradle @@ -1,28 +1,3 @@ -buildscript { - // For sharing constants between builds - Properties constants = new Properties() - file("$projectDir/../../constants.properties").withInputStream { constants.load(it) } - - ext.kotlin_version = constants.getProperty("kotlinVersion") - ext.javaassist_version = "3.12.1.GA" - - repositories { - mavenLocal() - mavenCentral() - jcenter() - } - - dependencies { - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - } -} - -repositories { - mavenLocal() - mavenCentral() - jcenter() -} - apply plugin: 'kotlin' apply plugin: 'idea' apply plugin: 'net.corda.plugins.publish-utils' @@ -33,7 +8,7 @@ description 'A javaagent to allow hooking into Kryo checkpoints' dependencies { compileOnly "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" compileOnly "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" - compileOnly "javassist:javassist:$javaassist_version" + compileOnly "org.javassist:javassist:$javaassist_version" compileOnly "com.esotericsoftware:kryo:$kryo_version" compileOnly "co.paralleluniverse:quasar-core:$quasar_version" From 361cd678cbae3a0c496eb992f4bebbf4382db51e Mon Sep 17 00:00:00 2001 From: Jose Coll Date: Thu, 2 Nov 2023 18:31:43 +0000 Subject: [PATCH 098/126] ENT-10531 Add additional index on distribution records tables. Sync table names. (#7548) --- .../migration/node-core.changelog-v25.xml | 28 +++++++++++++++---- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/node/src/main/resources/migration/node-core.changelog-v25.xml b/node/src/main/resources/migration/node-core.changelog-v25.xml index bc31223fe7..dee14a6fa2 100644 --- a/node/src/main/resources/migration/node-core.changelog-v25.xml +++ b/node/src/main/resources/migration/node-core.changelog-v25.xml @@ -10,7 +10,7 @@ - + @@ -34,11 +34,19 @@ - - + + + + + + + + + @@ -62,10 +70,18 @@ - + + + + + + + + @@ -81,13 +97,13 @@ - + - + From 632a7481db6bc9fc41033a6578ad2c062bdf0791 Mon Sep 17 00:00:00 2001 From: Balwant Kothari Date: Fri, 3 Nov 2023 19:14:49 +0530 Subject: [PATCH 099/126] Bumping up H2 version 2.2.224 --- constants.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/constants.properties b/constants.properties index 5c01bc82b7..5b577af652 100644 --- a/constants.properties +++ b/constants.properties @@ -78,7 +78,7 @@ joptSimpleVersion=5.0.2 jansiVersion=1.18 hibernateVersion=5.6.14.Final # h2Version - Update docs if renamed or removed. -h2Version=2.1.214 +h2Version=2.2.224 rxjavaVersion=1.3.8 dokkaVersion=0.10.1 eddsaVersion=0.3.0 From 702842caeb42857abf4b721b49cbc6816299dcce Mon Sep 17 00:00:00 2001 From: Adel El-Beik <48713346+adelel1@users.noreply.github.com> Date: Fri, 3 Nov 2023 15:43:51 +0000 Subject: [PATCH 100/126] ENT-9940: Updated quasar to 0.7.16_r3 (#7559) --- constants.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/constants.properties b/constants.properties index 5c01bc82b7..e0f4a6f88f 100644 --- a/constants.properties +++ b/constants.properties @@ -17,7 +17,7 @@ openTelemetryVersion=1.20.1 openTelemetrySemConvVersion=1.20.1-alpha guavaVersion=28.0-jre # Quasar version to use with Java 8: -quasarVersion=0.7.15_r3 +quasarVersion=0.7.16_r3 # Quasar version to use with Java 11: quasarVersion11=0.8.1_r3 jdkClassifier11=jdk11 From 3c34caabc3fe7beb476550dde0dff2f3b85d9a56 Mon Sep 17 00:00:00 2001 From: Chris Cochrane <78791827+chriscochrane@users.noreply.github.com> Date: Tue, 7 Nov 2023 10:46:03 +0000 Subject: [PATCH 101/126] ENT-10888,ENT-11108 - Security vulnerabilties (#7556) * Security vulnerabilties * Upgraded netty * ENT-11108: Upgraded tcnative depedency to 2.2.224. * Added architecture classifiers for netty-tcnative --- constants.properties | 6 +++--- node/build.gradle | 7 ++++++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/constants.properties b/constants.properties index 9c47bda8d8..c62bc72c80 100644 --- a/constants.properties +++ b/constants.properties @@ -38,7 +38,7 @@ openSourceBranch=https://github.com/corda/corda/blob/release/os/4.4 openSourceSamplesBranch=https://github.com/corda/samples/blob/release-V4 jolokiaAgentVersion=1.6.1 detektVersion=1.0.1 -tcnativeVersion=2.0.48.Final +tcnativeVersion=2.0.62.Final commonsConfiguration2Version=2.8.0 commonsTextVersion=1.10.0 @@ -52,14 +52,14 @@ artemisVersion=2.19.1 # TODO Upgrade Jackson only when corda is using kotlin 1.3.10 jacksonVersion=2.13.5 jacksonKotlinVersion=2.9.7 -jettyVersion=9.4.52.v20230823 +jettyVersion=9.4.53.v20231009 jerseyVersion=2.25 servletVersion=4.0.1 assertjVersion=3.12.2 slf4JVersion=1.7.30 log4JVersion=2.17.1 okhttpVersion=3.14.9 -nettyVersion=4.1.77.Final +nettyVersion=4.1.100.Final fileuploadVersion=1.4 kryoVersion=4.0.2 kryoSerializerVersion=0.43 diff --git a/node/build.gradle b/node/build.gradle index 81418360f7..e16db75947 100644 --- a/node/build.gradle +++ b/node/build.gradle @@ -209,7 +209,12 @@ dependencies { compile "com.palominolabs.metrics:metrics-new-relic:${metrics_new_relic_version}" // Adding native SSL library to allow using native SSL with Artemis and AMQP - compile "io.netty:netty-tcnative-boringssl-static:$tcnative_version" + compile "io.netty:netty-tcnative-boringssl-static:$tcnative_version:linux-x86_64" + compile "io.netty:netty-tcnative-boringssl-static:$tcnative_version:linux-aarch_64" + compile "io.netty:netty-tcnative-boringssl-static:$tcnative_version:osx-x86_64" + compile "io.netty:netty-tcnative-boringssl-static:$tcnative_version:osx-aarch_64" + compile "io.netty:netty-tcnative-boringssl-static:$tcnative_version:windows-x86_64" + compile "io.netty:netty-tcnative-classes:$tcnative_version" // Byteman for runtime (termination) rules injection on the running node // Submission tool allowing to install rules on running nodes From c445c72d3a974fd71ce153f85e2ccfa3365c83a1 Mon Sep 17 00:00:00 2001 From: Jose Coll Date: Wed, 8 Nov 2023 08:52:25 +0000 Subject: [PATCH 102/126] ENT-10110 Store Ledger Recovery records only if the transaction was locally stored in the first place. (#7564) --- .../DBTransactionStorageLedgerRecovery.kt | 99 ----------- ...DBTransactionStorageLedgerRecoveryTests.kt | 154 ------------------ 2 files changed, 253 deletions(-) diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecovery.kt b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecovery.kt index c68942bd8e..a8e72accee 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecovery.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecovery.kt @@ -4,16 +4,12 @@ import net.corda.core.crypto.SecureHash import net.corda.core.flows.DistributionList.ReceiverDistributionList import net.corda.core.flows.DistributionList.SenderDistributionList import net.corda.core.flows.DistributionRecordKey -import net.corda.core.flows.DistributionRecordType -import net.corda.core.flows.DistributionRecords import net.corda.core.flows.ReceiverDistributionRecord -import net.corda.core.flows.RecoveryTimeWindow import net.corda.core.flows.SenderDistributionRecord import net.corda.core.flows.TransactionMetadata import net.corda.core.identity.CordaX500Name import net.corda.core.internal.NamedCacheFactory import net.corda.core.node.StatesToRecord -import net.corda.core.node.services.vault.Sort import net.corda.core.utilities.OpaqueBytes import net.corda.node.CordaClock import net.corda.node.services.EncryptionService @@ -32,7 +28,6 @@ import javax.persistence.Entity import javax.persistence.Id import javax.persistence.Lob import javax.persistence.Table -import javax.persistence.criteria.Predicate class DBTransactionStorageLedgerRecovery(private val database: CordaPersistence, cacheFactory: NamedCacheFactory, @@ -214,100 +209,6 @@ class DBTransactionStorageLedgerRecovery(private val database: CordaPersistence, } } - fun queryDistributionRecords(timeWindow: RecoveryTimeWindow, - recordType: DistributionRecordType = DistributionRecordType.ALL, - excludingTxnIds: Set = emptySet(), - orderByTimestamp: Sort.Direction? = null - ): DistributionRecords { - return when(recordType) { - DistributionRecordType.SENDER -> - DistributionRecords(senderRecords = - querySenderDistributionRecords(timeWindow, excludingTxnIds = excludingTxnIds, orderByTimestamp = orderByTimestamp)) - DistributionRecordType.RECEIVER -> - DistributionRecords(receiverRecords = - queryReceiverDistributionRecords(timeWindow, excludingTxnIds = excludingTxnIds, orderByTimestamp = orderByTimestamp)) - DistributionRecordType.ALL -> - DistributionRecords(senderRecords = - querySenderDistributionRecords(timeWindow, excludingTxnIds = excludingTxnIds, orderByTimestamp = orderByTimestamp), - receiverRecords = - queryReceiverDistributionRecords(timeWindow, excludingTxnIds = excludingTxnIds, orderByTimestamp = orderByTimestamp)) - } - } - - @Suppress("SpreadOperator") - fun querySenderDistributionRecords(timeWindow: RecoveryTimeWindow, - peers: Set = emptySet(), - excludingTxnIds: Set = emptySet(), - orderByTimestamp: Sort.Direction? = null - ): List { - return database.transaction { - val criteriaBuilder = session.criteriaBuilder - val criteriaQuery = criteriaBuilder.createQuery(DBSenderDistributionRecord::class.java) - val txnMetadata = criteriaQuery.from(DBSenderDistributionRecord::class.java) - val predicates = mutableListOf() - val compositeKey = txnMetadata.get("compositeKey") - predicates.add(criteriaBuilder.greaterThanOrEqualTo(compositeKey.get(PersistentKey::timestamp.name), timeWindow.fromTime)) - predicates.add(criteriaBuilder.and(criteriaBuilder.lessThanOrEqualTo(compositeKey.get(PersistentKey::timestamp.name), timeWindow.untilTime))) - if (excludingTxnIds.isNotEmpty()) { - predicates.add(criteriaBuilder.and(criteriaBuilder.not(compositeKey.get(PersistentKey::txId.name).`in`( - excludingTxnIds.map { it.toString() })))) - } - if (peers.isNotEmpty()) { - val peerPartyIds = peers.map { partyInfoCache.getPartyIdByCordaX500Name(it).toString() } - predicates.add(criteriaBuilder.and(compositeKey.get(PersistentKey::peerPartyId.name).`in`(peerPartyIds))) - } - criteriaQuery.where(*predicates.toTypedArray()) - // optionally order by timestamp - orderByTimestamp?.let { - val orderCriteria = - when (orderByTimestamp) { - // when adding column position of 'group by' shift in case columns were removed - Sort.Direction.ASC -> criteriaBuilder.asc(compositeKey.get(PersistentKey::timestamp.name)) - Sort.Direction.DESC -> criteriaBuilder.desc(compositeKey.get(PersistentKey::timestamp.name)) - } - criteriaQuery.orderBy(orderCriteria) - } - session.createQuery(criteriaQuery).resultList - }.map { it.toSenderDistributionRecord() } - } - - @Suppress("SpreadOperator") - fun queryReceiverDistributionRecords(timeWindow: RecoveryTimeWindow, - initiators: Set = emptySet(), - excludingTxnIds: Set = emptySet(), - orderByTimestamp: Sort.Direction? = null - ): List { - return database.transaction { - val criteriaBuilder = session.criteriaBuilder - val criteriaQuery = criteriaBuilder.createQuery(DBReceiverDistributionRecord::class.java) - val txnMetadata = criteriaQuery.from(DBReceiverDistributionRecord::class.java) - val predicates = mutableListOf() - val compositeKey = txnMetadata.get("compositeKey") - val timestamp = compositeKey.get(PersistentKey::timestamp.name) - predicates.add(criteriaBuilder.greaterThanOrEqualTo(timestamp, timeWindow.fromTime)) - predicates.add(criteriaBuilder.and(criteriaBuilder.lessThanOrEqualTo(timestamp, timeWindow.untilTime))) - if (excludingTxnIds.isNotEmpty()) { - val txId = compositeKey.get(PersistentKey::txId.name) - predicates.add(criteriaBuilder.and(criteriaBuilder.not(txId.`in`(excludingTxnIds.map { it.toString() })))) - } - if (initiators.isNotEmpty()) { - val initiatorPartyIds = initiators.map { partyInfoCache.getPartyIdByCordaX500Name(it).toString() } - predicates.add(criteriaBuilder.and(compositeKey.get(PersistentKey::peerPartyId.name).`in`(initiatorPartyIds))) - } - criteriaQuery.where(*predicates.toTypedArray()) - // optionally order by timestamp - orderByTimestamp?.let { - val orderCriteria = when (orderByTimestamp) { - // when adding column position of 'group by' shift in case columns were removed - Sort.Direction.ASC -> criteriaBuilder.asc(timestamp) - Sort.Direction.DESC -> criteriaBuilder.desc(timestamp) - } - criteriaQuery.orderBy(orderCriteria) - } - session.createQuery(criteriaQuery).resultList - }.map { it.toReceiverDistributionRecord() } - } - fun decryptHashedDistributionList(encryptedBytes: ByteArray): HashedDistributionList { return HashedDistributionList.decrypt(encryptedBytes, encryptionService) } diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecoveryTests.kt b/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecoveryTests.kt index 9cdf700611..0df955e0f1 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecoveryTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageLedgerRecoveryTests.kt @@ -8,9 +8,7 @@ import net.corda.core.crypto.SignatureMetadata import net.corda.core.crypto.sign import net.corda.core.flows.DistributionList.ReceiverDistributionList import net.corda.core.flows.DistributionList.SenderDistributionList -import net.corda.core.flows.DistributionRecordType import net.corda.core.flows.ReceiverDistributionRecord -import net.corda.core.flows.RecoveryTimeWindow import net.corda.core.flows.SenderDistributionRecord import net.corda.core.flows.TransactionMetadata import net.corda.core.node.NodeInfo @@ -51,9 +49,7 @@ import org.junit.Rule import org.junit.Test import java.security.KeyPair import java.time.Clock -import java.time.Duration import java.time.Instant -import java.time.temporal.ChronoUnit import kotlin.test.assertEquals import kotlin.test.assertFailsWith import kotlin.test.assertNull @@ -92,156 +88,6 @@ class DBTransactionStorageLedgerRecoveryTests { return transactionRecovery.clock.instant() } - @Test(timeout = 300_000) - fun `query local ledger for transactions with recovery peers within time window`() { - val beforeFirstTxn = now().truncatedTo(ChronoUnit.SECONDS) - val txn = newTransaction() - transactionRecovery.addUnnotarisedTransaction(txn) - transactionRecovery.addSenderTransactionRecoveryMetadata(txn.id, TransactionMetadata(ALICE_NAME, SenderDistributionList(ALL_VISIBLE, mapOf(BOB_NAME to ONLY_RELEVANT)))) - val timeWindow = RecoveryTimeWindow(fromTime = beforeFirstTxn, - untilTime = beforeFirstTxn.plus(1, ChronoUnit.MINUTES)) - val results = transactionRecovery.querySenderDistributionRecords(timeWindow) - assertEquals(1, results.size) - (transactionRecovery.clock as TestClock).advanceBy(Duration.ofSeconds(1)) - val afterFirstTxn = now().truncatedTo(ChronoUnit.SECONDS) - val txn2 = newTransaction() - transactionRecovery.addUnnotarisedTransaction(txn2) - transactionRecovery.addSenderTransactionRecoveryMetadata(txn2.id, TransactionMetadata(ALICE_NAME, SenderDistributionList(ONLY_RELEVANT, mapOf(CHARLIE_NAME to ONLY_RELEVANT)))) - assertEquals(2, transactionRecovery.querySenderDistributionRecords(timeWindow).size) - assertEquals(1, transactionRecovery.querySenderDistributionRecords(RecoveryTimeWindow(fromTime = afterFirstTxn, - untilTime = afterFirstTxn.plus(1, ChronoUnit.MINUTES))).size) - } - - @Test(timeout = 300_000) - fun `query local ledger for transactions within timeWindow and excluding remoteTransactionIds`() { - val transaction1 = newTransaction() - transactionRecovery.addUnnotarisedTransaction(transaction1) - transactionRecovery.addSenderTransactionRecoveryMetadata(transaction1.id, TransactionMetadata(ALICE_NAME, SenderDistributionList(ALL_VISIBLE, mapOf(BOB_NAME to ONLY_RELEVANT)))) - val transaction2 = newTransaction() - transactionRecovery.addUnnotarisedTransaction(transaction2) - transactionRecovery.addSenderTransactionRecoveryMetadata(transaction2.id, TransactionMetadata(ALICE_NAME, SenderDistributionList(ALL_VISIBLE, mapOf(BOB_NAME to ONLY_RELEVANT)))) - val timeWindow = RecoveryTimeWindow(fromTime = now().minus(1, ChronoUnit.DAYS)) - val results = transactionRecovery.querySenderDistributionRecords(timeWindow, excludingTxnIds = setOf(transaction1.id)) - assertEquals(1, results.size) - assertEquals(transaction2.id, results[0].txId) - } - - @Test(timeout = 300_000) - fun `query local ledger for transactions within timeWindow and for given peers`() { - val transaction1 = newTransaction() - transactionRecovery.addUnnotarisedTransaction(transaction1) - transactionRecovery.addSenderTransactionRecoveryMetadata(transaction1.id, TransactionMetadata(ALICE_NAME, SenderDistributionList(ALL_VISIBLE, mapOf(BOB_NAME to ONLY_RELEVANT)))) - val transaction2 = newTransaction() - transactionRecovery.addUnnotarisedTransaction(transaction2) - transactionRecovery.addSenderTransactionRecoveryMetadata(transaction2.id, TransactionMetadata(ALICE_NAME, SenderDistributionList(ALL_VISIBLE, mapOf(CHARLIE_NAME to ONLY_RELEVANT)))) - val timeWindow = RecoveryTimeWindow(fromTime = now().minus(1, ChronoUnit.DAYS)) - val results = transactionRecovery.querySenderDistributionRecords(timeWindow, peers = setOf(CHARLIE_NAME)) - assertEquals(1, results.size) - assertEquals(transaction2.id, results[0].txId) - } - - @Test(timeout = 300_000) - fun `query local ledger by distribution record type`() { - val transaction1 = newTransaction() - // sender txn - transactionRecovery.addUnnotarisedTransaction(transaction1) - transactionRecovery.addSenderTransactionRecoveryMetadata(transaction1.id, TransactionMetadata(ALICE_NAME, SenderDistributionList(ALL_VISIBLE, mapOf(BOB_NAME to ALL_VISIBLE)))) - val transaction2 = newTransaction() - // receiver txn - transactionRecovery.addUnnotarisedTransaction(transaction2) - val encryptedDL = transactionRecovery.addSenderTransactionRecoveryMetadata(transaction2.id, - TransactionMetadata(BOB_NAME, SenderDistributionList(ONLY_RELEVANT, mapOf(ALICE_NAME to ALL_VISIBLE)))) - transactionRecovery.addReceiverTransactionRecoveryMetadata(transaction2.id, BOB_NAME, - TransactionMetadata(BOB_NAME, ReceiverDistributionList(encryptedDL, ALL_VISIBLE))) - val timeWindow = RecoveryTimeWindow(fromTime = now().minus(1, ChronoUnit.DAYS)) - transactionRecovery.queryDistributionRecords(timeWindow, recordType = DistributionRecordType.SENDER).let { - assertEquals(2, it.size) - assertEquals(SecureHash.sha256(BOB_NAME.toString()), it.senderRecords[0].peerPartyId) - assertEquals(ALL_VISIBLE, it.senderRecords[0].senderStatesToRecord) - } - transactionRecovery.queryDistributionRecords(timeWindow, recordType = DistributionRecordType.RECEIVER).let { - assertEquals(1, it.size) - assertEquals(SecureHash.sha256(BOB_NAME.toString()), it.receiverRecords[0].peerPartyId) - assertEquals(ALL_VISIBLE, (HashedDistributionList.decrypt(it.receiverRecords[0].encryptedDistributionList.bytes, encryptionService)).peerHashToStatesToRecord.map { it.value }[0]) - } - val resultsAll = transactionRecovery.queryDistributionRecords(timeWindow, recordType = DistributionRecordType.ALL) - assertEquals(3, resultsAll.size) - } - - @Test(timeout = 300_000) - fun `query for sender distribution records by peers`() { - val txn1 = newTransaction() - transactionRecovery.addUnnotarisedTransaction(txn1) - transactionRecovery.addSenderTransactionRecoveryMetadata(txn1.id, TransactionMetadata(ALICE_NAME, SenderDistributionList(ALL_VISIBLE, mapOf(BOB_NAME to ALL_VISIBLE)))) - val txn2 = newTransaction() - transactionRecovery.addUnnotarisedTransaction(txn2) - transactionRecovery.addSenderTransactionRecoveryMetadata(txn2.id, TransactionMetadata(ALICE_NAME, SenderDistributionList(ONLY_RELEVANT, mapOf(CHARLIE_NAME to ONLY_RELEVANT)))) - val txn3 = newTransaction() - transactionRecovery.addUnnotarisedTransaction(txn3) - transactionRecovery.addSenderTransactionRecoveryMetadata(txn3.id, TransactionMetadata(ALICE_NAME, SenderDistributionList(ONLY_RELEVANT, mapOf(BOB_NAME to ONLY_RELEVANT, CHARLIE_NAME to ALL_VISIBLE)))) - val txn4 = newTransaction() - transactionRecovery.addUnnotarisedTransaction(txn4) - transactionRecovery.addSenderTransactionRecoveryMetadata(txn4.id, TransactionMetadata(BOB_NAME, SenderDistributionList(ONLY_RELEVANT, mapOf(ALICE_NAME to ONLY_RELEVANT)))) - val txn5 = newTransaction() - transactionRecovery.addUnnotarisedTransaction(txn5) - transactionRecovery.addSenderTransactionRecoveryMetadata(txn5.id, TransactionMetadata(CHARLIE_NAME, SenderDistributionList(ONLY_RELEVANT, emptyMap()))) - assertEquals(5, readSenderDistributionRecordFromDB().size) - - val timeWindow = RecoveryTimeWindow(fromTime = now().minus(1, ChronoUnit.DAYS)) - transactionRecovery.querySenderDistributionRecords(timeWindow, peers = setOf(BOB_NAME)).let { - assertEquals(2, it.size) - assertEquals(it[0].senderStatesToRecord, ALL_VISIBLE) - assertEquals(it[1].senderStatesToRecord, ONLY_RELEVANT) - } - assertEquals(1, transactionRecovery.querySenderDistributionRecords(timeWindow, peers = setOf(ALICE_NAME)).size) - assertEquals(2, transactionRecovery.querySenderDistributionRecords(timeWindow, peers = setOf(CHARLIE_NAME)).size) - } - - @Test(timeout = 300_000) - fun `query for receiver distribution records by initiator`() { - val txn1 = newTransaction() - transactionRecovery.addUnnotarisedTransaction(txn1) - val encryptedDL1 = transactionRecovery.addSenderTransactionRecoveryMetadata(txn1.id, - TransactionMetadata(ALICE_NAME, SenderDistributionList(ONLY_RELEVANT, mapOf(BOB_NAME to ALL_VISIBLE, CHARLIE_NAME to ALL_VISIBLE)))) - transactionRecovery.addReceiverTransactionRecoveryMetadata(txn1.id, ALICE_NAME, - TransactionMetadata(ALICE_NAME, ReceiverDistributionList(encryptedDL1, ALL_VISIBLE))) - val txn2 = newTransaction() - transactionRecovery.addUnnotarisedTransaction(txn2) - val encryptedDL2 = transactionRecovery.addSenderTransactionRecoveryMetadata(txn2.id, - TransactionMetadata(ALICE_NAME, SenderDistributionList(ONLY_RELEVANT, mapOf(BOB_NAME to ONLY_RELEVANT)))) - transactionRecovery.addReceiverTransactionRecoveryMetadata(txn2.id, ALICE_NAME, - TransactionMetadata(ALICE_NAME, ReceiverDistributionList(encryptedDL2, ONLY_RELEVANT))) - val txn3 = newTransaction() - transactionRecovery.addUnnotarisedTransaction(txn3) - val encryptedDL3 = transactionRecovery.addSenderTransactionRecoveryMetadata(txn3.id, - TransactionMetadata(ALICE_NAME, SenderDistributionList(ONLY_RELEVANT, mapOf(CHARLIE_NAME to NONE)))) - transactionRecovery.addReceiverTransactionRecoveryMetadata(txn3.id, ALICE_NAME, - TransactionMetadata(ALICE_NAME, ReceiverDistributionList(encryptedDL3, NONE))) - val txn4 = newTransaction() - transactionRecovery.addUnnotarisedTransaction(txn4) - val encryptedDL4 = transactionRecovery.addSenderTransactionRecoveryMetadata(txn4.id, - TransactionMetadata(BOB_NAME, SenderDistributionList(ONLY_RELEVANT, mapOf(ALICE_NAME to ALL_VISIBLE)))) - transactionRecovery.addReceiverTransactionRecoveryMetadata(txn4.id, BOB_NAME, - TransactionMetadata(BOB_NAME, ReceiverDistributionList(encryptedDL4, ALL_VISIBLE))) - val txn5 = newTransaction() - transactionRecovery.addUnnotarisedTransaction(txn5) - val encryptedDL5 = transactionRecovery.addSenderTransactionRecoveryMetadata(txn5.id, - TransactionMetadata(CHARLIE_NAME, SenderDistributionList(ONLY_RELEVANT, mapOf(BOB_NAME to ONLY_RELEVANT)))) - transactionRecovery.addReceiverTransactionRecoveryMetadata(txn5.id, CHARLIE_NAME, - TransactionMetadata(CHARLIE_NAME, ReceiverDistributionList(encryptedDL5, ONLY_RELEVANT))) - - val timeWindow = RecoveryTimeWindow(fromTime = now().minus(1, ChronoUnit.DAYS)) - transactionRecovery.queryReceiverDistributionRecords(timeWindow, initiators = setOf(ALICE_NAME)).let { - assertEquals(3, it.size) - assertEquals(HashedDistributionList.decrypt(it[0].encryptedDistributionList.bytes, encryptionService).peerHashToStatesToRecord.map { it.value }[0], ALL_VISIBLE) - assertEquals(HashedDistributionList.decrypt(it[1].encryptedDistributionList.bytes, encryptionService).peerHashToStatesToRecord.map { it.value }[0], ONLY_RELEVANT) - assertEquals(HashedDistributionList.decrypt(it[2].encryptedDistributionList.bytes, encryptionService).peerHashToStatesToRecord.map { it.value }[0], NONE) - } - assertEquals(1, transactionRecovery.queryReceiverDistributionRecords(timeWindow, initiators = setOf(BOB_NAME)).size) - assertEquals(1, transactionRecovery.queryReceiverDistributionRecords(timeWindow, initiators = setOf(CHARLIE_NAME)).size) - assertEquals(2, transactionRecovery.queryReceiverDistributionRecords(timeWindow, initiators = setOf(BOB_NAME, CHARLIE_NAME)).size) - } - @Test(timeout = 300_000) fun `transaction without peers does not store recovery metadata in database`() { val senderTransaction = newTransaction() From e94e02fc4b9331dc85be15d24af5466a973691f9 Mon Sep 17 00:00:00 2001 From: Jose Coll Date: Thu, 9 Nov 2023 11:25:56 +0000 Subject: [PATCH 103/126] Update index name (to meet Oracle max length limitation of 30 chars). --- node/src/main/resources/migration/node-core.changelog-v25.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node/src/main/resources/migration/node-core.changelog-v25.xml b/node/src/main/resources/migration/node-core.changelog-v25.xml index dee14a6fa2..75a4144667 100644 --- a/node/src/main/resources/migration/node-core.changelog-v25.xml +++ b/node/src/main/resources/migration/node-core.changelog-v25.xml @@ -39,7 +39,7 @@ - + From 974d81cf4162d6e4715ba952a101a6132d2ffbb5 Mon Sep 17 00:00:00 2001 From: Chris Cochrane Date: Fri, 10 Nov 2023 12:41:44 +0000 Subject: [PATCH 104/126] Revert netty version --- constants.properties | 4 ++-- node/build.gradle | 7 +------ 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/constants.properties b/constants.properties index c62bc72c80..7db38579d3 100644 --- a/constants.properties +++ b/constants.properties @@ -38,7 +38,7 @@ openSourceBranch=https://github.com/corda/corda/blob/release/os/4.4 openSourceSamplesBranch=https://github.com/corda/samples/blob/release-V4 jolokiaAgentVersion=1.6.1 detektVersion=1.0.1 -tcnativeVersion=2.0.62.Final +tcnativeVersion=2.0.48.Final commonsConfiguration2Version=2.8.0 commonsTextVersion=1.10.0 @@ -59,7 +59,7 @@ assertjVersion=3.12.2 slf4JVersion=1.7.30 log4JVersion=2.17.1 okhttpVersion=3.14.9 -nettyVersion=4.1.100.Final +nettyVersion=4.1.77.Final fileuploadVersion=1.4 kryoVersion=4.0.2 kryoSerializerVersion=0.43 diff --git a/node/build.gradle b/node/build.gradle index e16db75947..81418360f7 100644 --- a/node/build.gradle +++ b/node/build.gradle @@ -209,12 +209,7 @@ dependencies { compile "com.palominolabs.metrics:metrics-new-relic:${metrics_new_relic_version}" // Adding native SSL library to allow using native SSL with Artemis and AMQP - compile "io.netty:netty-tcnative-boringssl-static:$tcnative_version:linux-x86_64" - compile "io.netty:netty-tcnative-boringssl-static:$tcnative_version:linux-aarch_64" - compile "io.netty:netty-tcnative-boringssl-static:$tcnative_version:osx-x86_64" - compile "io.netty:netty-tcnative-boringssl-static:$tcnative_version:osx-aarch_64" - compile "io.netty:netty-tcnative-boringssl-static:$tcnative_version:windows-x86_64" - compile "io.netty:netty-tcnative-classes:$tcnative_version" + compile "io.netty:netty-tcnative-boringssl-static:$tcnative_version" // Byteman for runtime (termination) rules injection on the running node // Submission tool allowing to install rules on running nodes From 9065c1703ad533d2aa6c921cde5e34b74a5417d7 Mon Sep 17 00:00:00 2001 From: Chris Cochrane Date: Mon, 13 Nov 2023 17:07:49 +0000 Subject: [PATCH 105/126] Updated changeset to match Enterprise --- .../node/services/persistence/DBTransactionStorage.kt | 2 ++ .../resources/migration/node-core.changelog-v23.xml | 11 +++++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt index b66dedb22b..42078c9764 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt @@ -37,6 +37,7 @@ import net.corda.nodeapi.internal.persistence.contextTransactionOrNull import net.corda.nodeapi.internal.persistence.currentDBSession import net.corda.nodeapi.internal.persistence.wrapWithDatabaseTransaction import net.corda.serialization.internal.CordaSerializationEncoding.SNAPPY +import org.hibernate.annotations.Type import rx.Observable import rx.subjects.PublishSubject import java.time.Instant @@ -78,6 +79,7 @@ open class DBTransactionStorage(private val database: CordaPersistence, cacheFac val timestamp: Instant, @Column(name = "signatures") + @Type(type = "corda-blob") val signatures: ByteArray? ) diff --git a/node/src/main/resources/migration/node-core.changelog-v23.xml b/node/src/main/resources/migration/node-core.changelog-v23.xml index 095f5e6bee..235c65b31b 100644 --- a/node/src/main/resources/migration/node-core.changelog-v23.xml +++ b/node/src/main/resources/migration/node-core.changelog-v23.xml @@ -4,9 +4,16 @@ xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.5.xsd" logicalFilePath="migration/node-services.changelog-init.xml"> - + - + + + + + + + + \ No newline at end of file From b715140625c0d26a4dce249d9c7fc9fcc520d4e4 Mon Sep 17 00:00:00 2001 From: Adel El-Beik Date: Wed, 15 Nov 2023 16:11:45 +0000 Subject: [PATCH 106/126] ENT-11107: Ctors added to LedgerRecoveryFlow to match EnterpriseLedgerRecoveryFlow. --- .../net/corda/core/flows/LedgerRecoverFlow.kt | 38 ++++++++++++++++++- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/flows/LedgerRecoverFlow.kt b/core/src/main/kotlin/net/corda/core/flows/LedgerRecoverFlow.kt index 5153f98e91..3e16d044ad 100644 --- a/core/src/main/kotlin/net/corda/core/flows/LedgerRecoverFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/LedgerRecoverFlow.kt @@ -11,8 +11,42 @@ import net.corda.core.utilities.ProgressTracker */ @StartableByRPC class LedgerRecoveryFlow( - private val parameters: LedgerRecoveryParameters, - override val progressTracker: ProgressTracker = ProgressTracker()) : FlowLogic() { + private val parameters: LedgerRecoveryParameters, + override val progressTracker: ProgressTracker = ProgressTracker()) : FlowLogic() { + + // constructors added to aid Corda Node Shell flow command invocation + constructor(recoveryPeer: Party) : this(LedgerRecoveryParameters(setOf(recoveryPeer))) + constructor(recoveryPeers: Collection) : this(LedgerRecoveryParameters(recoveryPeers)) + constructor(useAllNetworkNodes: Boolean) : this(LedgerRecoveryParameters(emptySet(), useAllNetworkNodes = useAllNetworkNodes)) + constructor(recoveryPeer: Party, timeWindow: RecoveryTimeWindow) : + this(LedgerRecoveryParameters(setOf(recoveryPeer), timeWindow)) + constructor(recoveryPeer: Party, timeWindow: RecoveryTimeWindow, dryRun: Boolean) : + this(LedgerRecoveryParameters(setOf(recoveryPeer), timeWindow, dryRun = dryRun)) + constructor(recoveryPeer: Party, timeWindow: RecoveryTimeWindow, dryRun: Boolean, verboseLogging: Boolean) : + this(LedgerRecoveryParameters(setOf(recoveryPeer), timeWindow, dryRun = dryRun, verboseLogging = verboseLogging)) + constructor(recoveryPeer: Party, timeWindow: RecoveryTimeWindow, dryRun: Boolean, verboseLogging: Boolean, alsoFinalize: Boolean) : + this(LedgerRecoveryParameters(setOf(recoveryPeer), timeWindow, dryRun = dryRun, verboseLogging = verboseLogging, alsoFinalize = alsoFinalize)) + constructor(recoveryPeers: Collection, timeWindow: RecoveryTimeWindow) : + this(LedgerRecoveryParameters(recoveryPeers, timeWindow)) + constructor(recoveryPeers: Collection, timeWindow: RecoveryTimeWindow, dryRun: Boolean) : + this(LedgerRecoveryParameters(recoveryPeers, timeWindow, dryRun = dryRun)) + constructor(recoveryPeers: Collection, timeWindow: RecoveryTimeWindow, dryRun: Boolean, verboseLogging: Boolean) : + this(LedgerRecoveryParameters(recoveryPeers, timeWindow, dryRun = dryRun, verboseLogging = verboseLogging)) + constructor(recoveryPeers: Collection, timeWindow: RecoveryTimeWindow, dryRun: Boolean, verboseLogging: Boolean, alsoFinalize: Boolean) : + this(LedgerRecoveryParameters(recoveryPeers, timeWindow, dryRun = dryRun, verboseLogging = verboseLogging, alsoFinalize = alsoFinalize)) + constructor(useAllNetworkNodes: Boolean, timeWindow: RecoveryTimeWindow) : + this(LedgerRecoveryParameters(emptySet(), timeWindow, useAllNetworkNodes = useAllNetworkNodes)) + constructor(useAllNetworkNodes: Boolean, timeWindow: RecoveryTimeWindow, dryRun: Boolean) : + this(LedgerRecoveryParameters(emptySet(), timeWindow, useAllNetworkNodes = useAllNetworkNodes, dryRun = dryRun)) + constructor(useAllNetworkNodes: Boolean, timeWindow: RecoveryTimeWindow, dryRun: Boolean, verboseLogging: Boolean) : + this(LedgerRecoveryParameters(emptySet(), timeWindow, useAllNetworkNodes = useAllNetworkNodes, dryRun = dryRun, verboseLogging = verboseLogging)) + constructor(useAllNetworkNodes: Boolean, timeWindow: RecoveryTimeWindow, dryRun: Boolean, verboseLogging: Boolean, recoveryBatchSize: Int, alsoFinalize: Boolean) : + this(LedgerRecoveryParameters(emptySet(), timeWindow, useAllNetworkNodes = useAllNetworkNodes, dryRun = dryRun, verboseLogging = verboseLogging, recoveryBatchSize = recoveryBatchSize, alsoFinalize = alsoFinalize)) + constructor(useAllNetworkNodes: Boolean, timeWindow: RecoveryTimeWindow, dryRun: Boolean, verboseLogging: Boolean, recoveryBatchSize: Int) : + this(LedgerRecoveryParameters(emptySet(), timeWindow, useAllNetworkNodes = useAllNetworkNodes, dryRun = dryRun, verboseLogging = verboseLogging, recoveryBatchSize = recoveryBatchSize)) + constructor(recoveryPeers: Collection, timeWindow: RecoveryTimeWindow, useAllNetworkNodes: Boolean, dryRun: Boolean, useTimeWindowNarrowing: Boolean, verboseLogging: Boolean, recoveryBatchSize: Int) : + this(LedgerRecoveryParameters(recoveryPeers, timeWindow, useAllNetworkNodes, + dryRun = dryRun, useTimeWindowNarrowing = useTimeWindowNarrowing, verboseLogging = verboseLogging, recoveryBatchSize = recoveryBatchSize)) @CordaInternal data class ExtraConstructorArgs(val parameters: LedgerRecoveryParameters) From 52e43d422377eac7e708405cbefc63e98fc1922f Mon Sep 17 00:00:00 2001 From: Jacob Scott Date: Thu, 16 Nov 2023 16:21:07 +0000 Subject: [PATCH 107/126] ES-1622: Switch corda/corda Github Actions to use Github App for authentication (#7577) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update jira_assign_issue.yml * Update jira_close_issue.yml * Update jira_create_issue.yml * Update .github/workflows/jira_assign_issue.yml * Update .github/workflows/jira_assign_issue.yml --------- Co-authored-by: Waldemar Żurowski <45210402+wzur-r3@users.noreply.github.com> --- .github/workflows/jira_assign_issue.yml | 8 +++++++- .github/workflows/jira_close_issue.yml | 8 +++++++- .github/workflows/jira_create_issue.yml | 9 ++++++++- 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/.github/workflows/jira_assign_issue.yml b/.github/workflows/jira_assign_issue.yml index 7d8b4fba96..9efd0f87a0 100644 --- a/.github/workflows/jira_assign_issue.yml +++ b/.github/workflows/jira_assign_issue.yml @@ -8,12 +8,18 @@ jobs: sync_assigned: runs-on: ubuntu-latest steps: + - name: Generate a token + id: generate_token + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ secrets.AUTH_APP_ID }} + private-key: ${{ secrets.AUTH_APP_PK }} - name: Assign uses: corda/jira-sync-assigned-action@master with: jiraBaseUrl: ${{ secrets.JIRA_BASE_URL }} jiraEmail: ${{ secrets.JIRA_USER_EMAIL }} jiraToken: ${{ secrets.JIRA_API_TOKEN }} - token: ${{ secrets.GH_TOKEN }} + token: ${{ steps.generate_token.outputs.token }} owner: corda repository: corda diff --git a/.github/workflows/jira_close_issue.yml b/.github/workflows/jira_close_issue.yml index dfe3d2443a..00f5b06363 100644 --- a/.github/workflows/jira_close_issue.yml +++ b/.github/workflows/jira_close_issue.yml @@ -8,12 +8,18 @@ jobs: sync_closed: runs-on: ubuntu-latest steps: + - name: Generate a token + id: generate_token + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ secrets.AUTH_APP_ID }} + private-key: ${{ secrets.AUTH_APP_PK }} - name: Close uses: corda/jira-sync-closed-action@master with: jiraBaseUrl: https://r3-cev.atlassian.net jiraEmail: ${{ secrets.JIRA_USER_EMAIL }} jiraToken: ${{ secrets.JIRA_API_TOKEN }} - token: ${{ secrets.GH_TOKEN }} + token: ${{ steps.generate_token.outputs.token }} owner: corda repository: corda diff --git a/.github/workflows/jira_create_issue.yml b/.github/workflows/jira_create_issue.yml index 66a3bbdc37..066fb8ca45 100644 --- a/.github/workflows/jira_create_issue.yml +++ b/.github/workflows/jira_create_issue.yml @@ -10,6 +10,13 @@ jobs: steps: - uses: actions/checkout@v2 + - name: Generate a token + id: generate_token + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ secrets.AUTH_APP_ID }} + private-key: ${{ secrets.AUTH_APP_PK }} + - name: Jira Create issue id: create uses: corda/jira-create-issue-action@master @@ -30,7 +37,7 @@ jobs: - name: Create comment uses: peter-evans/create-or-update-comment@v1 with: - token: ${{ secrets.GH_TOKEN }} + token: ${{ steps.generate_token.outputs.token }} issue-number: ${{ github.event.issue.number }} body: | Automatically created Jira issue: ${{ steps.create.outputs.issue }} From d8b19de46e4d34840dc286d5b6b6fedcf0787487 Mon Sep 17 00:00:00 2001 From: Chris Cochrane Date: Tue, 21 Nov 2023 11:48:37 +0000 Subject: [PATCH 108/126] Renamed index to keep Oracle happy --- node/src/main/resources/migration/node-core.changelog-v25.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node/src/main/resources/migration/node-core.changelog-v25.xml b/node/src/main/resources/migration/node-core.changelog-v25.xml index 75a4144667..28ae879bc1 100644 --- a/node/src/main/resources/migration/node-core.changelog-v25.xml +++ b/node/src/main/resources/migration/node-core.changelog-v25.xml @@ -75,7 +75,7 @@ - + From 47fd1ec1e4a510f28ee5d129c4a4ad971569ddac Mon Sep 17 00:00:00 2001 From: Adel El-Beik Date: Thu, 23 Nov 2023 11:33:16 +0000 Subject: [PATCH 109/126] ENT-11171: Updated JDK8 to 392. --- docker/src/docker/Dockerfile | 2 +- docker/src/docker/Dockerfile-debug | 2 +- docker/src/docker/DockerfileAL | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docker/src/docker/Dockerfile b/docker/src/docker/Dockerfile index 84b259361b..a6014f8973 100644 --- a/docker/src/docker/Dockerfile +++ b/docker/src/docker/Dockerfile @@ -1,4 +1,4 @@ -FROM azul/zulu-openjdk:8u382 +FROM azul/zulu-openjdk:8u392 ## Remove Azul Zulu repo, as it is gone by now RUN rm -rf /etc/apt/sources.list.d/zulu.list diff --git a/docker/src/docker/Dockerfile-debug b/docker/src/docker/Dockerfile-debug index aa19ffcd5b..b7dd204884 100644 --- a/docker/src/docker/Dockerfile-debug +++ b/docker/src/docker/Dockerfile-debug @@ -1,4 +1,4 @@ -FROM azul/zulu-openjdk:8u382 +FROM azul/zulu-openjdk:8u392 ## Add packages, clean cache, create dirs, create corda user and change ownership RUN apt-get update && \ diff --git a/docker/src/docker/DockerfileAL b/docker/src/docker/DockerfileAL index 7447d84496..816ef57027 100644 --- a/docker/src/docker/DockerfileAL +++ b/docker/src/docker/DockerfileAL @@ -1,4 +1,4 @@ -FROM amazoncorretto:8u382-al2 +FROM amazoncorretto:8u392-al2 ## Add packages, clean cache, create dirs, create corda user and change ownership RUN yum -y install bash && \ From 72e5c4fed226e26a6e6383e12c86c31866b25596 Mon Sep 17 00:00:00 2001 From: Adel El-Beik <48713346+adelel1@users.noreply.github.com> Date: Tue, 28 Nov 2023 07:10:53 +0000 Subject: [PATCH 110/126] ENT-11036: initiatedBy is now a list of CordaX500Names. (#7592) --- .../main/kotlin/net/corda/core/flows/FinalityRecoveryFlow.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/kotlin/net/corda/core/flows/FinalityRecoveryFlow.kt b/core/src/main/kotlin/net/corda/core/flows/FinalityRecoveryFlow.kt index 2e0adf9eb2..1d03305243 100644 --- a/core/src/main/kotlin/net/corda/core/flows/FinalityRecoveryFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/FinalityRecoveryFlow.kt @@ -56,7 +56,7 @@ class FlowRecoveryException(message: String, cause: Throwable? = null) : FlowExc @CordaSerializable data class FlowRecoveryQuery( val timeframe: FlowTimeWindow? = null, - val initiatedBy: CordaX500Name? = null, + val initiatedBy: List? = null, val counterParties: List? = null) { init { require(timeframe != null || initiatedBy != null || counterParties != null) { From 59ff476a409270999cf63cce7adaa273a544d959 Mon Sep 17 00:00:00 2001 From: Rick Parker Date: Tue, 28 Nov 2023 11:15:21 +0000 Subject: [PATCH 111/126] ENT-11050: Support flag to indicate if in transaction back chain resolution, and use it to drop deserialization errors to warn (#7591) * Support flag to indicate if in transaction back chain resolution, and use it to drop deserialization errors to warn * Fix and rename test that has different outcome now --- .../node/VaultUpdateDeserializationTest.kt | 33 ++++++++----------- .../node/services/DbTransactionsResolver.kt | 3 +- .../node/services/api/ServiceHubInternal.kt | 14 ++++---- .../node/services/api/VaultServiceInternal.kt | 3 +- .../node/services/vault/NodeVaultService.kt | 19 ++++++----- 5 files changed, 36 insertions(+), 36 deletions(-) diff --git a/node/src/integration-test/kotlin/net/corda/node/VaultUpdateDeserializationTest.kt b/node/src/integration-test/kotlin/net/corda/node/VaultUpdateDeserializationTest.kt index 9b0bc3f0e9..48aadf45d4 100644 --- a/node/src/integration-test/kotlin/net/corda/node/VaultUpdateDeserializationTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/VaultUpdateDeserializationTest.kt @@ -4,7 +4,6 @@ import co.paralleluniverse.strands.Strand import junit.framework.TestCase.assertEquals import junit.framework.TestCase.assertNotNull import junit.framework.TestCase.assertTrue -import net.corda.core.flows.UnexpectedFlowEndException import net.corda.core.internal.InputStreamAndHash import net.corda.core.internal.deleteRecursively import net.corda.core.internal.div @@ -55,13 +54,15 @@ class VaultUpdateDeserializationTest { /* * Transaction sent from A -> B with Notarisation * Test that a deserialization error is raised where the receiver node of a transaction has an incompatible contract jar. - * In the case of a notarised transaction, a deserialisation error is thrown in the receiver SignTransactionFlow (before finality) - * upon receiving the transaction to be signed and attempting to record its dependencies. - * The ledger will not record any transactions, and the flow must be retried by the sender upon installing the correct contract jar + * But only on new transactions, and not in the back chain. + * In the case of a notarised transaction, a deserialisation error is thrown in the receiver in the second phase of finality + * when updating the vault. The sender will not block, and the back chain is successfully recorded + * on the receiver even though those states have deserialization errors too. The flow on the receiver is hospitalised. + * The flow will be retried by the receiver upon installing the correct contract jar * version at the receiver and re-starting the node. */ - @Test(timeout=300_000) - fun `Notarised transaction fails completely upon receiver deserialization failure collecting signatures when using incompatible contract jar`() { + @Test(timeout = 300_000) + fun `Notarised transaction fails but back chain succeeds upon receiver deserialization failure when using incompatible contract jar`() { driver(driverParameters(listOf(flowVersion1, contractVersion1))) { val alice = startNode(NodeParameters(additionalCordapps = listOf(flowVersion1, contractVersion1)), providedName = ALICE_NAME).getOrThrow() @@ -75,20 +76,15 @@ class VaultUpdateDeserializationTest { val stx = alice.rpc.startFlow(::AttachmentIssueFlow, hash, defaultNotaryIdentity).returnValue.getOrThrow(30.seconds) val spendableState = stx.coreTransaction.outRef(0) - // NOTE: exception is propagated from Receiver - try { - alice.rpc.startFlow(::AttachmentFlowV1, bob.nodeInfo.singleIdentity(), defaultNotaryIdentity, hash, spendableState).returnValue.getOrThrow(30.seconds) - } - catch(e: UnexpectedFlowEndException) { - println("Bob fails to deserialise transaction upon receipt of transaction for signing.") - } + alice.rpc.startFlow(::AttachmentFlowV1, bob.nodeInfo.singleIdentity(), defaultNotaryIdentity, hash, spendableState).returnValue.getOrThrow(30.seconds) + assertEquals(0, bob.rpc.vaultQueryBy().states.size) assertEquals(1, alice.rpc.vaultQueryBy().states.size) // check transaction records @Suppress("DEPRECATION") - assertEquals(1, alice.rpc.internalVerifiedTransactionsSnapshot().size) // issuance only + assertEquals(2, alice.rpc.internalVerifiedTransactionsSnapshot().size) // both @Suppress("DEPRECATION") - assertTrue(bob.rpc.internalVerifiedTransactionsSnapshot().isEmpty()) + assertEquals(1, bob.rpc.internalVerifiedTransactionsSnapshot().size) // issuance only // restart Bob with correct contract jar version (bob as OutOfProcess).process.destroyForcibly() @@ -97,13 +93,12 @@ class VaultUpdateDeserializationTest { val restartedBob = startNode(NodeParameters(additionalCordapps = listOf(flowVersion1, contractVersion1)), providedName = BOB_NAME).getOrThrow() - // re-run failed flow - alice.rpc.startFlow(::AttachmentFlowV1, restartedBob.nodeInfo.singleIdentity(), defaultNotaryIdentity, hash, spendableState).returnValue.getOrThrow(30.seconds) - + // original hospitalized transaction should now have been re-processed with correct contract jar assertEquals(1, waitForVaultUpdate(restartedBob)) assertEquals(1, alice.rpc.vaultQueryBy().states.size) @Suppress("DEPRECATION") - assertTrue(restartedBob.rpc.internalVerifiedTransactionsSnapshot().isNotEmpty()) + assertEquals(2, restartedBob.rpc.internalVerifiedTransactionsSnapshot().size) // both + assertEquals(1, restartedBob.rpc.vaultQueryBy().states.size) } } diff --git a/node/src/main/kotlin/net/corda/node/services/DbTransactionsResolver.kt b/node/src/main/kotlin/net/corda/node/services/DbTransactionsResolver.kt index 668b95ff92..9a8e9d5eda 100644 --- a/node/src/main/kotlin/net/corda/node/services/DbTransactionsResolver.kt +++ b/node/src/main/kotlin/net/corda/node/services/DbTransactionsResolver.kt @@ -13,6 +13,7 @@ import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.debug import net.corda.core.utilities.seconds import net.corda.core.utilities.trace +import net.corda.node.services.api.ServiceHubInternal import net.corda.node.services.api.WritableTransactionStorage import java.util.* @@ -107,7 +108,7 @@ class DbTransactionsResolver(private val flow: ResolveTransactionsFlow) : Transa } if (txStatus == TransactionStatus.UNVERIFIED) { tx.verify(flow.serviceHub) - flow.serviceHub.recordTransactions(usedStatesToRecord, listOf(tx)) + (flow.serviceHub as ServiceHubInternal).recordTransactions(usedStatesToRecord, listOf(tx), false, disableSoftLocking = true) } else { logger.debug { "No need to record $txId as it's already been verified" } } diff --git a/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt b/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt index 0ee732b210..b283c137c4 100644 --- a/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt +++ b/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt @@ -5,8 +5,8 @@ import net.corda.core.context.InvocationContext import net.corda.core.crypto.SecureHash import net.corda.core.crypto.TransactionSignature import net.corda.core.flows.FlowLogic -import net.corda.core.flows.TransactionMetadata import net.corda.core.flows.StateMachineRunId +import net.corda.core.flows.TransactionMetadata import net.corda.core.identity.CordaX500Name import net.corda.core.internal.FlowStateMachineHandle import net.corda.core.internal.NamedCacheFactory @@ -38,7 +38,6 @@ import net.corda.node.services.persistence.AttachmentStorageInternal import net.corda.node.services.statemachine.ExternalEvent import net.corda.node.services.statemachine.FlowStateMachineImpl import net.corda.nodeapi.internal.persistence.CordaPersistence -import java.lang.IllegalStateException import java.security.SignatureException import java.util.ArrayList import java.util.Collections @@ -88,6 +87,7 @@ interface ServiceHubInternal : ServiceHubCoreInternal { stateMachineRecordedTransactionMapping: StateMachineRecordedTransactionMappingStorage, vaultService: VaultServiceInternal, database: CordaPersistence, + disableSoftLocking: Boolean = false, updateFn: (SignedTransaction) -> Boolean = validatedTransactions::addTransaction ) { @@ -147,7 +147,7 @@ interface ServiceHubInternal : ServiceHubCoreInternal { // // Because the primary use case for recording irrelevant states is observer/regulator nodes, who are unlikely // to make writes to the ledger very often or at all, we choose to punt this issue for the time being. - vaultService.notifyAll(statesToRecord, recordedTransactions.map { it.coreTransaction }, previouslySeenTxs.map { it.coreTransaction }) + vaultService.notifyAll(statesToRecord, recordedTransactions.map { it.coreTransaction }, previouslySeenTxs.map { it.coreTransaction }, disableSoftLocking) } } @@ -202,15 +202,14 @@ interface ServiceHubInternal : ServiceHubCoreInternal { @Suppress("NestedBlockDepth") @VisibleForTesting - fun recordTransactions(statesToRecord: StatesToRecord, txs: Iterable, disableSignatureVerification: Boolean) { + fun recordTransactions(statesToRecord: StatesToRecord, txs: Iterable, disableSignatureVerification: Boolean, disableSoftLocking: Boolean = false) { txs.forEach { requireSupportedHashType(it) if (it.coreTransaction is WireTransaction) { if (disableSignatureVerification) { log.warnOnce("The current usage of recordTransactions is unsafe." + "Recording transactions without signature verification may lead to severe problems with ledger consistency.") - } - else { + } else { try { it.verifyRequiredSignatures() } @@ -226,7 +225,8 @@ interface ServiceHubInternal : ServiceHubCoreInternal { validatedTransactions, stateMachineRecordedTransactionMapping, vaultService, - database + database, + disableSoftLocking ) } diff --git a/node/src/main/kotlin/net/corda/node/services/api/VaultServiceInternal.kt b/node/src/main/kotlin/net/corda/node/services/api/VaultServiceInternal.kt index e95ba4b015..3c54a3321d 100644 --- a/node/src/main/kotlin/net/corda/node/services/api/VaultServiceInternal.kt +++ b/node/src/main/kotlin/net/corda/node/services/api/VaultServiceInternal.kt @@ -15,7 +15,8 @@ interface VaultServiceInternal : VaultService { * indicate whether an update consists entirely of regular or notary change transactions, which may require * different processing logic. */ - fun notifyAll(statesToRecord: StatesToRecord, txns: Iterable, previouslySeenTxns: Iterable = emptyList()) + fun notifyAll(statesToRecord: StatesToRecord, txns: Iterable, previouslySeenTxns: Iterable = emptyList(), + disableSoftLocking: Boolean = false) /** * Same as notifyAll but with a single transaction. diff --git a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt index a2623b638d..e0fdce29c0 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt @@ -69,7 +69,8 @@ import java.security.PublicKey import java.sql.SQLException import java.time.Clock import java.time.Instant -import java.util.* +import java.util.Arrays +import java.util.UUID import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.CopyOnWriteArraySet import java.util.stream.Stream @@ -80,8 +81,6 @@ import javax.persistence.criteria.CriteriaQuery import javax.persistence.criteria.CriteriaUpdate import javax.persistence.criteria.Predicate import javax.persistence.criteria.Root -import kotlin.collections.ArrayList -import kotlin.collections.LinkedHashSet import kotlin.collections.component1 import kotlin.collections.component2 @@ -284,12 +283,13 @@ class NodeVaultService( internal val publishUpdates get() = mutex.locked { updatesPublisher } /** Groups adjacent transactions into batches to generate separate net updates per transaction type. */ - override fun notifyAll(statesToRecord: StatesToRecord, txns: Iterable, previouslySeenTxns: Iterable) { + override fun notifyAll(statesToRecord: StatesToRecord, txns: Iterable, previouslySeenTxns: Iterable, + disableSoftLocking: Boolean) { if (statesToRecord == StatesToRecord.NONE || (!txns.any() && !previouslySeenTxns.any())) return val batch = mutableListOf() fun flushBatch(previouslySeen: Boolean) { - val updates = makeUpdates(batch, statesToRecord, previouslySeen) + val updates = makeUpdates(batch, statesToRecord, previouslySeen, disableSoftLocking) processAndNotify(updates) batch.clear() } @@ -308,7 +308,8 @@ class NodeVaultService( processTransactions(txns, false) } - private fun makeUpdates(batch: Iterable, statesToRecord: StatesToRecord, previouslySeen: Boolean): List> { + @Suppress("ComplexMethod", "ThrowsCount") + private fun makeUpdates(batch: Iterable, statesToRecord: StatesToRecord, previouslySeen: Boolean, disableSoftLocking: Boolean): List> { fun withValidDeserialization(list: List, txId: SecureHash): Map { var error: TransactionDeserialisationException? = null @@ -320,13 +321,15 @@ class NodeVaultService( // This will cause a failure as we can't deserialize such states in the context of the `appClassloader`. // For now we ignore these states. // In the future we will use the AttachmentsClassloader to correctly deserialize and asses the relevancy. - if (IGNORE_TRANSACTION_DESERIALIZATION_ERRORS) { + // Disabled if soft locking disabled, as assumes you are in the back chain and that maybe it is less important than top + // level transaction. + if (IGNORE_TRANSACTION_DESERIALIZATION_ERRORS || disableSoftLocking) { log.warnOnce("The current usage of transaction deserialization for the vault is unsafe." + "Ignoring vault updates due to failed deserialized states may lead to severe problems with ledger consistency. ") log.warn("Could not deserialize state $idx from transaction $txId. Cause: $e") } else { log.error("Could not deserialize state $idx from transaction $txId. Cause: $e") - if(error == null) error = e + if (error == null) error = e } null } From 8f87b2060be4ea14e986fd706e1503675e2c3503 Mon Sep 17 00:00:00 2001 From: Jose Coll Date: Thu, 30 Nov 2023 19:13:34 +0000 Subject: [PATCH 112/126] ENT-11245 Catch and transform new enum type value. (#7596) --- core/src/main/kotlin/net/corda/core/internal/FetchDataFlow.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/src/main/kotlin/net/corda/core/internal/FetchDataFlow.kt b/core/src/main/kotlin/net/corda/core/internal/FetchDataFlow.kt index 07564bcbf4..441a0e669a 100644 --- a/core/src/main/kotlin/net/corda/core/internal/FetchDataFlow.kt +++ b/core/src/main/kotlin/net/corda/core/internal/FetchDataFlow.kt @@ -80,7 +80,8 @@ sealed class FetchDataFlow( // against not having unknown by using the platform version as a guard. @CordaSerializationTransformEnumDefaults( CordaSerializationTransformEnumDefault("BATCH_TRANSACTION", "TRANSACTION"), - CordaSerializationTransformEnumDefault("UNKNOWN", "TRANSACTION") + CordaSerializationTransformEnumDefault("UNKNOWN", "TRANSACTION"), + CordaSerializationTransformEnumDefault("TRANSACTION_RECOVERY", "TRANSACTION") ) @CordaSerializable enum class DataType { From 835e95c222a35d5a5f0fde9530852e36b908e5b0 Mon Sep 17 00:00:00 2001 From: Ronan Browne Date: Sun, 12 May 2024 18:37:43 +0100 Subject: [PATCH 113/126] ES-2371: Consume H2 DB snapshot version --- build.gradle | 14 ++++++++++++++ constants.properties | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index c63f8e5370..51f3e45137 100644 --- a/build.gradle +++ b/build.gradle @@ -438,6 +438,20 @@ allprojects { includeGroup 'com.github.detro' } } + // required until h2database 2.2.229 is released + maven { + url "${artifactory_contextUrl}/corda-dependencies-dev" + authentication { + basic(BasicAuthentication) + } + credentials { + username = findProperty('cordaArtifactoryUsername') ?: System.getenv('CORDA_ARTIFACTORY_USERNAME') + password = findProperty('cordaArtifactoryPassword') ?: System.getenv('CORDA_ARTIFACTORY_PASSWORD') + } + content { + includeGroupByRegex 'com\\.h2database(\\..*)?' + } + } maven { url "${publicArtifactURL}/corda-dev" content { diff --git a/constants.properties b/constants.properties index 7db38579d3..fcdb5deb26 100644 --- a/constants.properties +++ b/constants.properties @@ -78,7 +78,7 @@ joptSimpleVersion=5.0.2 jansiVersion=1.18 hibernateVersion=5.6.14.Final # h2Version - Update docs if renamed or removed. -h2Version=2.2.224 +h2Version=2.2.229-SNAPSHOT rxjavaVersion=1.3.8 dokkaVersion=0.10.1 eddsaVersion=0.3.0 From 204a86d9a60339a3f0e9f079b61ab75fc0b5d648 Mon Sep 17 00:00:00 2001 From: Ronan Browne Date: Mon, 13 May 2024 14:17:44 +0100 Subject: [PATCH 114/126] ES-2371: Update version --- constants.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/constants.properties b/constants.properties index fcdb5deb26..b9697c4058 100644 --- a/constants.properties +++ b/constants.properties @@ -78,7 +78,7 @@ joptSimpleVersion=5.0.2 jansiVersion=1.18 hibernateVersion=5.6.14.Final # h2Version - Update docs if renamed or removed. -h2Version=2.2.229-SNAPSHOT +h2Version=2.2.224-R3 rxjavaVersion=1.3.8 dokkaVersion=0.10.1 eddsaVersion=0.3.0 From 4efda6d46b04af00bdc5701b47a3de8dbda60759 Mon Sep 17 00:00:00 2001 From: Ronan Browne Date: Mon, 13 May 2024 15:30:01 +0100 Subject: [PATCH 115/126] ES-2371: use public mirror --- build.gradle | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/build.gradle b/build.gradle index 51f3e45137..531d154f6a 100644 --- a/build.gradle +++ b/build.gradle @@ -438,16 +438,9 @@ allprojects { includeGroup 'com.github.detro' } } - // required until h2database 2.2.229 is released + // required until next version of h2database is released post 2.2.224 maven { - url "${artifactory_contextUrl}/corda-dependencies-dev" - authentication { - basic(BasicAuthentication) - } - credentials { - username = findProperty('cordaArtifactoryUsername') ?: System.getenv('CORDA_ARTIFACTORY_USERNAME') - password = findProperty('cordaArtifactoryPassword') ?: System.getenv('CORDA_ARTIFACTORY_PASSWORD') - } + url "${publicArtifactURL}/corda-dependencies-dev" content { includeGroupByRegex 'com\\.h2database(\\..*)?' } From b8947892bb49ac439bafa00fa7c30c901e08b04a Mon Sep 17 00:00:00 2001 From: snyk-bot Date: Wed, 22 May 2024 07:10:18 +0000 Subject: [PATCH 116/126] fix: release-tools/testing/requirements.txt to reduce vulnerabilities The following vulnerabilities are fixed by pinning transitive dependencies: - https://snyk.io/vuln/SNYK-PYTHON-REQUESTS-6928867 --- release-tools/testing/requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/release-tools/testing/requirements.txt b/release-tools/testing/requirements.txt index b4bc32760d..88053d96bf 100644 --- a/release-tools/testing/requirements.txt +++ b/release-tools/testing/requirements.txt @@ -1,3 +1,4 @@ jira==2.0.0 keyring==13.1.0 termcolor==1.1.0 +requests>=2.32.0 # not directly required, pinned by Snyk to avoid a vulnerability From 7209a4e25c17a15b39e37c1066c5ba219c70904a Mon Sep 17 00:00:00 2001 From: Adel El-Beik Date: Thu, 23 May 2024 16:07:45 +0100 Subject: [PATCH 117/126] ES-2371: Revert h2 data test change. Will be done in separate branch. --- build.gradle | 7 ------- constants.properties | 2 +- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/build.gradle b/build.gradle index 531d154f6a..c63f8e5370 100644 --- a/build.gradle +++ b/build.gradle @@ -438,13 +438,6 @@ allprojects { includeGroup 'com.github.detro' } } - // required until next version of h2database is released post 2.2.224 - maven { - url "${publicArtifactURL}/corda-dependencies-dev" - content { - includeGroupByRegex 'com\\.h2database(\\..*)?' - } - } maven { url "${publicArtifactURL}/corda-dev" content { diff --git a/constants.properties b/constants.properties index b9697c4058..7db38579d3 100644 --- a/constants.properties +++ b/constants.properties @@ -78,7 +78,7 @@ joptSimpleVersion=5.0.2 jansiVersion=1.18 hibernateVersion=5.6.14.Final # h2Version - Update docs if renamed or removed. -h2Version=2.2.224-R3 +h2Version=2.2.224 rxjavaVersion=1.3.8 dokkaVersion=0.10.1 eddsaVersion=0.3.0 From 9b0a3b1e085cb80ced8a2acb3846a798db3bf085 Mon Sep 17 00:00:00 2001 From: snyk-bot Date: Tue, 18 Jun 2024 17:33:47 +0000 Subject: [PATCH 118/126] fix: release-tools/testing/requirements.txt to reduce vulnerabilities The following vulnerabilities are fixed by pinning transitive dependencies: - https://snyk.io/vuln/SNYK-PYTHON-URLLIB3-7267250 --- release-tools/testing/requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/release-tools/testing/requirements.txt b/release-tools/testing/requirements.txt index b4bc32760d..5edbbb0855 100644 --- a/release-tools/testing/requirements.txt +++ b/release-tools/testing/requirements.txt @@ -1,3 +1,4 @@ jira==2.0.0 keyring==13.1.0 termcolor==1.1.0 +urllib3>=2.2.2 # not directly required, pinned by Snyk to avoid a vulnerability From a86853adbf46371de99a290ab97ba03f5d362376 Mon Sep 17 00:00:00 2001 From: chriscochrane Date: Fri, 12 Jul 2024 13:57:51 +0100 Subject: [PATCH 119/126] Dependency updates for security issues --- constants.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/constants.properties b/constants.properties index 7db38579d3..db3a76ac42 100644 --- a/constants.properties +++ b/constants.properties @@ -59,7 +59,7 @@ assertjVersion=3.12.2 slf4JVersion=1.7.30 log4JVersion=2.17.1 okhttpVersion=3.14.9 -nettyVersion=4.1.77.Final +nettyVersion=4.1.111.Final fileuploadVersion=1.4 kryoVersion=4.0.2 kryoSerializerVersion=0.43 @@ -94,7 +94,7 @@ ghostdriverVersion=2.1.0 jschVersion=0.1.55 # Override Artemis version protonjVersion=0.33.0 -snappyVersion=0.4 +snappyVersion=0.5 jcabiManifestsVersion=1.1 picocliVersion=3.9.6 commonsLangVersion=3.9 From 743b1d7fc89e8ecec2ab6ecba82ee8a023b2e8c1 Mon Sep 17 00:00:00 2001 From: chriscochrane Date: Mon, 15 Jul 2024 10:57:34 +0100 Subject: [PATCH 120/126] Upgrade Bouncy Castle --- constants.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/constants.properties b/constants.properties index db3a76ac42..c3019cdd44 100644 --- a/constants.properties +++ b/constants.properties @@ -24,7 +24,7 @@ jdkClassifier11=jdk11 dockerJavaVersion=3.2.5 proguardVersion=6.1.1 // bouncy castle version must not be changed on a patch release. Needs a full release test cycle to flush out any issues. -bouncycastleVersion=1.75 +bouncycastleVersion=1.78.1 classgraphVersion=4.8.135 disruptorVersion=3.4.2 typesafeConfigVersion=1.3.4 From e8b84e4a821acf377b9288b5c4e535bdd5dcf5e3 Mon Sep 17 00:00:00 2001 From: snyk-bot Date: Tue, 16 Jul 2024 03:16:29 +0000 Subject: [PATCH 121/126] fix: release-tools/testing/requirements.txt to reduce vulnerabilities The following vulnerabilities are fixed by pinning transitive dependencies: - https://snyk.io/vuln/SNYK-PYTHON-SETUPTOOLS-7448482 --- release-tools/testing/requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/release-tools/testing/requirements.txt b/release-tools/testing/requirements.txt index b4bc32760d..271a52a433 100644 --- a/release-tools/testing/requirements.txt +++ b/release-tools/testing/requirements.txt @@ -1,3 +1,4 @@ jira==2.0.0 keyring==13.1.0 termcolor==1.1.0 +setuptools>=70.0.0 # not directly required, pinned by Snyk to avoid a vulnerability From 060bdab88f3e6c85fc541763689dde143fb7ed86 Mon Sep 17 00:00:00 2001 From: chriscochrane Date: Tue, 16 Jul 2024 10:53:44 +0100 Subject: [PATCH 122/126] Restored previous version of netty --- constants.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/constants.properties b/constants.properties index c3019cdd44..bc542095ab 100644 --- a/constants.properties +++ b/constants.properties @@ -59,7 +59,7 @@ assertjVersion=3.12.2 slf4JVersion=1.7.30 log4JVersion=2.17.1 okhttpVersion=3.14.9 -nettyVersion=4.1.111.Final +nettyVersion=4.1.77.Final fileuploadVersion=1.4 kryoVersion=4.0.2 kryoSerializerVersion=0.43 From ca7cf73ceb1a897221e3c3db4e029d3c07e2f909 Mon Sep 17 00:00:00 2001 From: snyk-bot Date: Sat, 27 Jul 2024 07:20:27 +0000 Subject: [PATCH 123/126] fix: docker/src/docker/DockerfileAL to reduce vulnerabilities The following vulnerabilities are fixed with an upgrade: - https://snyk.io/vuln/SNYK-AMZN2-CPIO-6371135 - https://snyk.io/vuln/SNYK-AMZN2-GLIBCMINIMALLANGPACK-6745170 - https://snyk.io/vuln/SNYK-AMZN2-LIBNGHTTP2-6745071 - https://snyk.io/vuln/SNYK-AMZN2-NSS-6229002 - https://snyk.io/vuln/SNYK-AMZN2-NSSTOOLS-6229107 --- docker/src/docker/DockerfileAL | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/src/docker/DockerfileAL b/docker/src/docker/DockerfileAL index 816ef57027..11f3db261b 100644 --- a/docker/src/docker/DockerfileAL +++ b/docker/src/docker/DockerfileAL @@ -1,4 +1,4 @@ -FROM amazoncorretto:8u392-al2 +FROM amazoncorretto:8u422-al2 ## Add packages, clean cache, create dirs, create corda user and change ownership RUN yum -y install bash && \ From bd901b5121d73f36ad37bce80d66265b6d655d93 Mon Sep 17 00:00:00 2001 From: snyk-bot Date: Thu, 1 Aug 2024 05:15:49 +0000 Subject: [PATCH 124/126] fix: release-tools/testing/requirements.txt to reduce vulnerabilities The following vulnerabilities are fixed by pinning transitive dependencies: - https://snyk.io/vuln/SNYK-PYTHON-REQUESTS-6928867 - https://snyk.io/vuln/SNYK-PYTHON-SETUPTOOLS-3180412 - https://snyk.io/vuln/SNYK-PYTHON-SETUPTOOLS-7448482 - https://snyk.io/vuln/SNYK-PYTHON-URLLIB3-7267250 --- release-tools/testing/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/release-tools/testing/requirements.txt b/release-tools/testing/requirements.txt index ebec25c140..c6129d0ca0 100644 --- a/release-tools/testing/requirements.txt +++ b/release-tools/testing/requirements.txt @@ -2,5 +2,5 @@ jira==2.0.0 keyring==13.1.0 termcolor==1.1.0 urllib3>=2.2.2 # not directly required, pinned by Snyk to avoid a vulnerability -requests>=2.32.0 # not directly required, pinned by Snyk to avoid a vulnerability +requests>=2.32.2 # not directly required, pinned by Snyk to avoid a vulnerability setuptools>=70.0.0 # not directly required, pinned by Snyk to avoid a vulnerability From cc44c77ed49ea574b5af1148b89723217deb6aed Mon Sep 17 00:00:00 2001 From: Rick Parker Date: Thu, 15 Aug 2024 07:06:58 +0100 Subject: [PATCH 125/126] ENT-12108 ReceiveFinalityFlow returns a transaction that is not signed by the notary (#7804) --- .../test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt | 3 ++- .../main/kotlin/net/corda/core/flows/ReceiveTransactionFlow.kt | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt b/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt index 4eb85ec26b..9c4ecf4d99 100644 --- a/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt +++ b/core-tests/src/test/kotlin/net/corda/coretests/flows/FinalityFlowTests.kt @@ -518,7 +518,8 @@ class FinalityFlowTests : WithFinality { @Suspendable override fun call() { val handleNotaryError = otherSide.receive().unwrap { it } - subFlow(ReceiveFinalityFlow(otherSide, handlePropagatedNotaryError = handleNotaryError)) + val stx = subFlow(ReceiveFinalityFlow(otherSide, handlePropagatedNotaryError = handleNotaryError)) + stx.verify(serviceHub) } } diff --git a/core/src/main/kotlin/net/corda/core/flows/ReceiveTransactionFlow.kt b/core/src/main/kotlin/net/corda/core/flows/ReceiveTransactionFlow.kt index 3fcd20ca47..d109b53f48 100644 --- a/core/src/main/kotlin/net/corda/core/flows/ReceiveTransactionFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/ReceiveTransactionFlow.kt @@ -127,6 +127,7 @@ open class ReceiveTransactionFlow constructor(private val otherSideSession: Flow (serviceHub as ServiceHubCoreInternal).finalizeTransactionWithExtraSignatures(stx, notarySignatures, statesToRecord) logger.info("Peer finalised transaction with notary signature.") } + return stx + notarySignatures } catch (e: NotaryException) { logger.info("Peer received notary error.") val overrideHandlePropagatedNotaryError = handlePropagatedNotaryError From 0d85c6d27f51eda20299edf15ee81d0d9adc4ed3 Mon Sep 17 00:00:00 2001 From: Chris Cochrane <78791827+chriscochrane@users.noreply.github.com> Date: Thu, 19 Sep 2024 16:02:58 +0100 Subject: [PATCH 126/126] ENT-12094 - Upgraded sshd-common (#7810) --- testing/node-driver/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/node-driver/build.gradle b/testing/node-driver/build.gradle index 772813c41b..acf209a843 100644 --- a/testing/node-driver/build.gradle +++ b/testing/node-driver/build.gradle @@ -27,7 +27,7 @@ sourceSets { dependencies { compile project(':test-utils') - compile group: 'org.apache.sshd', name: 'sshd-common', version: '2.9.2' + compile group: 'org.apache.sshd', name: 'sshd-common', version: '2.13.2' // Integration test helpers testCompile "org.assertj:assertj-core:$assertj_version"

  • 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%0D9%N$(&d8R3O%mn${*|_x$z`9_G^j*BX>+Y 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 q$Q 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�N>Qlsr6dmQP;kH(S(vu6&e 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