From 6e6d8bf2f8ec7b4d20e36b5d4a951638d43eaeda Mon Sep 17 00:00:00 2001 From: AlphaSaraday Date: Thu, 6 Nov 2025 16:58:45 +0800 Subject: [PATCH] [20251106] add monitor module --- CHANGELOG.md | 170 +++++ acb-committeeptc/monitor-node-cli/README.md | 232 +++++++ .../monitor-node-cli/desc_tar.xml | 32 + acb-committeeptc/monitor-node-cli/pom.xml | 173 ++++++ .../committee/monitor/node/cli/Launcher.java | 59 ++ .../node/cli/commands/BaseCommands.java | 56 ++ .../node/cli/commands/CoreCommands.java | 183 ++++++ .../node/cli/commands/UtilsCommands.java | 94 +++ .../node/cli/config/CustomPromptProvider.java | 30 + .../src/main/resources/application.yml | 13 + .../src/main/resources/banner.txt | 7 + .../src/main/resources/start.sh | 63 ++ acb-committeeptc/monitor-node/.gitignore | 33 + .../.mvn/wrapper/maven-wrapper.properties | 19 + acb-committeeptc/monitor-node/README.md | 22 + acb-committeeptc/monitor-node/desc_tar.xml | 48 ++ acb-committeeptc/monitor-node/mvnw | 259 ++++++++ acb-committeeptc/monitor-node/mvnw.cmd | 149 +++++ acb-committeeptc/monitor-node/pom.xml | 235 +++++++ .../monitor/node/NodeApplication.java | 38 ++ .../CrossChainServiceGrpcClientManager.java | 101 +++ .../MonitorSystemGrpcClientManager.java | 41 ++ .../node/commons/core/MonitorContract.java | 18 + .../commons/core/MonitorSystemRespMsg.java | 26 + .../node/commons/enums/BCDNSStateEnum.java | 33 + .../node/commons/enums/MonitorTypeEnum.java | 21 + .../BlockStateNotValidatedYetException.java | 38 ++ .../exception/CommitteeNodeErrorCodeEnum.java | 55 ++ .../exception/CommitteeNodeException.java | 43 ++ .../CommitteeNodeInternalException.java | 34 + .../exception/DataAccessLayerException.java | 38 ++ .../exception/InvalidBtaException.java | 38 ++ .../InvalidConsensusStateException.java | 38 ++ .../InvalidCrossChainMessageException.java | 38 ++ .../exception/InvalidRequestException.java | 38 ++ .../node/commons/models/BCDNSServiceDO.java | 60 ++ .../node/commons/models/BtaWrapper.java | 52 ++ .../models/DomainSpaceCertWrapper.java | 46 ++ .../node/commons/models/TpBtaWrapper.java | 47 ++ .../ValidatedConsensusStateWrapper.java | 48 ++ .../monitor/node/config/CredentialConfig.java | 49 ++ .../monitor/node/config/ServerConfig.java | 56 ++ .../monitor/node/dal/convert/ConvertUtil.java | 132 ++++ .../node/dal/entities/BCDNSServiceEntity.java | 49 ++ .../monitor/node/dal/entities/BaseEntity.java | 58 ++ .../monitor/node/dal/entities/BtaEntity.java | 45 ++ .../dal/entities/DomainSpaceCertEntity.java | 45 ++ .../node/dal/entities/SystemConfigEntity.java | 38 ++ .../node/dal/entities/TpBtaEntity.java | 57 ++ .../ValidatedConsensusStatesEntity.java | 48 ++ .../node/dal/mapper/BCDNSServiceMapper.java | 23 + .../monitor/node/dal/mapper/BtaMapper.java | 23 + .../dal/mapper/DomainSpaceCertMapper.java | 23 + .../node/dal/mapper/SystemConfigMapper.java | 23 + .../monitor/node/dal/mapper/TpBtaMapper.java | 23 + .../ValidatedConsensusStatesMapper.java | 28 + .../dal/repository/BCDNSRepositoryImpl.java | 317 ++++++++++ .../EndorseServiceRepositoryImpl.java | 351 +++++++++++ .../repository/SystemConfigRepository.java | 138 +++++ .../interfaces/IBCDNSRepository.java | 56 ++ .../interfaces/IEndorseServiceRepository.java | 57 ++ .../interfaces/ISystemConfigRepository.java | 41 ++ .../monitor/node/server/AdminServiceImpl.java | 171 ++++++ .../node/server/MonitorNodeServiceImpl.java | 510 +++++++++++++++ .../node/server/MonitorOrderServiceImpl.java | 73 +++ .../interceptor/RequestTraceInterceptor.java | 33 + .../node/service/IBCDNSManageService.java | 71 +++ .../node/service/IEndorserService.java | 49 ++ .../node/service/IHcdvsPluginService.java | 52 ++ .../monitor/node/service/IMonitorService.java | 9 + .../node/service/IScheduledTaskService.java | 22 + .../node/service/impl/BCDNSManageService.java | 359 +++++++++++ .../service/impl/EndorserServiceImpl.java | 505 +++++++++++++++ .../service/impl/HcdvsPluginServiceImpl.java | 227 +++++++ .../node/service/impl/MonitorServiceImpl.java | 110 ++++ .../impl/ScheduledTaskServiceImpl.java | 81 +++ .../src/main/proto/admingrpc.proto | 74 +++ .../src/main/proto/monitorSystemgrpc.proto | 71 +++ .../src/main/proto/pluginserver.proto | 432 +++++++++++++ .../src/main/resources/application.yml | 59 ++ .../src/main/resources/banner.txt | 7 + .../monitor-node/src/main/resources/ddl.sql | 115 ++++ .../src/main/resources/logback-spring.xml | 123 ++++ .../main/resources/scripts/init_tls_certs.sh | 51 ++ .../resources/scripts/monitor-node.service | 28 + .../src/main/resources/scripts/print.sh | 73 +++ .../src/main/resources/scripts/start.sh | 139 +++++ .../src/main/resources/scripts/stop.sh | 35 ++ .../ptc/committee/monitor/node/TestBase.java | 192 ++++++ .../monitor/node/dal/BCDNSRepositoryTest.java | 125 ++++ .../dal/EndorseServiceRepositoryTest.java | 162 +++++ .../node/dal/SystemConfigRepositoryTest.java | 73 +++ .../node/server/AdminServiceImplTest.java | 216 +++++++ .../node/server/MonitorNodeServiceTest.java | 580 ++++++++++++++++++ .../node/server/MonitorOrderServiceTest.java | 4 + .../node/service/BCDNSManageServiceTest.java | 133 ++++ .../node/service/EndorserServiceTest.java | 401 ++++++++++++ .../node/service/HCDVSServiceTest.java | 88 +++ .../src/test/resources/application-test.yml | 43 ++ .../src/test/resources/data/ddl.sql | 93 +++ .../src/test/resources/data/drop_all.sql | 1 + .../src/test/resources/private_key.pem | 5 + .../monitor-node/src/test/resources/ptc.crt | 13 + .../src/test/resources/public_key.pem | 4 + acb-committeeptc/pom.xml | 2 + .../server/CrossChainServiceImpl.java | 102 +++ .../server/exception/ServerErrorCodeEnum.java | 12 + .../src/main/proto/pluginserver.proto | 49 ++ .../relayer/commons/model/BlockchainMeta.java | 4 + .../manager/bbc/IMonitorClientContract.java | 13 + .../manager/bbc/ISDPMsgClientContract.java | 2 + ...torClientContractHeteroBlockchainImpl.java | 34 + .../bbc/SDPMsgClientHeteroBlockchainImpl.java | 5 + .../manager/blockchain/BlockchainManager.java | 33 + .../blockchain/AbstractBlockchainClient.java | 3 + .../blockchain/HeteroBlockchainClient.java | 8 + .../network/ws/client/generated/Request.java | 8 +- .../ws/client/generated/RequestResponse.java | 8 +- .../pluginserver/GRpcBBCServiceClient.java | 90 +++ .../r-core/src/main/proto/pluginserver.proto | 49 ++ .../facade/admin/types/SysContractsInfo.java | 3 + .../admin/impl/BlockchainNamespace.java | 6 + .../commons/bbc/AbstractBBCContext.java | 5 + .../bbc/syscontract/MonitorContract.java | 13 + .../core/monitor/AbstractMonitorMessage.java | 25 + .../core/monitor/AbstractMonitorOrder.java | 74 +++ .../commons/core/monitor/IMonitorMessage.java | 12 + .../commons/core/monitor/IMonitorOrder.java | 25 + .../core/monitor/MonitorMessageFactory.java | 39 ++ .../core/monitor/MonitorMessageV1.java | 109 ++++ .../core/monitor/MonitorOrderFactory.java | 41 ++ .../commons/core/monitor/MonitorOrderV1.java | 123 ++++ .../exception/CommonsErrorCodeEnum.java | 17 +- .../core/write/IAntChainBridgeDataWriter.java | 6 +- .../spi/bbc/core/write/IMonitorWriter.java | 18 + .../spi/bbc/core/write/ISDPWriter.java | 2 + .../ethereum2/offchain-plugin/pom.xml | 2 + .../plugins/ethereum2/EthereumBBCService.java | 204 +++++- .../ethereum2/conf/EthereumConfig.java | 3 + .../plugins/ethereum2/core/AcbEthClient.java | 419 ++++++++++++- .../ethereum2/EthereumBBCServiceTest.java | 295 ++++++++- .../solidity/sys/AppContract.sol | 23 +- .../solidity/sys/CommitteePtcVerifier.sol | 4 +- .../onchain-plugin/solidity/sys/Monitor.sol | 298 +++++++++ .../solidity/sys/MonitorVerifier.sol | 94 +++ .../onchain-plugin/solidity/sys/PtcHub.sol | 15 +- .../onchain-plugin/solidity/sys/SDPMsg.sol | 33 +- .../sys/interfaces/IContractUsingMonitor.sol | 10 + .../solidity/sys/interfaces/IMonitor.sol | 77 +++ .../sys/interfaces/IMonitorVerifier.sol | 32 + .../solidity/sys/interfaces/IPtcHub.sol | 2 + .../solidity/sys/interfaces/IPtcVerifier.sol | 2 +- .../solidity/sys/interfaces/ISDPMessage.sol | 4 +- .../solidity/sys/interfaces/ISubProtocol.sol | 2 + .../solidity/sys/lib/monitor/MonitorLib.sol | 136 ++++ .../solidity/sys/lib/ptc/CommitteeLib.sol | 71 ++- ...6\346\265\201\347\250\213\345\233\276.png" | Bin 0 -> 385054 bytes ...0\346\201\257\347\273\223\346\236\204.png" | Bin 0 -> 40790 bytes ...e\347\232\204\347\273\223\346\236\204.png" | Bin 0 -> 15492 bytes 159 files changed, 12658 insertions(+), 68 deletions(-) create mode 100644 CHANGELOG.md create mode 100755 acb-committeeptc/monitor-node-cli/README.md create mode 100755 acb-committeeptc/monitor-node-cli/desc_tar.xml create mode 100755 acb-committeeptc/monitor-node-cli/pom.xml create mode 100755 acb-committeeptc/monitor-node-cli/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/cli/Launcher.java create mode 100755 acb-committeeptc/monitor-node-cli/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/cli/commands/BaseCommands.java create mode 100755 acb-committeeptc/monitor-node-cli/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/cli/commands/CoreCommands.java create mode 100755 acb-committeeptc/monitor-node-cli/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/cli/commands/UtilsCommands.java create mode 100755 acb-committeeptc/monitor-node-cli/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/cli/config/CustomPromptProvider.java create mode 100755 acb-committeeptc/monitor-node-cli/src/main/resources/application.yml create mode 100755 acb-committeeptc/monitor-node-cli/src/main/resources/banner.txt create mode 100755 acb-committeeptc/monitor-node-cli/src/main/resources/start.sh create mode 100755 acb-committeeptc/monitor-node/.gitignore create mode 100755 acb-committeeptc/monitor-node/.mvn/wrapper/maven-wrapper.properties create mode 100755 acb-committeeptc/monitor-node/README.md create mode 100755 acb-committeeptc/monitor-node/desc_tar.xml create mode 100755 acb-committeeptc/monitor-node/mvnw create mode 100755 acb-committeeptc/monitor-node/mvnw.cmd create mode 100755 acb-committeeptc/monitor-node/pom.xml create mode 100755 acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/NodeApplication.java create mode 100755 acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/client/CrossChainServiceGrpcClientManager.java create mode 100755 acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/client/MonitorSystemGrpcClientManager.java create mode 100755 acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/commons/core/MonitorContract.java create mode 100755 acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/commons/core/MonitorSystemRespMsg.java create mode 100755 acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/commons/enums/BCDNSStateEnum.java create mode 100755 acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/commons/enums/MonitorTypeEnum.java create mode 100755 acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/commons/exception/BlockStateNotValidatedYetException.java create mode 100755 acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/commons/exception/CommitteeNodeErrorCodeEnum.java create mode 100755 acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/commons/exception/CommitteeNodeException.java create mode 100755 acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/commons/exception/CommitteeNodeInternalException.java create mode 100755 acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/commons/exception/DataAccessLayerException.java create mode 100755 acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/commons/exception/InvalidBtaException.java create mode 100755 acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/commons/exception/InvalidConsensusStateException.java create mode 100755 acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/commons/exception/InvalidCrossChainMessageException.java create mode 100755 acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/commons/exception/InvalidRequestException.java create mode 100755 acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/commons/models/BCDNSServiceDO.java create mode 100755 acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/commons/models/BtaWrapper.java create mode 100755 acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/commons/models/DomainSpaceCertWrapper.java create mode 100755 acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/commons/models/TpBtaWrapper.java create mode 100755 acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/commons/models/ValidatedConsensusStateWrapper.java create mode 100755 acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/config/CredentialConfig.java create mode 100755 acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/config/ServerConfig.java create mode 100755 acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/dal/convert/ConvertUtil.java create mode 100755 acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/dal/entities/BCDNSServiceEntity.java create mode 100755 acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/dal/entities/BaseEntity.java create mode 100755 acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/dal/entities/BtaEntity.java create mode 100755 acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/dal/entities/DomainSpaceCertEntity.java create mode 100755 acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/dal/entities/SystemConfigEntity.java create mode 100755 acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/dal/entities/TpBtaEntity.java create mode 100755 acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/dal/entities/ValidatedConsensusStatesEntity.java create mode 100755 acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/dal/mapper/BCDNSServiceMapper.java create mode 100755 acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/dal/mapper/BtaMapper.java create mode 100755 acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/dal/mapper/DomainSpaceCertMapper.java create mode 100755 acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/dal/mapper/SystemConfigMapper.java create mode 100755 acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/dal/mapper/TpBtaMapper.java create mode 100755 acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/dal/mapper/ValidatedConsensusStatesMapper.java create mode 100755 acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/dal/repository/BCDNSRepositoryImpl.java create mode 100755 acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/dal/repository/EndorseServiceRepositoryImpl.java create mode 100755 acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/dal/repository/SystemConfigRepository.java create mode 100755 acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/dal/repository/interfaces/IBCDNSRepository.java create mode 100755 acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/dal/repository/interfaces/IEndorseServiceRepository.java create mode 100755 acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/dal/repository/interfaces/ISystemConfigRepository.java create mode 100755 acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/server/AdminServiceImpl.java create mode 100755 acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/server/MonitorNodeServiceImpl.java create mode 100755 acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/server/MonitorOrderServiceImpl.java create mode 100755 acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/server/interceptor/RequestTraceInterceptor.java create mode 100755 acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/service/IBCDNSManageService.java create mode 100755 acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/service/IEndorserService.java create mode 100755 acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/service/IHcdvsPluginService.java create mode 100755 acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/service/IMonitorService.java create mode 100755 acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/service/IScheduledTaskService.java create mode 100755 acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/service/impl/BCDNSManageService.java create mode 100755 acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/service/impl/EndorserServiceImpl.java create mode 100755 acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/service/impl/HcdvsPluginServiceImpl.java create mode 100755 acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/service/impl/MonitorServiceImpl.java create mode 100755 acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/service/impl/ScheduledTaskServiceImpl.java create mode 100755 acb-committeeptc/monitor-node/src/main/proto/admingrpc.proto create mode 100755 acb-committeeptc/monitor-node/src/main/proto/monitorSystemgrpc.proto create mode 100755 acb-committeeptc/monitor-node/src/main/proto/pluginserver.proto create mode 100755 acb-committeeptc/monitor-node/src/main/resources/application.yml create mode 100755 acb-committeeptc/monitor-node/src/main/resources/banner.txt create mode 100755 acb-committeeptc/monitor-node/src/main/resources/ddl.sql create mode 100755 acb-committeeptc/monitor-node/src/main/resources/logback-spring.xml create mode 100755 acb-committeeptc/monitor-node/src/main/resources/scripts/init_tls_certs.sh create mode 100755 acb-committeeptc/monitor-node/src/main/resources/scripts/monitor-node.service create mode 100755 acb-committeeptc/monitor-node/src/main/resources/scripts/print.sh create mode 100755 acb-committeeptc/monitor-node/src/main/resources/scripts/start.sh create mode 100755 acb-committeeptc/monitor-node/src/main/resources/scripts/stop.sh create mode 100755 acb-committeeptc/monitor-node/src/test/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/TestBase.java create mode 100755 acb-committeeptc/monitor-node/src/test/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/dal/BCDNSRepositoryTest.java create mode 100755 acb-committeeptc/monitor-node/src/test/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/dal/EndorseServiceRepositoryTest.java create mode 100755 acb-committeeptc/monitor-node/src/test/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/dal/SystemConfigRepositoryTest.java create mode 100755 acb-committeeptc/monitor-node/src/test/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/server/AdminServiceImplTest.java create mode 100755 acb-committeeptc/monitor-node/src/test/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/server/MonitorNodeServiceTest.java create mode 100755 acb-committeeptc/monitor-node/src/test/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/server/MonitorOrderServiceTest.java create mode 100755 acb-committeeptc/monitor-node/src/test/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/service/BCDNSManageServiceTest.java create mode 100755 acb-committeeptc/monitor-node/src/test/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/service/EndorserServiceTest.java create mode 100755 acb-committeeptc/monitor-node/src/test/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/service/HCDVSServiceTest.java create mode 100755 acb-committeeptc/monitor-node/src/test/resources/application-test.yml create mode 100755 acb-committeeptc/monitor-node/src/test/resources/data/ddl.sql create mode 100755 acb-committeeptc/monitor-node/src/test/resources/data/drop_all.sql create mode 100755 acb-committeeptc/monitor-node/src/test/resources/private_key.pem create mode 100755 acb-committeeptc/monitor-node/src/test/resources/ptc.crt create mode 100755 acb-committeeptc/monitor-node/src/test/resources/public_key.pem create mode 100755 acb-relayer/r-core/src/main/java/com/alipay/antchain/bridge/relayer/core/manager/bbc/IMonitorClientContract.java create mode 100755 acb-relayer/r-core/src/main/java/com/alipay/antchain/bridge/relayer/core/manager/bbc/MonitorClientContractHeteroBlockchainImpl.java create mode 100755 acb-sdk/antchain-bridge-commons/src/main/java/com/alipay/antchain/bridge/commons/bbc/syscontract/MonitorContract.java create mode 100755 acb-sdk/antchain-bridge-commons/src/main/java/com/alipay/antchain/bridge/commons/core/monitor/AbstractMonitorMessage.java create mode 100644 acb-sdk/antchain-bridge-commons/src/main/java/com/alipay/antchain/bridge/commons/core/monitor/AbstractMonitorOrder.java create mode 100755 acb-sdk/antchain-bridge-commons/src/main/java/com/alipay/antchain/bridge/commons/core/monitor/IMonitorMessage.java create mode 100644 acb-sdk/antchain-bridge-commons/src/main/java/com/alipay/antchain/bridge/commons/core/monitor/IMonitorOrder.java create mode 100755 acb-sdk/antchain-bridge-commons/src/main/java/com/alipay/antchain/bridge/commons/core/monitor/MonitorMessageFactory.java create mode 100755 acb-sdk/antchain-bridge-commons/src/main/java/com/alipay/antchain/bridge/commons/core/monitor/MonitorMessageV1.java create mode 100644 acb-sdk/antchain-bridge-commons/src/main/java/com/alipay/antchain/bridge/commons/core/monitor/MonitorOrderFactory.java create mode 100644 acb-sdk/antchain-bridge-commons/src/main/java/com/alipay/antchain/bridge/commons/core/monitor/MonitorOrderV1.java create mode 100755 acb-sdk/antchain-bridge-spi/src/main/java/com/alipay/antchain/bridge/plugins/spi/bbc/core/write/IMonitorWriter.java create mode 100755 acb-sdk/pluginset/ethereum2/onchain-plugin/solidity/sys/Monitor.sol create mode 100755 acb-sdk/pluginset/ethereum2/onchain-plugin/solidity/sys/MonitorVerifier.sol create mode 100755 acb-sdk/pluginset/ethereum2/onchain-plugin/solidity/sys/interfaces/IContractUsingMonitor.sol create mode 100755 acb-sdk/pluginset/ethereum2/onchain-plugin/solidity/sys/interfaces/IMonitor.sol create mode 100755 acb-sdk/pluginset/ethereum2/onchain-plugin/solidity/sys/interfaces/IMonitorVerifier.sol create mode 100755 acb-sdk/pluginset/ethereum2/onchain-plugin/solidity/sys/lib/monitor/MonitorLib.sol create mode 100644 "docs/images/\345\220\253\347\233\221\347\256\241\347\232\204\350\267\250\351\223\276\346\265\201\347\250\213\345\233\276.png" create mode 100644 "docs/images/\345\220\253\347\233\221\347\256\241\347\232\204\350\267\250\351\223\276\346\266\210\346\201\257\347\273\223\346\236\204.png" create mode 100644 "docs/images/\347\233\221\347\256\241\346\214\207\344\273\244\347\261\273\345\236\213monitorOrderType\347\232\204\347\273\223\346\236\204.png" diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..a53d9de4 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,170 @@ +# [2025-11-06] 本PR所有改动总结 + +## acb-committeeptc +### monitor-node +复制node模块并进行修改,改动如下: +- monitor.node.client + - 与pluginserver和监管系统进行GPRC通信的客户端 +- monitor.node.commons.core 与 monitor.node.commons.enums + - 增加监管功能所需的一些配置 +- monitor.node.server + - 在MonitorNodeServiceImpl.verifyCrossChainMessage服务中增加事中监管功能 + - 新增MonitorOrderServiceImpl服务,完成接收监管指令功能 +- monitor.node.service + - 完成上述功能的内部实现 + +### monitor-node-cli +复制node-cli模块,并只修改了配置文件以适配新模块名。 + +## acb-relayer +- 优化r-cli的命令**setup-contract** + - 能够一键部署AM SDP PTC Monitor相关的所有合约并完成初始化 +- 优化r-cli的命令**get-blockchain-contracts** + - 能够查询AM SDP 以及Monitor合约的地址,其中Monitor合约地址将代替SDP合约地址,在DAPP合约中进行初始化 + +## acb-pluginserver +增加了如下接口功能(BBC插件的新增功能与之对应) +- **SetupMonitorMessage** + - 部署Monitor合约和MonitorVerifer合约,并在Monitor合约中初始化MonitorVerifer合约的地址 +- **SetMonitorContract** + - 在SDP合约中初始化Monitor合约地址 +- **SetProtocolInMonitor** + - 在Monitor合约中初始化SDP合约地址 +- **SetMonitorControl** + - 在Monitor合约中初始化变量monitorControl,控制监管开关(默认开) +- **SetPtcHubInMonitorVerifier** + - 在MonitorVerifer合约中初始化PtcHub合约的地址 +- **RelayMonitorOrder** + - 将监管指令发送到Monitor合约 + +## acb-sdk +- antchain.bridge.commons + - 在BBCConext增加了监管合约地址和状态 + - 增加了对监管合约的序列化和反序列化 +- antchain.bridge.spi + - 增加了监管合约所需接口 +- pluginset.ethereum2 + - offchain + - 增加了支持监管合约部署、相关初始化、发送监管指令等接口 + - onchain + - 新增monitor合约:在dapp和sdp合约层之间,完成事前监管,并能接收和存储监管指令,与monitorVerifer合约交互完成监管节点签名的验证 + - 新增monitorVerifer合约,与monitor交互;接收跨链消息时从ptcHub合约获取监管节点签名 + - 删除了sys/lib/ptc下的CommitteePtcVerifier.sol,原因如下: + - 并没有其他合约import该合约, + - 该合约的功能在同目录下的CommitteeLib.sol中已经实现 + - 在添加功能代码时,如果不删除会导致编译失败 + + +## 注意事项 +**如果需要对一条链chain-B下达监管指令,该链必须先接收一条跨链消息。** 原因如下: +- 监管指令上链时,是由committeeptc中的监管节点直接调用pluginserver的BBC服务来发送交易,不会经过relayer +- 监管指令在链上需要验证监管节点签名来保证指令的真实性,所以链上需事先存储监管节点的公钥 +- 监管节点的公钥和其他节点一样,存储在ptchub合约接收的的tpbta中 +- 按照antchain的设计,当relayer接收到一条目的链为chain-B的跨链消息时,会检查是否已经上传chain-B最新的tpbta到链上,如果没有则上传 +- 所以如果chain-B没有接收过跨链消息,ptchub合约上就不会存储tpbta,从而无法获取监管节点公钥,无法完成监管指令的签名验证,导致无法成功接收监管指令 + + +## 含监管的流程图示 +- 下图为含监管的跨链流程图: + +![](docs/images/含监管的跨链流程图.png) + + +## 其他说明 + +### 跨链消息结构设计变更说明 +监管合约作为SDP上层合约,封装DApp消息的同时增加监管字段monitor_type和监管信息monitor_msg。 + + + +| monitor_type值(uint32类型) | monitor_type含义 | monitor_msg含义(string类型) | +| ---------------------- | ------------------------------------------------------------ | --------------------------- | +| 1 | 发送方发出的不要求监管的跨链消息 | 可选 | +| 2 | 对于发送方:发出要求监管的跨链消息对于接收方:成功接收到带监管的跨链消息 | 可选 | +| 3 | 监管未通过,回滚到发送方的监管回滚消息 | 可选,如监管未通过的原因 | + + +### 背书策略配置说明 +为加入antchain的区块链配置背书策略时,监管节点需要设置为true,举例说明如下。 +- 当监管开启时,监管节点会向监管系统请求跨链消息的合法性,**合法则返回一个正确签名,不合法则返回一个空签名**。 +- 当监管关闭时,监管节点的运行逻辑和其他节点完全相同,只是不会在PtcHub合约与monitorVerifier合约进行签名验证。 +``` +{ + "committee_id": "default", + "endorsers": [ + { + "node_id": "node1", + "node_public_key": { + "key_id": "default", + "public_key": "" + }, + "required": true + }, + { + "node_id": "monitor-node", + "node_public_key": { + "key_id": "default", + "public_key": "" + }, + "required": true + } + ], + "policy": { + "threshold": ">=0" + } +} +``` + + +### 监管指令结构说明 +acb-committeeptc/monitor-node/src/main/proto/monitorSystemgrpc.proto中的监管指令结构如下: +``` +message MonitorOrder { + string product = 1; + string domain = 2; + uint64 monitorOrderType = 3; + string senderDomain = 4; + string fromAddress = 5; + string receiverDomain = 6; + string toAddress = 7; + string transactionContent = 8; + string extra = 9; +} +``` +监管节点会解析出监管指令各字段,并构造包含监管指令的交易发送到指定区块链的监管合约,最终由监管合约更新监管规则。该结构各字段含义如下: +- product + - 监管指令要下发到的区块链的类型,例如etherum2,fiscobcos等 +- domain + - 监管指令要下发到的区块链的域名 +- monitorOrderType + - 监管指令的类型。该字段长度为32bit,采用了分层编码的设计方式(如下图),分为主类型和子类型,每种主类型标识一种监管维度,每种主类型下分多种子类型 + - 在当前设计中,每个主类型占1bit,每个子类型占3bit,即每种主类型共有8种子类型 + - 主类型的具体含义由监管系统定义。以“黑名单”作为主类型来举例,该主类型的子类型可以包含: + - 禁止本区块链的应用合约a发送跨链交易; + - 禁止本区块链向区块链B的应用合约b发送跨链交易; + - 禁止本区块链向区块链B发送跨链交易等。 + + + +- senderDomain + - 跨链过程中源区块链域名,处理方式随monitorOrderType含义而变化 + - 例如监管指令是“禁止某区块链域名发送跨链消息”,则监管合约会把senderDomain加入黑名单,最终效果为该区块链无法在跨链系统发送跨链消息。 +- fromAddress + - 跨链过程中源区块链的应用合约地址,处理方式随monitorOrderType含义而变化 + - 例如监管指令是“禁止某区块链的某应用合约发送跨链消息”,则监管合约会把fromAddress加入黑名单,最终效果为区块链的该应用合约无法在该跨链系统发送跨链消息。 +- receiverDomain + - 跨链过程中目的区块链域名,处理方式随monitorOrderType含义而变化 +- toAddress + - 跨链过程中目的区块链的应用合约地址,处理方式随monitorOrderType含义而变化。 +- transactionContent + - 针对可能要对跨链过程中的原始跨链消息内容本身进行审查而设计了该字段,用于在链上审查跨链消息内容的合规性。 +- extra + - 额外信息。用于存放监管系统希望在链上存储的一些监管指令描述,或者上述字段未充分考虑的情况等,也可为空。 + +目前监管合约中对监管指令的支持,只完成了**合约黑名单**和**控制监管开关**两种功能。 +- 合约黑名单功能对应的监管指令,用二进制表示如下: + - **1000** 0000 0000 0000 0000 0000 0000 0000 + - 即32bit中第一对4bit组表示“黑名单”及其子类型。子类型"000"表示加入黑名单,"001"表示移除出黑名单。 +- 控制监管开关的监管指令,用二进制表示如下: + - 0000 **1000** 0000 0000 0000 0000 0000 0000 + - 即32bit中第二对4bit组表示“监管控制”及其子类型。子类型"000"表示关闭监管,"001"表示开启监管。 \ No newline at end of file diff --git a/acb-committeeptc/monitor-node-cli/README.md b/acb-committeeptc/monitor-node-cli/README.md new file mode 100755 index 00000000..663037e4 --- /dev/null +++ b/acb-committeeptc/monitor-node-cli/README.md @@ -0,0 +1,232 @@ +
+ am logo +

Monitor Node CLI

+
+ +# 介绍 + +Monitor Node CLI目前功能和Node CLI相同。 +Monitor Node CLI工具是用于管理Committee Monitor Node的交互式命令行工具,它可以完成BCDNS服务注册等工作。 + +# 使用 + +## 编译 + +**在开始之前,请您确保安装了maven和JDK,这里推荐使用[jdk-21](https://adoptium.net/zh-CN/temurin/releases/?version=21)版本** + +**确保安装了AntChain Bridge Plugin SDK,详情请[见](acb-sdk/README.md)** + +在monitor-node-cli模块根目录运行maven命令即可: + +```shell +cd monitor-node-cli && mvn package -Dmaven.test.skip=true +``` + +在`monitor-node-cli/target`目录下会生成一个压缩包`monitor-node-cli-bin.tar.gz`,解压该压缩包即可使用。 + +解压编译生成包后可以看到文件如下: + +``` +./monitor-node-cli +├── README.md +├── bin +│   └── start.sh +└── lib + └── monitor-node-cli.jar + +2 directories, 3 files +``` + +## 启动 + +查看脚本帮助信息: + +```shell +$ ./bin/start.sh -h + + start.sh - Start the AntChain Bridge Committee Node Command Line Interface Tool + + Usage: + start.sh + + Examples: + 1. start with the default server address `localhost` and default port `10088`: + start.sh + 2. start with specific server address and port: + start.sh -H 0.0.0.0 -p 10088 + + Options: + -H admin server host of committee node. + -p admin server port of committee node. + -h print help information. + +``` + +启动命令执行情况如下: + +```shell +$ ./r-cli/bin/start.sh + + _ _ ___ ____ _____ + | \ | | / _ \ | _ \ | ____| + | \| || | | || | | || _| + | |\ || |_| || |_| || |___ + |_| \_| \___/ |____/ |_____| + + CLI 0.1.0-SNAPSHOT + +monitor-node:> +``` + +启动成功后即可在`monitor-node:>`启动符后执行cli命令。 + +# 命令操作详情 + +- 直接输入`help`可以查看支持命令概况 +- 直接输入`version`可以查看当前中继CLI工具版本 +- 直接输入`history`可以查询历史命令记录 + +### add-ptc-trust-root + +手动增加PTC Trust Root 到Committee Node存储里。 + +参数: + +- rawPtcTrustRootFile:文件路径,指向包含Base64格式的序列化PTC Trust Root内容的文件; + +执行: + +``` +add-ptc-trust-root --rawPtcTrustRootFile /path/to/ptctrustroot-file +``` + +### register-bcdnsservice + +在Committee Node中,注册特定的BCDNS服务,即BCDNS的客户端。 + +命令参数如下: + +- `--bcdnsType`:(必选)BCDNS服务类型,提供`bif`和 `embedded`和两种类型。其中`bif`为目前可用的星火链网BCDNS服务,为中继外部依赖服务,`embedded`为嵌入式BCDNS服务(计划开发中,敬请期待); +- `--domainSpace`:(可选)当前中继服务的域名空间名,一个域名空间名绑定一个BCDNS服务,该项默认为空字符串,即当前中继的根域名空间名默认为空字符串; +- `--propFile`:(必选)配置文件路径,即初始化BCDNS服务存根所需的客户端配置文件路径,例如`/path/to/bif_bcdns_conf.json`,该配置文件可以使用`5.3 generate-bif-bcdns-conf`命令生成; +- `--bcdnsCertPath`:(可选)BCDNS服务的证书路径,可不提供,若未提供该证书命令执行注册时会向BCDNS服务请求证书。 + +用法如下: + +```shell +relayer:> register-bcdnsservice --bcdnsType bif --propFile /path/to/bif_bcdns_conf.json +success +``` + +### get-bcdnsservice + +用于查询指定域名空间中继所绑定的BCDNS服务信息。 + +命令参数如下: + +- `--domainSpace`:(可选)中继的域名空间名,该项默认为空字符串。 + +用法如下: + +```shell +# 当前中继域名空间名为空字符串,故直接使用默认域名空间名 +relayer:> get-bcdnsservice +{"domainSpace":"","domainSpaceCertWrapper":{"desc":"","domainSpace":"","domainSpaceCert":{"credentialSubject":"AADhAAAA...YTAifV19","credentialSubjectInstance":{"applicant":{"rawId":"ZGlkOmJp...RENwQw==","type":"BID"},"bcdnsRootOwner":{"$ref":"$.domainSpaceCertWrapper.domainSpaceCert.credentialSubjectInstance.applicant"},"bcdnsRootSubjectInfo":"eyJwdWJs...MCJ9XX0=","name":"root_verifiable_credential","rawSubjectPublicKey":"Q3hxGTc6...i3cJwqA=","subject":"eyJwdWJs...MCJ9XX0=","subjectPublicKey":{"algorithm":"Ed25519","encoded":"MCowBQYDK...i3cJwqA=","format":"X.509","pointEncoding":"Q3hxGTc6...i3cJwqA="}},"encodedToSign":"AACHAQAA...In1dfQ==","expirationDate":1733538286,"id":"did:bid:ef29QeET...2Mzdj8ph","issuanceDate":1702002286,"issuer":{"rawId":"ZGlkOmJp...ZUdNQw==","type":"BID"},"proof":{"certHash":"+9D7B4Eh...vA1cBaE=","hashAlgo":"SM3","rawProof":"RND0SpVq...C6aMDA==","sigAlgo":"Ed25519"},"type":"BCDNS_TRUST_ROOT_CERTIFICATE","version":"1"},"ownerOid":{"rawId":"ZGlkOmJp...RENwQw==","type":"BID"}},"ownerOid":{"rawId":"ZGlkOmJp...RENwQw==","type":"BID"},"properties":"ewogICJj...IH0KfQoK","state":"WORKING","type":"BIF"} +``` + +### delete-bcdnsservice + +用于删除指定域名空间的中继所绑定的BCDNS服务,删除后可重新绑定其他BCDNS服务。 + +命令参数如下: + +- `--domainSpace`:(可选)中继的域名空间名,该项默认为空字符串。 + +用法如下: +```shell +# 删除BCDNS服务 +relayer:> delete-bcdnsservice +success + +# 查询BCDNS服务 +relayer:> get-bcdnsservice +not found +``` + +### get-bcdnscertificate 查询BCDNS服务证书 + +用于查询指定域名空间的中继所绑定的BCDNS服务的证书。 + +命令参数如下: + +- `--domainSpace`:(可选)中继的域名空间名,该项默认为空字符串。 + +用法如下: + +```shell +relayer:> get-bcdnscertificate +-----BEGIN BCDNS TRUST ROOT CERTIFICATE----- +AAAVAgAAAAABAAAAMQEAKQAAAGRpZDpiaWQ6ZWYyOVFlRVRRcDVnOHdabXBLRTNR +...... +pp1tvNQJKwumjAw= +-----END BCDNS TRUST ROOT CERTIFICATE----- + +``` + +### stop-bcdnsservice 停止BCDNS服务 + +用于停止指定域名空间的中继所绑定的BCDNS服务的运行。 + +命令参数如下: + +- `--domainSpace`:(可选)中继的域名空间名,该项默认为空字符串。 + +用法如下: + +```shell +# 停止BCDNS服务 +relayer:> stop-bcdnsservice +success + +# 停止后查看BCDNS服务信息可以看到,信息详情中的状态为`FROZEN` +relayer:> get-bcdnsservice +{"domainSpace":"","domainSpaceCertWrapper":{......},"ownerOid":{......},"properties":"......","state":"FROZEN","type":"BIF"} +``` + +### restart-bcdnsservice 重启BCDNS服务 + +用于重新启动指定域名空间的中继所绑定的BCDNS服务。 + +命令参数如下: + +- `--domainSpace`:(可选)中继的域名空间名,该项默认为空字符串。 + +用法如下: + +```shell +# 重启BCDNS服务 +relayer:> restart-bcdnsservice +success + +# 停止后查看BCDNS服务信息可以看到,信息详情中的状态为`WORKING` +relayer:> get-bcdnsservice +{"domainSpace":"","domainSpaceCertWrapper":{......},"ownerOid":{......},"properties":"......","state":"WORKING","type":"BIF"} +``` + +### generate-node-account + +Committee Node初始化时,用来生成节点需要的私钥和公钥文件,后续将用来完成背书签名✍️。 + +参数: + +- keyAlgo:私钥的算法,支持:SECP256K1、RSA、ECDSA(secp256r1)、SM2、ED25519; +- outDir:密钥文件存储的文件夹路径,默认当前路径; + +用法如下: + +``` +node:> generate-node-account --keyAlgo SECP256K1 --outDir ./ +private key path: /path/to/./private_key.pem +public key path: /path/to/./public_key.pem +``` + diff --git a/acb-committeeptc/monitor-node-cli/desc_tar.xml b/acb-committeeptc/monitor-node-cli/desc_tar.xml new file mode 100755 index 00000000..47e0f299 --- /dev/null +++ b/acb-committeeptc/monitor-node-cli/desc_tar.xml @@ -0,0 +1,32 @@ + + bin + + tar.gz + + true + + + ${project.build.directory} + ${file.separator}lib + + monitor-node-cli.jar + + + + ${project.basedir}/src/main/resources + ${file.separator}bin + + *.sh + + + + ${project.basedir} + ${file.separator} + + README.md + + + + \ No newline at end of file diff --git a/acb-committeeptc/monitor-node-cli/pom.xml b/acb-committeeptc/monitor-node-cli/pom.xml new file mode 100755 index 00000000..38726a79 --- /dev/null +++ b/acb-committeeptc/monitor-node-cli/pom.xml @@ -0,0 +1,173 @@ + + + + + 4.0.0 + + com.alipay.antchain.bridge + committee-ptc + 0.1.0-SNAPSHOT + + + monitor-node-cli + 0.1.0-SNAPSHOT + monitor-node-cli + monitor node cli + + + 21 + 21 + UTF-8 + + + + + org.springframework.boot + spring-boot-devtools + runtime + true + + + org.springframework.shell + spring-shell-starter + + + cn.hutool + hutool-all + + + net.devh + grpc-client-spring-boot-starter + + + org.projectlombok + lombok + true + + + javax.annotation + javax.annotation-api + + + com.alipay.antchain.bridge + antchain-bridge-commons + + + org.bouncycastle + bcpkix-jdk18on + + + + + monitor-node-cli + + + kr.motd.maven + os-maven-plugin + 1.5.0.Final + + + + + src/main/resources + + **/application.yml + **/*.xml + **/banner.txt + + + true + + + + + org.xolstice.maven.plugins + protobuf-maven-plugin + ${protobuf-plugin.version} + + com.google.protobuf:protoc:${protobuf.version}:exe:${os.detected.classifier} + grpc-java + io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier} + + ${project.basedir}/../monitor-node/src/main/proto + + + **/admingrpc.proto + + + + + + compile + compile-custom + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + com.alipay.antchain.bridge.ptc.committee.monitor.node.cli.Launcher + + + + build-info + + build-info + + + + ${java.version} + ${project.description} + + + + + + repackage + + + + + + org.apache.maven.plugins + maven-assembly-plugin + 3.1.0 + + + + + desc_tar.xml + + + make-tar + package + + single + + + + + + + + \ No newline at end of file diff --git a/acb-committeeptc/monitor-node-cli/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/cli/Launcher.java b/acb-committeeptc/monitor-node-cli/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/cli/Launcher.java new file mode 100755 index 00000000..8bcdb224 --- /dev/null +++ b/acb-committeeptc/monitor-node-cli/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/cli/Launcher.java @@ -0,0 +1,59 @@ +/* + * Copyright 2023 Ant Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alipay.antchain.bridge.ptc.committee.monitor.node.cli; + +import java.util.ArrayList; +import java.util.List; + +import cn.hutool.core.collection.ListUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import org.springframework.boot.WebApplicationType; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.builder.SpringApplicationBuilder; + +@SpringBootApplication(scanBasePackages = {"com.alipay.antchain.bridge.ptc.committee.monitor.node"}) +public class Launcher { + public static void main(String[] args) { + List propList = new ArrayList<>(); + if (ObjectUtil.isNotEmpty(args)) { + + List argsList = ListUtil.toList(args); + + var port = argsList.stream().filter(x -> StrUtil.startWith(x, "--port")).findAny().orElse(""); + var portNum = 10088; + if (StrUtil.isNotEmpty(port)) { + portNum = Integer.parseInt(StrUtil.split(port, "=").get(1)); + } + + var host = argsList.stream().filter(x -> StrUtil.startWith(x, "--host")).findAny().orElse(""); + var hostVal = "127.0.0.1"; + if (StrUtil.isNotEmpty(host)) { + hostVal = StrUtil.split(host, "=").get(1); + } + + propList.add(StrUtil.format("grpc.client.admin.address=static://{}:{}", hostVal, portNum)); + + argsList = argsList.stream().filter(x -> !StrUtil.startWithAny(x, "--port", "--host")).toList(); + args = argsList.toArray(new String[0]); + } + new SpringApplicationBuilder(Launcher.class) + .web(WebApplicationType.NONE) + .properties(propList.toArray(new String[0])) + .run(args); + } +} diff --git a/acb-committeeptc/monitor-node-cli/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/cli/commands/BaseCommands.java b/acb-committeeptc/monitor-node-cli/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/cli/commands/BaseCommands.java new file mode 100755 index 00000000..54445927 --- /dev/null +++ b/acb-committeeptc/monitor-node-cli/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/cli/commands/BaseCommands.java @@ -0,0 +1,56 @@ +/* + * Copyright 2023 Ant Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alipay.antchain.bridge.ptc.committee.monitor.node.cli.commands; + +import java.io.IOException; +import java.net.Socket; + +import cn.hutool.core.util.StrUtil; +import org.springframework.shell.Availability; +import org.springframework.shell.standard.ShellMethodAvailability; + +public abstract class BaseCommands { + + public abstract String getAdminAddress(); + + public abstract boolean needAdminServer(); + + @ShellMethodAvailability + public Availability baseAvailability() { + if (needAdminServer()) { + var addrArr = StrUtil.split(StrUtil.split(getAdminAddress(), "//").get(1), ":"); + + if (!checkServerStatus(addrArr.get(0), Integer.parseInt(addrArr.get(1)))) { + return Availability.unavailable( + StrUtil.format("admin server {} is unreachable", getAdminAddress()) + ); + } + } + + return Availability.available(); + } + + private boolean checkServerStatus(String host, int port) { + try { + Socket socket = new Socket(host, port); + socket.close(); + return true; + } catch (IOException e) { + return false; + } + } +} diff --git a/acb-committeeptc/monitor-node-cli/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/cli/commands/CoreCommands.java b/acb-committeeptc/monitor-node-cli/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/cli/commands/CoreCommands.java new file mode 100755 index 00000000..ffcf23c9 --- /dev/null +++ b/acb-committeeptc/monitor-node-cli/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/cli/commands/CoreCommands.java @@ -0,0 +1,183 @@ +/* + * Copyright 2024 Ant Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alipay.antchain.bridge.ptc.committee.monitor.node.cli.commands; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import cn.hutool.core.util.StrUtil; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.server.grpc.*; +import com.google.protobuf.ByteString; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import net.devh.boot.grpc.client.inject.GrpcClient; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.shell.standard.*; + +@Getter +@ShellCommandGroup(value = "Commands about core functions") +@ShellComponent +@Slf4j +public class CoreCommands extends BaseCommands { + + @Value("${grpc.client.admin.address:static://localhost:10088}") + private String adminAddress; + + @GrpcClient("admin") + private AdminServiceGrpc.AdminServiceBlockingStub adminServiceBlockingStub; + + @Override + public boolean needAdminServer() { + return true; + } + + @ShellMethod(value = "Register a new BCDNS bound with specified domain space into Relayer") + Object registerBCDNSService( + @ShellOption(help = "The domain space owned by the BCDNS, default the root space \"\"", defaultValue = "") String domainSpace, + @ShellOption(help = "The type of the BCDNS, e.g. embedded, bif") String bcdnsType, + @ShellOption(valueProvider = FileValueProvider.class, help = "The properties file path needed to initialize the service stub, e.g. /path/to/bif_bcdns_conf.json") String propFile, + @ShellOption(valueProvider = FileValueProvider.class, help = "The path to BCDNS trust root certificate file if you have it", defaultValue = "") String bcdnsCertPath + ) { + try { + var resp = adminServiceBlockingStub.registerBcdnsService( + RegisterBcdnsServiceRequest.newBuilder() + .setDomainSpace(domainSpace) + .setBcdnsType(bcdnsType) + .setConfig(ByteString.copyFrom(Files.readAllBytes(Paths.get(propFile)))) + .setBcdnsRootCert(StrUtil.isEmpty(bcdnsCertPath) ? "" : Files.readString(Paths.get(bcdnsCertPath))) + .build() + ); + if (resp.getCode() != 0) { + return "failed to register BCDNS service: " + resp.getErrorMsg(); + } + return "success"; + } catch (Throwable t) { + throw new RuntimeException("unexpected error please input stacktrace to check the detail", t); + } + } + + @ShellMethod(value = "Get the BCDNS data bound with specified domain space") + Object getBCDNSService(@ShellOption(help = "The domain space bound with BCDNS, default the root space", defaultValue = "") String domainSpace) { + try { + var resp = adminServiceBlockingStub.getBcdnsServiceInfo( + GetBcdnsServiceInfoRequest.newBuilder() + .setDomainSpace(domainSpace) + .build() + ); + if (resp.getCode() != 0) { + return "failed to get BCDNS service info: " + resp.getErrorMsg(); + } + return resp.getGetBcdnsServiceInfoResp().getInfoJson(); + } catch (Throwable t) { + throw new RuntimeException("unexpected error please input stacktrace to check the detail", t); + } + } + + @ShellMethod(value = "Delete the BCDNS bound with specified domain space") + Object deleteBCDNSService(@ShellOption(help = "The domain space bound with BCDNS, default the root space", defaultValue = "") String domainSpace) { + try { + var resp = adminServiceBlockingStub.deleteBcdnsService( + DeleteBcdnsServiceRequest.newBuilder() + .setDomainSpace(domainSpace) + .build() + ); + if (resp.getCode() != 0) { + return "failed to delete BCDNS service: " + resp.getErrorMsg(); + } + return "success"; + } catch (Throwable t) { + throw new RuntimeException("unexpected error please input stacktrace to check the detail", t); + } + } + + @ShellMethod(value = "Get the BCDNS trust root certificate bound with specified domain space") + Object getBCDNSCertificate(@ShellOption(help = "The domain space bound with BCDNS, default the root space", defaultValue = "") String domainSpace) { + try { + var resp = adminServiceBlockingStub.getBcdnsCertificate( + GetBcdnsCertificateRequest.newBuilder() + .setDomainSpace(domainSpace) + .build() + ); + if (resp.getCode() != 0) { + return "failed to get BCDNS certificate: " + resp.getErrorMsg(); + } + return resp.getGetBcdnsCertificateResp().getCertificate(); + } catch (Throwable t) { + throw new RuntimeException("unexpected error please input stacktrace to check the detail", t); + } + } + + @ShellMethod(value = "Stop the local BCDNS service stub") + Object stopBCDNSService(@ShellOption(help = "The domain space bound with BCDNS, default the root space", defaultValue = "") String domainSpace) { + try { + var resp = adminServiceBlockingStub.stopBcdnsService( + StopBcdnsServiceRequest.newBuilder() + .setDomainSpace(domainSpace) + .build() + ); + if (resp.getCode() != 0) { + return "failed to stop BCDNS: " + resp.getErrorMsg(); + } + return "success"; + } catch (Throwable t) { + throw new RuntimeException("unexpected error please input stacktrace to check the detail", t); + } + } + + @ShellMethod(value = "Restart the local BCDNS service stub from stop") + Object restartBCDNSService(@ShellOption(help = "domain space, default the root space", defaultValue = "") String domainSpace) { + try { + var resp = adminServiceBlockingStub.restartBcdnsService( + RestartBcdnsServiceRequest.newBuilder() + .setDomainSpace(domainSpace) + .build() + ); + if (resp.getCode() != 0) { + return "failed to restart BCDNS: " + resp.getErrorMsg(); + } + return "success"; + } catch (Throwable t) { + throw new RuntimeException("unexpected error please input stacktrace to check the detail", t); + } + } + + @ShellMethod(value = "Add committee-ptc trust root manually") + Object addPtcTrustRoot( + @ShellOption(help = "file path leading to the serialized PTCTrustRoot which has been signed by supervisor") + String rawPtcTrustRootFile + ) { + try { + var filePath = Path.of(rawPtcTrustRootFile); + if (!Files.exists(filePath)) { + return "file not exists"; + } + + var resp = adminServiceBlockingStub.addPtcTrustRoot( + AddPtcTrustRootRequest.newBuilder() + .setRawTrustRoot(ByteString.copyFrom(Files.readAllBytes(filePath))) + .build() + ); + if (resp.getCode() != 0) { + return "failed to add ptc trust root: " + resp.getErrorMsg(); + } + return "success"; + } catch (Throwable t) { + throw new RuntimeException("unexpected error please input stacktrace to check the detail", t); + } + } +} diff --git a/acb-committeeptc/monitor-node-cli/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/cli/commands/UtilsCommands.java b/acb-committeeptc/monitor-node-cli/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/cli/commands/UtilsCommands.java new file mode 100755 index 00000000..f24caf70 --- /dev/null +++ b/acb-committeeptc/monitor-node-cli/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/cli/commands/UtilsCommands.java @@ -0,0 +1,94 @@ +/* + * Copyright 2024 Ant Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alipay.antchain.bridge.ptc.committee.monitor.node.cli.commands; + +import java.io.StringWriter; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.*; + +import cn.hutool.core.util.StrUtil; +import com.alipay.antchain.bridge.commons.utils.crypto.SignAlgoEnum; +import lombok.Getter; +import lombok.SneakyThrows; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.openssl.jcajce.JcaPEMWriter; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.shell.standard.*; + +@Getter +@ShellCommandGroup(value = "Utils Commands") +@ShellComponent +public class UtilsCommands extends BaseCommands { + + static { + Security.addProvider(new BouncyCastleProvider()); + } + + @Value("${grpc.client.admin.address:static://localhost:10088}") + private String adminAddress; + + @Override + public boolean needAdminServer() { + return false; + } + + @ShellMethod(value = "Generate PEM files for the node private and public key") + public String generateNodeAccount( + @ShellOption(help = "Key algorithm, default SECP256K1", defaultValue = "SECP256K1") String keyAlgo, + @ShellOption(valueProvider = FileValueProvider.class, help = "Directory path to save the keys", defaultValue = "") String outDir + ) { + try { + var keyPair = SignAlgoEnum.getSignAlgoByKeySuffix(keyAlgo).getSigner().generateKeyPair(); + + // dump the private key into pem + Path privatePath = Paths.get(outDir, "private_key.pem"); + writePrivateKey(keyPair.getPrivate(), privatePath); + + // dump the public key into pem + Path publicPath = Paths.get(outDir, "public_key.pem"); + writePublicKey(keyPair.getPublic(), publicPath); + + return StrUtil.format("private key path: {}\npublic key path: {}", privatePath.toAbsolutePath(), publicPath.toAbsolutePath()); + } catch (Exception e) { + throw new RuntimeException("unexpected error please input stacktrace to check the detail", e); + } + } + + @SneakyThrows + private void writePrivateKey(PrivateKey privateKey, Path outputFile) { + // dump the private key into pem + StringWriter stringWriter = new StringWriter(256); + JcaPEMWriter jcaPEMWriter = new JcaPEMWriter(stringWriter); + jcaPEMWriter.writeObject(privateKey); + jcaPEMWriter.close(); + String privatePem = stringWriter.toString(); + Files.write(outputFile, privatePem.getBytes()); + } + + @SneakyThrows + private void writePublicKey(PublicKey publicKey, Path outputFile) { + // dump the public key into pem + StringWriter stringWriter = new StringWriter(256); + JcaPEMWriter jcaPEMWriter = new JcaPEMWriter(stringWriter); + jcaPEMWriter.writeObject(publicKey); + jcaPEMWriter.close(); + String pubkeyPem = stringWriter.toString(); + Files.write(outputFile, pubkeyPem.getBytes()); + } +} diff --git a/acb-committeeptc/monitor-node-cli/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/cli/config/CustomPromptProvider.java b/acb-committeeptc/monitor-node-cli/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/cli/config/CustomPromptProvider.java new file mode 100755 index 00000000..69392dee --- /dev/null +++ b/acb-committeeptc/monitor-node-cli/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/cli/config/CustomPromptProvider.java @@ -0,0 +1,30 @@ +/* + * Copyright 2023 Ant Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alipay.antchain.bridge.ptc.committee.monitor.node.cli.config; + +import org.jline.utils.AttributedString; +import org.jline.utils.AttributedStyle; +import org.springframework.shell.jline.PromptProvider; +import org.springframework.stereotype.Component; + +@Component +public class CustomPromptProvider implements PromptProvider { + @Override + public AttributedString getPrompt() { + return new AttributedString("monitor-node:> ", AttributedStyle.DEFAULT.foreground(AttributedStyle.YELLOW)); + } +} \ No newline at end of file diff --git a/acb-committeeptc/monitor-node-cli/src/main/resources/application.yml b/acb-committeeptc/monitor-node-cli/src/main/resources/application.yml new file mode 100755 index 00000000..4f7580ed --- /dev/null +++ b/acb-committeeptc/monitor-node-cli/src/main/resources/application.yml @@ -0,0 +1,13 @@ +spring: + shell: + interactive: + enabled: true + history: + name: ".monitor-node-cli-history" +logging: + level: + root: off +grpc: + client: + admin: + negotiation-type: plaintext \ No newline at end of file diff --git a/acb-committeeptc/monitor-node-cli/src/main/resources/banner.txt b/acb-committeeptc/monitor-node-cli/src/main/resources/banner.txt new file mode 100755 index 00000000..c75b2738 --- /dev/null +++ b/acb-committeeptc/monitor-node-cli/src/main/resources/banner.txt @@ -0,0 +1,7 @@ + __ __ ___ _ _ ___ _____ ___ ___ _ _ ___ ___ ___ +| \/ | / _ \ | \| | |_ _| |_ _| / _ \ | _ \ ___ | \| | / _ \ | \ | __| +| |\/| | | (_) | | .` | | | | | | (_) | | / |___| | .` | | (_) | | |) | | _| +|_| |_| \___/ |_|\_| |___| _|_|_ \___/ |_|_\ |_|\_| \___/ |___/ |___| + +${AnsiStyle.BOLD} CLI @project.version@ +${AnsiStyle.NORMAL} \ No newline at end of file diff --git a/acb-committeeptc/monitor-node-cli/src/main/resources/start.sh b/acb-committeeptc/monitor-node-cli/src/main/resources/start.sh new file mode 100755 index 00000000..1563ae2b --- /dev/null +++ b/acb-committeeptc/monitor-node-cli/src/main/resources/start.sh @@ -0,0 +1,63 @@ +#!/bin/bash + +bin=`dirname "${BASH_SOURCE-$0}"` +CLI_HOME=`cd "$bin"; pwd` + +Help=$( + cat <<-"HELP" + + start.sh - Start the AntChain Bridge Monitor Node Command Line Interface Tool + + Usage: + start.sh + + Examples: + 1. start with the default server address `localhost` and default port `10088`: + start.sh + 2. start with specific server address and port: + start.sh -H 0.0.0.0 -p 10088 + + Options: + -H admin server host of monitor node. + -p admin server port of monitor node. + -h print help information. + +HELP +) + +CURR_DIR="$( + cd $(dirname $0) + pwd +)" + +while getopts "hH:p:" opt; do + case "$opt" in + "h") + echo "$Help" + exit 0 + ;; + "H") + SERVER_HOST=$OPTARG + ;; + "p") + SERVER_PORT=$OPTARG + ;; + "?") + echo "invalid arguments. " + exit 1 + ;; + *) + echo "Unknown error while processing options" + exit 1 + ;; + esac +done + + +which java > /dev/null +if [ $? -eq 1 ]; then + echo "no java installed. " + exit 1 +fi + +java -jar ${CLI_HOME}/../lib/monitor-node-cli.jar --port=${SERVER_PORT:-10088} --host=${SERVER_HOST:-"localhost"} \ No newline at end of file diff --git a/acb-committeeptc/monitor-node/.gitignore b/acb-committeeptc/monitor-node/.gitignore new file mode 100755 index 00000000..549e00a2 --- /dev/null +++ b/acb-committeeptc/monitor-node/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/acb-committeeptc/monitor-node/.mvn/wrapper/maven-wrapper.properties b/acb-committeeptc/monitor-node/.mvn/wrapper/maven-wrapper.properties new file mode 100755 index 00000000..8f96f52c --- /dev/null +++ b/acb-committeeptc/monitor-node/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,19 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +wrapperVersion=3.3.2 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.7/apache-maven-3.9.7-bin.zip diff --git a/acb-committeeptc/monitor-node/README.md b/acb-committeeptc/monitor-node/README.md new file mode 100755 index 00000000..32a90a12 --- /dev/null +++ b/acb-committeeptc/monitor-node/README.md @@ -0,0 +1,22 @@ +
+ am logo +

Committee Monitor Node

+

+ + pull requests welcome badge + + + Language + + + GitHub contributors + + + License + +

+
+ +# 介绍 + +Committee Monitor Node是存在于委员会中的监管节点,与本跨链系统外的监管系统提供交互接口,完成跨链消息事中监管和监管指令下达的服务。 \ No newline at end of file diff --git a/acb-committeeptc/monitor-node/desc_tar.xml b/acb-committeeptc/monitor-node/desc_tar.xml new file mode 100755 index 00000000..575768b4 --- /dev/null +++ b/acb-committeeptc/monitor-node/desc_tar.xml @@ -0,0 +1,48 @@ + + ${version} + + tar.gz + + true + + + ${project.basedir}/target + ${file.separator}lib + + ${artifactId}-${version}.jar + + + + ${project.basedir}/src/main/resources/scripts + ${file.separator}bin + + *.sh + monitor-node.service + + + + ${project.basedir}/src/main/resources + ${file.separator}config + + application.yml + ddl.sql + + + + ${project.basedir} + ${file.separator} + + README.md + + + + ${project.basedir}/../monitor-node-cli/target + ${file.separator} + + monitor-node-cli-bin.tar.gz + + + + \ No newline at end of file diff --git a/acb-committeeptc/monitor-node/mvnw b/acb-committeeptc/monitor-node/mvnw new file mode 100755 index 00000000..d7c358e5 --- /dev/null +++ b/acb-committeeptc/monitor-node/mvnw @@ -0,0 +1,259 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.3.2 +# +# Optional ENV vars +# ----------------- +# JAVA_HOME - location of a JDK home dir, required when download maven via java source +# MVNW_REPOURL - repo url base for downloading maven distribution +# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output +# ---------------------------------------------------------------------------- + +set -euf +[ "${MVNW_VERBOSE-}" != debug ] || set -x + +# OS specific support. +native_path() { printf %s\\n "$1"; } +case "$(uname)" in +CYGWIN* | MINGW*) + [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" + native_path() { cygpath --path --windows "$1"; } + ;; +esac + +# set JAVACMD and JAVACCMD +set_java_home() { + # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched + if [ -n "${JAVA_HOME-}" ]; then + if [ -x "$JAVA_HOME/jre/sh/java" ]; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACCMD="$JAVA_HOME/jre/sh/javac" + else + JAVACMD="$JAVA_HOME/bin/java" + JAVACCMD="$JAVA_HOME/bin/javac" + + if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then + echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 + echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 + return 1 + fi + fi + else + JAVACMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v java + )" || : + JAVACCMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v javac + )" || : + + if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then + echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 + return 1 + fi + fi +} + +# hash string like Java String::hashCode +hash_string() { + str="${1:-}" h=0 + while [ -n "$str" ]; do + char="${str%"${str#?}"}" + h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) + str="${str#?}" + done + printf %x\\n $h +} + +verbose() { :; } +[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } + +die() { + printf %s\\n "$1" >&2 + exit 1 +} + +trim() { + # MWRAPPER-139: + # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. + # Needed for removing poorly interpreted newline sequences when running in more + # exotic environments such as mingw bash on Windows. + printf "%s" "${1}" | tr -d '[:space:]' +} + +# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties +while IFS="=" read -r key value; do + case "${key-}" in + distributionUrl) distributionUrl=$(trim "${value-}") ;; + distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; + esac +done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties" + +case "${distributionUrl##*/}" in +maven-mvnd-*bin.*) + MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ + case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in + *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; + :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; + :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; + :Linux*x86_64*) distributionPlatform=linux-amd64 ;; + *) + echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 + distributionPlatform=linux-amd64 + ;; + esac + distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" + ;; +maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; +*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +esac + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" +distributionUrlName="${distributionUrl##*/}" +distributionUrlNameMain="${distributionUrlName%.*}" +distributionUrlNameMain="${distributionUrlNameMain%-bin}" +MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" +MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" + +exec_maven() { + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" +} + +if [ -d "$MAVEN_HOME" ]; then + verbose "found existing MAVEN_HOME at $MAVEN_HOME" + exec_maven "$@" +fi + +case "${distributionUrl-}" in +*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; +*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; +esac + +# prepare tmp dir +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } + trap clean HUP INT TERM EXIT +else + die "cannot create temp dir" +fi + +mkdir -p -- "${MAVEN_HOME%/*}" + +# Download and Install Apache Maven +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +verbose "Downloading from: $distributionUrl" +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +# select .zip or .tar.gz +if ! command -v unzip >/dev/null; then + distributionUrl="${distributionUrl%.zip}.tar.gz" + distributionUrlName="${distributionUrl##*/}" +fi + +# verbose opt +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v + +# normalize http auth +case "${MVNW_PASSWORD:+has-password}" in +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; +esac + +if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then + verbose "Found wget ... using wget" + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then + verbose "Found curl ... using curl" + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" +elif set_java_home; then + verbose "Falling back to use Java to download" + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" + cat >"$javaSource" <<-END + public class Downloader extends java.net.Authenticator + { + protected java.net.PasswordAuthentication getPasswordAuthentication() + { + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); + } + public static void main( String[] args ) throws Exception + { + setDefault( new Downloader() ); + java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); + } + } + END + # For Cygwin/MinGW, switch paths to Windows format before running javac and java + verbose " - Compiling Downloader.java ..." + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" + verbose " - Running Downloader.java ..." + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" +fi + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +if [ -n "${distributionSha256Sum-}" ]; then + distributionSha256Result=false + if [ "$MVN_CMD" = mvnd.sh ]; then + echo "Checksum validation is not supported for maven-mvnd." >&2 + echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + elif command -v sha256sum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + elif command -v shasum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + fi + if [ $distributionSha256Result = false ]; then + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 + exit 1 + fi +fi + +# unzip and move +if command -v unzip >/dev/null; then + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" +else + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" +fi +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" + +clean || : +exec_maven "$@" diff --git a/acb-committeeptc/monitor-node/mvnw.cmd b/acb-committeeptc/monitor-node/mvnw.cmd new file mode 100755 index 00000000..6f779cff --- /dev/null +++ b/acb-committeeptc/monitor-node/mvnw.cmd @@ -0,0 +1,149 @@ +<# : batch portion +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM https://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.3.2 +@REM +@REM Optional ENV vars +@REM MVNW_REPOURL - repo url base for downloading maven distribution +@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output +@REM ---------------------------------------------------------------------------- + +@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) +@SET __MVNW_CMD__= +@SET __MVNW_ERROR__= +@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% +@SET PSModulePath= +@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( + IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) +) +@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% +@SET __MVNW_PSMODULEP_SAVE= +@SET __MVNW_ARG0_NAME__= +@SET MVNW_USERNAME= +@SET MVNW_PASSWORD= +@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) +@echo Cannot start maven from wrapper >&2 && exit /b 1 +@GOTO :EOF +: end batch / begin powershell #> + +$ErrorActionPreference = "Stop" +if ($env:MVNW_VERBOSE -eq "true") { + $VerbosePreference = "Continue" +} + +# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties +$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl +if (!$distributionUrl) { + Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" +} + +switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { + "maven-mvnd-*" { + $USE_MVND = $true + $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" + $MVN_CMD = "mvnd.cmd" + break + } + default { + $USE_MVND = $false + $MVN_CMD = $script -replace '^mvnw','mvn' + break + } +} + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +if ($env:MVNW_REPOURL) { + $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" +} +$distributionUrlName = $distributionUrl -replace '^.*/','' +$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' +$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" +if ($env:MAVEN_USER_HOME) { + $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" +} +$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' +$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" + +if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { + Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" + Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" + exit $? +} + +if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { + Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" +} + +# prepare tmp dir +$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile +$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" +$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null +trap { + if ($TMP_DOWNLOAD_DIR.Exists) { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } + } +} + +New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null + +# Download and Install Apache Maven +Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +Write-Verbose "Downloading from: $distributionUrl" +Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +$webclient = New-Object System.Net.WebClient +if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { + $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) +} +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum +if ($distributionSha256Sum) { + if ($USE_MVND) { + Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." + } + Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash + if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { + Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." + } +} + +# unzip and move +Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null +try { + Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null +} catch { + if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { + Write-Error "fail to move MAVEN_HOME" + } +} finally { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } +} + +Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/acb-committeeptc/monitor-node/pom.xml b/acb-committeeptc/monitor-node/pom.xml new file mode 100755 index 00000000..2445721a --- /dev/null +++ b/acb-committeeptc/monitor-node/pom.xml @@ -0,0 +1,235 @@ + + + 4.0.0 + + com.alipay.antchain.bridge + committee-ptc + 0.1.0-SNAPSHOT + + + monitor-node + 0.1.0-SNAPSHOT + monitor-node + monitor node + + + + + + + + + + + + + + + + + 21 + + + + + org.springframework.boot + spring-boot-devtools + runtime + true + + + org.springframework.boot + spring-boot-configuration-processor + true + + + com.alipay.antchain.bridge + antchain-bridge-spi + + + com.alipay.antchain.bridge + antchain-bridge-ptc + + + com.alipay.antchain.bridge + antchain-bridge-bcdns + + + com.alipay.antchain.bridge + antchain-bridge-bcdns-factory + + + com.alipay.antchain.bridge + antchain-bridge-plugin-manager + + + com.alipay.antchain.bridge + committee-ptc-core + + + net.devh + grpc-server-spring-boot-starter + + + io.grpc + grpc-api + + + + + org.projectlombok + lombok + true + + + com.mysql + mysql-connector-j + + + protobuf-java + com.google.protobuf + + + + + cn.hutool + hutool-all + + + com.github.ulisesbocchio + jasypt-spring-boot-starter + + + com.baomidou + mybatis-plus-spring-boot3-starter + + + org.springframework.boot + spring-boot-starter-test + test + + + com.baomidou + mybatis-plus-boot-starter-test + test + + + javax.annotation + javax.annotation-api + + + com.github.ulisesbocchio + jasypt-spring-boot-starter + + + + junit + junit + test + + + com.h2database + h2 + test + + + io.grpc + grpc-testing + test + + + net.devh + grpc-client-spring-boot-starter + + + io.grpc + grpc-api + + + + + + + + + kr.motd.maven + os-maven-plugin + 1.7.0 + + + + + src/main/resources + + **/application.yml + **/*.xml + **/banner.txt + + + true + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + org.apache.maven.plugins + maven-assembly-plugin + 3.1.0 + + + + monitor-node + + desc_tar.xml + + + make-tar + package + + single + + + + + + org.xolstice.maven.plugins + protobuf-maven-plugin + ${protobuf-plugin.version} + + com.google.protobuf:protoc:${protobuf.version}:exe:${os.detected.classifier} + grpc-java + io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier} + + ${project.basedir}/src/main/proto/ + + + + + + compile + compile-custom + + + + + + com.github.ulisesbocchio + jasypt-maven-plugin + 3.0.5 + + + + + diff --git a/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/NodeApplication.java b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/NodeApplication.java new file mode 100755 index 00000000..b686e208 --- /dev/null +++ b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/NodeApplication.java @@ -0,0 +1,38 @@ +/* + * Copyright 2024 Ant Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alipay.antchain.bridge.ptc.committee.monitor.node; + +import com.ulisesbocchio.jasyptspringboot.annotation.EnableEncryptableProperties; +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.boot.WebApplicationType; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.scheduling.annotation.EnableScheduling; + +@SpringBootApplication(scanBasePackages = {"com.alipay.antchain.bridge.ptc.committee.monitor.node"}) +@MapperScan("com.alipay.antchain.bridge.ptc.committee.monitor.node.dal.mapper") +@EnableEncryptableProperties +@EnableScheduling +public class NodeApplication { + + public static void main(String[] args) { + new SpringApplicationBuilder(NodeApplication.class) + .web(WebApplicationType.NONE) + .run(args); + } + +} diff --git a/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/client/CrossChainServiceGrpcClientManager.java b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/client/CrossChainServiceGrpcClientManager.java new file mode 100755 index 00000000..ccce0f88 --- /dev/null +++ b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/client/CrossChainServiceGrpcClientManager.java @@ -0,0 +1,101 @@ +package com.alipay.antchain.bridge.ptc.committee.monitor.node.client; + +import java.io.FileInputStream; +import java.util.*; + +import cn.hutool.core.util.StrUtil; +import com.alipay.antchain.bridge.pluginserver.service.CrossChainServiceGrpc; +import io.grpc.ManagedChannel; +import io.grpc.ManagedChannelBuilder; +import io.grpc.TlsChannelCredentials; +import io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.io.Resource; +import org.springframework.stereotype.Component; + +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import javax.naming.ldap.LdapName; +import javax.naming.ldap.Rdn; +import javax.security.auth.x500.X500Principal; +import java.io.InputStream; + +@Component +public class CrossChainServiceGrpcClientManager { + + @Value("${grpc.clients.plugin-server.host}") + private String host; + + @Value("${grpc.clients.plugin-server.port}") + private int port; + + @Value("${grpc.clients.plugin-server.ps-id}") + private String psId; + + @Value("${grpc.clients.plugin-server.security.pluginServerCert}") + private Resource psCertResource; + + @Value("${grpc.clients.plugin-server.security.certificate-chain}") + private Resource tlsCaResource; + + @Value("${grpc.clients.plugin-server.security.private-key}") + private Resource tlsKeyResource; + + private final Map blockingStubMap = new HashMap<>(); + + public CrossChainServiceGrpc.CrossChainServiceBlockingStub createStub(String clientName) { + + String commonName = getPluginServerCertX509CommonName(psCertResource); + if (StrUtil.isEmpty(commonName)) { + throw new RuntimeException( + String.format("failed to get common name from x509 subject for plugin server %s", psId) + ); + } + + ManagedChannel channel; + try { + TlsChannelCredentials.Builder tlsBuilder = TlsChannelCredentials.newBuilder(); + tlsBuilder.keyManager(tlsCaResource.getInputStream(), tlsKeyResource.getInputStream()); + tlsBuilder.trustManager(psCertResource.getInputStream()); + channel = NettyChannelBuilder.forAddress(host, port, tlsBuilder.build()) + .overrideAuthority(commonName) + .build(); + } catch (Exception e) { + throw new RuntimeException( + String.format("failed to create client for psId %s", psId) + ); + } + + return CrossChainServiceGrpc.newBlockingStub(channel); + } + + public CrossChainServiceGrpc.CrossChainServiceBlockingStub getStub(String clientName) { + if (this.blockingStubMap.containsKey(clientName)) { + return this.blockingStubMap.get(clientName); + } + this.blockingStubMap.put(clientName, createStub(clientName)); + return this.blockingStubMap.get(clientName); + } + + private static String getPluginServerCertX509CommonName(Resource certResource) { + try (InputStream is = certResource.getInputStream()) { + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + X509Certificate cert = (X509Certificate) cf.generateCertificate(is); + + X500Principal principal = cert.getSubjectX500Principal(); + String dn = principal.getName(); + + LdapName ldapName = new LdapName(dn); + String commonName = null; + for (Rdn rdn : ldapName.getRdns()) { + if ("CN".equalsIgnoreCase(rdn.getType())) { + commonName = rdn.getValue().toString(); + break; + } + } + return commonName; + } catch (Exception e) { + throw new RuntimeException("Failed to get common name from certificate file", e); + } + } +} diff --git a/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/client/MonitorSystemGrpcClientManager.java b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/client/MonitorSystemGrpcClientManager.java new file mode 100755 index 00000000..fa0e1922 --- /dev/null +++ b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/client/MonitorSystemGrpcClientManager.java @@ -0,0 +1,41 @@ +package com.alipay.antchain.bridge.ptc.committee.monitor.node.client; + +import java.util.*; + +import com.alipay.antchain.bridge.commons.utils.crypto.HashAlgoEnum; +import com.alipay.antchain.bridge.ptc.committee.monitor.system.grpc.MonitorSystemServiceGrpc; +import io.grpc.ManagedChannel; +import io.grpc.ManagedChannelBuilder; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Component +public class MonitorSystemGrpcClientManager { + + @Value("${grpc.clients.monitor-system.host:localhost}") + private String host; + + @Value("${grpc.clients.monitor-system.port:50051}") + private int port; + + private final Map blockingStubMap = new HashMap<>(); + + public MonitorSystemServiceGrpc.MonitorSystemServiceBlockingStub createStub(String clientName) { + + // 创建 gRPC 通道 + ManagedChannel channel = ManagedChannelBuilder.forAddress(host, port) + .usePlaintext() // 使用明文传输 + .build(); + + // 创建并返回 gRPC 阻塞存根 + return MonitorSystemServiceGrpc.newBlockingStub(channel); + } + + public MonitorSystemServiceGrpc.MonitorSystemServiceBlockingStub getStub(String clientName) { + if (this.blockingStubMap.containsKey(clientName)) { + return this.blockingStubMap.get(clientName); + } + this.blockingStubMap.put(clientName, createStub(clientName)); + return this.blockingStubMap.get(clientName); + } +} diff --git a/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/commons/core/MonitorContract.java b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/commons/core/MonitorContract.java new file mode 100755 index 00000000..9b4d213c --- /dev/null +++ b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/commons/core/MonitorContract.java @@ -0,0 +1,18 @@ +package com.alipay.antchain.bridge.ptc.committee.monitor.node.commons.core; + +import com.alipay.antchain.bridge.commons.bbc.syscontract.ContractStatusEnum; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +public class MonitorContract { + + private String monitorContractAddress; + + private ContractStatusEnum status; +} \ No newline at end of file diff --git a/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/commons/core/MonitorSystemRespMsg.java b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/commons/core/MonitorSystemRespMsg.java new file mode 100755 index 00000000..ee4d77b2 --- /dev/null +++ b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/commons/core/MonitorSystemRespMsg.java @@ -0,0 +1,26 @@ +package com.alipay.antchain.bridge.ptc.committee.monitor.node.commons.core; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.annotation.JSONField; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@AllArgsConstructor +public class MonitorSystemRespMsg { + @JSONField(name = "result") + private int result; + + @JSONField(name = "error_msg") + private String errorMsg; + + public byte[] encode() { + return JSON.toJSONBytes(this); + } + + public static MonitorSystemRespMsg decode(byte[] rawData) { + return JSON.parseObject(rawData, MonitorSystemRespMsg.class); + } +} \ No newline at end of file diff --git a/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/commons/enums/BCDNSStateEnum.java b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/commons/enums/BCDNSStateEnum.java new file mode 100755 index 00000000..d85d814c --- /dev/null +++ b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/commons/enums/BCDNSStateEnum.java @@ -0,0 +1,33 @@ +/* + * Copyright 2024 Ant Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alipay.antchain.bridge.ptc.committee.monitor.node.commons.enums; + +import com.baomidou.mybatisplus.annotation.EnumValue; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum BCDNSStateEnum { + + WORKING(0), + + FROZEN(1); + + @EnumValue + private final Integer code; +} diff --git a/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/commons/enums/MonitorTypeEnum.java b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/commons/enums/MonitorTypeEnum.java new file mode 100755 index 00000000..50d3df2c --- /dev/null +++ b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/commons/enums/MonitorTypeEnum.java @@ -0,0 +1,21 @@ +package com.alipay.antchain.bridge.ptc.committee.monitor.node.commons.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum MonitorTypeEnum { + + NONE(0), + + MONITOR_CLOSE(1), + + MONITOR_OPEN(2), + + MONITOR_ROLLBACK(3), + + MONITOR_ORDER(4); + + private final int code; +} diff --git a/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/commons/exception/BlockStateNotValidatedYetException.java b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/commons/exception/BlockStateNotValidatedYetException.java new file mode 100755 index 00000000..5cc59eff --- /dev/null +++ b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/commons/exception/BlockStateNotValidatedYetException.java @@ -0,0 +1,38 @@ +/* + * Copyright 2024 Ant Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alipay.antchain.bridge.ptc.committee.monitor.node.commons.exception; + +import cn.hutool.core.util.StrUtil; + +public class BlockStateNotValidatedYetException extends CommitteeNodeException { + + public BlockStateNotValidatedYetException(String longMsg) { + super(CommitteeNodeErrorCodeEnum.SERVER_ENDORSE_BLOCK_STATE_ERROR, longMsg); + } + + public BlockStateNotValidatedYetException(String formatStr, Object... objects) { + super(CommitteeNodeErrorCodeEnum.SERVER_ENDORSE_BLOCK_STATE_ERROR, StrUtil.format(formatStr, objects)); + } + + public BlockStateNotValidatedYetException(Throwable throwable, String formatStr, Object... objects) { + super(CommitteeNodeErrorCodeEnum.SERVER_ENDORSE_BLOCK_STATE_ERROR, StrUtil.format(formatStr, objects), throwable); + } + + public BlockStateNotValidatedYetException(String longMsg, Throwable throwable) { + super(CommitteeNodeErrorCodeEnum.SERVER_ENDORSE_BLOCK_STATE_ERROR, longMsg, throwable); + } +} diff --git a/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/commons/exception/CommitteeNodeErrorCodeEnum.java b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/commons/exception/CommitteeNodeErrorCodeEnum.java new file mode 100755 index 00000000..8f639d4c --- /dev/null +++ b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/commons/exception/CommitteeNodeErrorCodeEnum.java @@ -0,0 +1,55 @@ +/* + * Copyright 2024 Ant Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alipay.antchain.bridge.ptc.committee.monitor.node.commons.exception; + +import lombok.Getter; + +@Getter +public enum CommitteeNodeErrorCodeEnum { + + UNKNOWN_INTERNAL_ERROR("0001", "internal error"), + + DAL_ERROR("0101", "data access layer error"), + + SERVER_INVALID_REQUEST("0201", "invalid request"), + + SERVER_VERIFY_BTA_ERROR("0202", "verify bta error"), + + SERVER_VERIFY_CONSENSUS_STATE_ERROR("0203", "verify consensus state error"), + + SERVER_VERIFY_CROSSCHAIN_MESSAGE_ERROR("0204", "verify crosschain message error"), + + SERVER_ENDORSE_BLOCK_STATE_ERROR("0205", "block state not validated yet"); + + /** + * Error code for errors happened in project {@code antchain-bridge-committee-node} + */ + private final String errorCode; + + private final int errorCodeNum; + + /** + * Every code has a short message to describe the error stuff + */ + private final String shortMsg; + + CommitteeNodeErrorCodeEnum(String errorCode, String shortMsg) { + this.errorCode = errorCode; + this.errorCodeNum = Integer.parseInt(errorCode, 16); + this.shortMsg = shortMsg; + } +} diff --git a/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/commons/exception/CommitteeNodeException.java b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/commons/exception/CommitteeNodeException.java new file mode 100755 index 00000000..fb68d9f0 --- /dev/null +++ b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/commons/exception/CommitteeNodeException.java @@ -0,0 +1,43 @@ +/* + * Copyright 2024 Ant Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alipay.antchain.bridge.ptc.committee.monitor.node.commons.exception; + +import cn.hutool.core.util.StrUtil; +import com.alipay.antchain.bridge.commons.exception.base.AntChainBridgeBaseException; + +public class CommitteeNodeException extends AntChainBridgeBaseException { + + public CommitteeNodeException(CommitteeNodeErrorCodeEnum errorCode, String longMsg) { + super(errorCode.getErrorCode(), errorCode.getShortMsg(), longMsg); + } + + public CommitteeNodeException(CommitteeNodeErrorCodeEnum errorCode, String formatStr, Object... objects) { + super(errorCode.getErrorCode(), errorCode.getShortMsg(), StrUtil.format(formatStr, objects)); + } + + public CommitteeNodeException(CommitteeNodeErrorCodeEnum errorCode, Throwable throwable, String formatStr, Object... objects) { + super(errorCode.getErrorCode(), errorCode.getShortMsg(), StrUtil.format(formatStr, objects), throwable); + } + + public CommitteeNodeException(CommitteeNodeErrorCodeEnum errorCode, String longMsg, Throwable throwable) { + super(errorCode.getErrorCode(), errorCode.getShortMsg(), longMsg, throwable); + } + + public int getCodeNum() { + return Integer.parseInt(this.getCode(), 16); + } +} diff --git a/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/commons/exception/CommitteeNodeInternalException.java b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/commons/exception/CommitteeNodeInternalException.java new file mode 100755 index 00000000..71bbc898 --- /dev/null +++ b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/commons/exception/CommitteeNodeInternalException.java @@ -0,0 +1,34 @@ +/* + * Copyright 2024 Ant Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alipay.antchain.bridge.ptc.committee.monitor.node.commons.exception; + +import cn.hutool.core.util.StrUtil; + +public class CommitteeNodeInternalException extends CommitteeNodeException { + + public CommitteeNodeInternalException(String message) { + super(CommitteeNodeErrorCodeEnum.UNKNOWN_INTERNAL_ERROR, message); + } + + public CommitteeNodeInternalException(String format, Object... args) { + super(CommitteeNodeErrorCodeEnum.UNKNOWN_INTERNAL_ERROR, StrUtil.format(format, args)); + } + + public CommitteeNodeInternalException(Throwable t, String format, Object... args) { + super(CommitteeNodeErrorCodeEnum.UNKNOWN_INTERNAL_ERROR, StrUtil.format(format, args), t); + } +} diff --git a/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/commons/exception/DataAccessLayerException.java b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/commons/exception/DataAccessLayerException.java new file mode 100755 index 00000000..300fd760 --- /dev/null +++ b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/commons/exception/DataAccessLayerException.java @@ -0,0 +1,38 @@ +/* + * Copyright 2024 Ant Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alipay.antchain.bridge.ptc.committee.monitor.node.commons.exception; + +import cn.hutool.core.util.StrUtil; + +public class DataAccessLayerException extends CommitteeNodeException { + + public DataAccessLayerException(String longMsg) { + super(CommitteeNodeErrorCodeEnum.DAL_ERROR, longMsg); + } + + public DataAccessLayerException(String formatStr, Object... objects) { + super(CommitteeNodeErrorCodeEnum.DAL_ERROR, StrUtil.format(formatStr, objects)); + } + + public DataAccessLayerException(Throwable throwable, String formatStr, Object... objects) { + super(CommitteeNodeErrorCodeEnum.DAL_ERROR, StrUtil.format(formatStr, objects), throwable); + } + + public DataAccessLayerException(String longMsg, Throwable throwable) { + super(CommitteeNodeErrorCodeEnum.DAL_ERROR, longMsg, throwable); + } +} diff --git a/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/commons/exception/InvalidBtaException.java b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/commons/exception/InvalidBtaException.java new file mode 100755 index 00000000..7dccba1c --- /dev/null +++ b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/commons/exception/InvalidBtaException.java @@ -0,0 +1,38 @@ +/* + * Copyright 2024 Ant Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alipay.antchain.bridge.ptc.committee.monitor.node.commons.exception; + +import cn.hutool.core.util.StrUtil; + +public class InvalidBtaException extends CommitteeNodeException { + + public InvalidBtaException(String longMsg) { + super(CommitteeNodeErrorCodeEnum.SERVER_VERIFY_BTA_ERROR, longMsg); + } + + public InvalidBtaException(String formatStr, Object... objects) { + super(CommitteeNodeErrorCodeEnum.SERVER_VERIFY_BTA_ERROR, StrUtil.format(formatStr, objects)); + } + + public InvalidBtaException(Throwable throwable, String formatStr, Object... objects) { + super(CommitteeNodeErrorCodeEnum.SERVER_VERIFY_BTA_ERROR, StrUtil.format(formatStr, objects), throwable); + } + + public InvalidBtaException(String longMsg, Throwable throwable) { + super(CommitteeNodeErrorCodeEnum.SERVER_VERIFY_BTA_ERROR, longMsg, throwable); + } +} diff --git a/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/commons/exception/InvalidConsensusStateException.java b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/commons/exception/InvalidConsensusStateException.java new file mode 100755 index 00000000..9583e6d2 --- /dev/null +++ b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/commons/exception/InvalidConsensusStateException.java @@ -0,0 +1,38 @@ +/* + * Copyright 2024 Ant Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alipay.antchain.bridge.ptc.committee.monitor.node.commons.exception; + +import cn.hutool.core.util.StrUtil; + +public class InvalidConsensusStateException extends CommitteeNodeException { + + public InvalidConsensusStateException(String longMsg) { + super(CommitteeNodeErrorCodeEnum.SERVER_VERIFY_CONSENSUS_STATE_ERROR, longMsg); + } + + public InvalidConsensusStateException(String formatStr, Object... objects) { + super(CommitteeNodeErrorCodeEnum.SERVER_VERIFY_CONSENSUS_STATE_ERROR, StrUtil.format(formatStr, objects)); + } + + public InvalidConsensusStateException(Throwable throwable, String formatStr, Object... objects) { + super(CommitteeNodeErrorCodeEnum.SERVER_VERIFY_CONSENSUS_STATE_ERROR, StrUtil.format(formatStr, objects), throwable); + } + + public InvalidConsensusStateException(String longMsg, Throwable throwable) { + super(CommitteeNodeErrorCodeEnum.SERVER_VERIFY_CONSENSUS_STATE_ERROR, longMsg, throwable); + } +} diff --git a/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/commons/exception/InvalidCrossChainMessageException.java b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/commons/exception/InvalidCrossChainMessageException.java new file mode 100755 index 00000000..c4fe7dcd --- /dev/null +++ b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/commons/exception/InvalidCrossChainMessageException.java @@ -0,0 +1,38 @@ +/* + * Copyright 2024 Ant Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alipay.antchain.bridge.ptc.committee.monitor.node.commons.exception; + +import cn.hutool.core.util.StrUtil; + +public class InvalidCrossChainMessageException extends CommitteeNodeException { + + public InvalidCrossChainMessageException(String longMsg) { + super(CommitteeNodeErrorCodeEnum.SERVER_VERIFY_CROSSCHAIN_MESSAGE_ERROR, longMsg); + } + + public InvalidCrossChainMessageException(String formatStr, Object... objects) { + super(CommitteeNodeErrorCodeEnum.SERVER_VERIFY_CROSSCHAIN_MESSAGE_ERROR, StrUtil.format(formatStr, objects)); + } + + public InvalidCrossChainMessageException(Throwable throwable, String formatStr, Object... objects) { + super(CommitteeNodeErrorCodeEnum.SERVER_VERIFY_CROSSCHAIN_MESSAGE_ERROR, StrUtil.format(formatStr, objects), throwable); + } + + public InvalidCrossChainMessageException(String longMsg, Throwable throwable) { + super(CommitteeNodeErrorCodeEnum.SERVER_VERIFY_CROSSCHAIN_MESSAGE_ERROR, longMsg, throwable); + } +} diff --git a/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/commons/exception/InvalidRequestException.java b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/commons/exception/InvalidRequestException.java new file mode 100755 index 00000000..d93de952 --- /dev/null +++ b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/commons/exception/InvalidRequestException.java @@ -0,0 +1,38 @@ +/* + * Copyright 2024 Ant Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alipay.antchain.bridge.ptc.committee.monitor.node.commons.exception; + +import cn.hutool.core.util.StrUtil; + +public class InvalidRequestException extends CommitteeNodeException { + + public InvalidRequestException(String longMsg) { + super(CommitteeNodeErrorCodeEnum.SERVER_INVALID_REQUEST, longMsg); + } + + public InvalidRequestException(String formatStr, Object... objects) { + super(CommitteeNodeErrorCodeEnum.SERVER_INVALID_REQUEST, StrUtil.format(formatStr, objects)); + } + + public InvalidRequestException(Throwable throwable, String formatStr, Object... objects) { + super(CommitteeNodeErrorCodeEnum.SERVER_INVALID_REQUEST, StrUtil.format(formatStr, objects), throwable); + } + + public InvalidRequestException(String longMsg, Throwable throwable) { + super(CommitteeNodeErrorCodeEnum.SERVER_INVALID_REQUEST, longMsg, throwable); + } +} diff --git a/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/commons/models/BCDNSServiceDO.java b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/commons/models/BCDNSServiceDO.java new file mode 100755 index 00000000..1500d5ee --- /dev/null +++ b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/commons/models/BCDNSServiceDO.java @@ -0,0 +1,60 @@ +/* + * Copyright 2024 Ant Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alipay.antchain.bridge.ptc.committee.monitor.node.commons.models; + +import java.io.IOException; +import java.lang.reflect.Type; + +import com.alibaba.fastjson.annotation.JSONField; +import com.alibaba.fastjson.serializer.JSONSerializer; +import com.alibaba.fastjson.serializer.ObjectSerializer; +import com.alipay.antchain.bridge.bcdns.service.BCDNSTypeEnum; +import com.alipay.antchain.bridge.commons.bcdns.utils.CrossChainCertificateUtil; +import com.alipay.antchain.bridge.commons.core.base.ObjectIdentity; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.commons.enums.BCDNSStateEnum; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +public class BCDNSServiceDO { + + private String domainSpace; + + @JSONField(serialize = false, deserialize = false) + private ObjectIdentity ownerOid; + + @JSONField(name = "domainSpaceCert", serializeUsing = DomainSpaceCertSerializer.class) + private DomainSpaceCertWrapper domainSpaceCertWrapper; + + private BCDNSTypeEnum type; + + private BCDNSStateEnum state; + + private byte[] properties; + + public static class DomainSpaceCertSerializer implements ObjectSerializer { + @Override + public void write(JSONSerializer serializer, Object object, Object fieldName, Type fieldType, int features) throws IOException { + serializer.write(CrossChainCertificateUtil.formatCrossChainCertificateToPem(((DomainSpaceCertWrapper) object).getDomainSpaceCert())); + } + } +} diff --git a/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/commons/models/BtaWrapper.java b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/commons/models/BtaWrapper.java new file mode 100755 index 00000000..688b0220 --- /dev/null +++ b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/commons/models/BtaWrapper.java @@ -0,0 +1,52 @@ +/* + * Copyright 2024 Ant Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alipay.antchain.bridge.ptc.committee.monitor.node.commons.models; + +import cn.hutool.core.util.ObjectUtil; +import com.alipay.antchain.bridge.commons.core.base.CrossChainDomain; +import com.alipay.antchain.bridge.commons.core.bta.IBlockchainTrustAnchor; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +public class BtaWrapper { + + private IBlockchainTrustAnchor bta; + + public String getDomain() { + return ObjectUtil.defaultIfNull(bta.getDomain(), new CrossChainDomain()).getDomain(); + } + + public String getProduct() { + return bta.getSubjectProduct(); + } + + public int getSubjectVersion() { + return bta.getSubjectVersion(); + } + + public int getBtaVersion() { + return bta.getVersion(); + } + + +} diff --git a/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/commons/models/DomainSpaceCertWrapper.java b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/commons/models/DomainSpaceCertWrapper.java new file mode 100755 index 00000000..ee617c39 --- /dev/null +++ b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/commons/models/DomainSpaceCertWrapper.java @@ -0,0 +1,46 @@ +/* + * Copyright 2024 Ant Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alipay.antchain.bridge.ptc.committee.monitor.node.commons.models; + +import com.alipay.antchain.bridge.commons.bcdns.AbstractCrossChainCertificate; +import com.alipay.antchain.bridge.commons.bcdns.utils.CrossChainCertificateUtil; +import com.alipay.antchain.bridge.commons.core.base.CrossChainDomain; +import com.alipay.antchain.bridge.commons.core.base.ObjectIdentity; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class DomainSpaceCertWrapper { + + public DomainSpaceCertWrapper(AbstractCrossChainCertificate domainSpaceCert) { + this.domainSpaceCert = domainSpaceCert; + this.domainSpace = CrossChainCertificateUtil.isBCDNSTrustRoot(domainSpaceCert) ? + CrossChainDomain.ROOT_DOMAIN_SPACE : CrossChainCertificateUtil.getCrossChainDomainSpace(domainSpaceCert).getDomain(); + this.parentDomainSpace = CrossChainCertificateUtil.isBCDNSTrustRoot(domainSpaceCert) ? + null : CrossChainCertificateUtil.getParentDomainSpace(domainSpaceCert).getDomain(); + this.ownerOid = domainSpaceCert.getCredentialSubjectInstance().getApplicant(); + } + + private String domainSpace; + + private String parentDomainSpace; + + private ObjectIdentity ownerOid; + + private AbstractCrossChainCertificate domainSpaceCert; +} diff --git a/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/commons/models/TpBtaWrapper.java b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/commons/models/TpBtaWrapper.java new file mode 100755 index 00000000..e1e0c56a --- /dev/null +++ b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/commons/models/TpBtaWrapper.java @@ -0,0 +1,47 @@ +/* + * Copyright 2024 Ant Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alipay.antchain.bridge.ptc.committee.monitor.node.commons.models; + +import com.alipay.antchain.bridge.commons.core.base.CrossChainLane; +import com.alipay.antchain.bridge.commons.core.ptc.ThirdPartyBlockchainTrustAnchor; +import com.alipay.antchain.bridge.ptc.committee.types.basic.CommitteeEndorseProof; +import com.alipay.antchain.bridge.ptc.committee.types.tpbta.CommitteeEndorseRoot; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +public class TpBtaWrapper { + + private ThirdPartyBlockchainTrustAnchor tpbta; + + public CommitteeEndorseRoot getEndorseRoot() { + return CommitteeEndorseRoot.decode(tpbta.getEndorseRoot()); + } + + public CommitteeEndorseProof getEndorseProof() { + return CommitteeEndorseProof.decode(tpbta.getEndorseProof()); + } + + public CrossChainLane getCrossChainLane() { + return tpbta.getCrossChainLane(); + } +} diff --git a/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/commons/models/ValidatedConsensusStateWrapper.java b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/commons/models/ValidatedConsensusStateWrapper.java new file mode 100755 index 00000000..2c2015aa --- /dev/null +++ b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/commons/models/ValidatedConsensusStateWrapper.java @@ -0,0 +1,48 @@ +/* + * Copyright 2024 Ant Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alipay.antchain.bridge.ptc.committee.monitor.node.commons.models; + +import java.math.BigInteger; + +import cn.hutool.core.util.ObjectUtil; +import com.alipay.antchain.bridge.commons.core.base.CrossChainDomain; +import com.alipay.antchain.bridge.commons.core.ptc.ValidatedConsensusState; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@AllArgsConstructor +@NoArgsConstructor +@Getter +@Setter +public class ValidatedConsensusStateWrapper { + + private ValidatedConsensusState validatedConsensusState; + + public String getDomain() { + return ObjectUtil.defaultIfNull(validatedConsensusState.getDomain(), new CrossChainDomain()).getDomain(); + } + + public BigInteger getHeight() { + return validatedConsensusState.getHeight(); + } + + public String getParentHash() { + return validatedConsensusState.getParentHashHex(); + } +} diff --git a/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/config/CredentialConfig.java b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/config/CredentialConfig.java new file mode 100755 index 00000000..2ae40338 --- /dev/null +++ b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/config/CredentialConfig.java @@ -0,0 +1,49 @@ +/* + * Copyright 2024 Ant Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alipay.antchain.bridge.ptc.committee.monitor.node.config; + +import java.security.PrivateKey; + +import com.alipay.antchain.bridge.commons.bcdns.AbstractCrossChainCertificate; +import com.alipay.antchain.bridge.commons.bcdns.utils.CrossChainCertificateUtil; +import com.alipay.antchain.bridge.commons.utils.crypto.SignAlgoEnum; +import lombok.Getter; +import lombok.SneakyThrows; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.Resource; + +@Configuration +@Getter +public class CredentialConfig { + + @Bean + @SneakyThrows + public PrivateKey nodeKey( + @Value("${committee.node.credential.sign-algo:KECCAK256_WITH_SECP256K1}") SignAlgoEnum signAlgo, + @Value("${committee.node.credential.private-key-file}") Resource privateKeyFile) { + return signAlgo.getSigner().readPemPrivateKey(privateKeyFile.getContentAsByteArray()); + } + + @Bean + @SneakyThrows + public AbstractCrossChainCertificate ptcCrossChainCert( + @Value("${committee.node.credential.cert-file}") Resource certFile) { + return CrossChainCertificateUtil.readCrossChainCertificateFromPem(certFile.getContentAsByteArray()); + } +} diff --git a/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/config/ServerConfig.java b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/config/ServerConfig.java new file mode 100755 index 00000000..1984f517 --- /dev/null +++ b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/config/ServerConfig.java @@ -0,0 +1,56 @@ +/* + * Copyright 2024 Ant Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alipay.antchain.bridge.ptc.committee.monitor.node.config; + +import java.net.InetAddress; +import java.net.InetSocketAddress; + +import cn.hutool.core.util.StrUtil; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.server.AdminServiceImpl; +import io.grpc.Server; +import io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@Slf4j +public class ServerConfig { + + @Value("${committee.node.admin.host:localhost}") + private String adminHost; + + @Value("${committee.node.admin.port:10088}") + private int adminPort; + + @Bean + @SneakyThrows + public Server adminGrpcServer(@Autowired AdminServiceImpl adminService) { + log.info("Starting plugin managing server on port {}", adminPort); + return NettyServerBuilder.forAddress( + new InetSocketAddress( + StrUtil.isEmpty(adminHost) ? InetAddress.getLoopbackAddress() : InetAddress.getByName(adminHost), + adminPort + ) + ).addService(adminService) + .build() + .start(); + } +} diff --git a/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/dal/convert/ConvertUtil.java b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/dal/convert/ConvertUtil.java new file mode 100755 index 00000000..e67657de --- /dev/null +++ b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/dal/convert/ConvertUtil.java @@ -0,0 +1,132 @@ +/* + * Copyright 2024 Ant Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alipay.antchain.bridge.ptc.committee.monitor.node.dal.convert; + +import cn.hutool.core.util.HexUtil; +import cn.hutool.core.util.ObjectUtil; +import com.alipay.antchain.bridge.bcdns.service.BCDNSTypeEnum; +import com.alipay.antchain.bridge.commons.bcdns.CrossChainCertificateFactory; +import com.alipay.antchain.bridge.commons.core.base.ObjectIdentity; +import com.alipay.antchain.bridge.commons.core.bta.BlockchainTrustAnchorFactory; +import com.alipay.antchain.bridge.commons.core.ptc.ThirdPartyBlockchainTrustAnchor; +import com.alipay.antchain.bridge.commons.core.ptc.ValidatedConsensusState; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.commons.models.*; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.dal.entities.*; + +public class ConvertUtil { + + @SuppressWarnings("unchecked") + public static T convertFrom(F from) { + return switch (from) { + case TpBtaEntity entity -> (T) convertFrom(entity); + case TpBtaWrapper wrapper -> (T) convertFrom(wrapper); + case BtaEntity entity -> (T) convertFrom(entity); + case BtaWrapper wrapper -> (T) convertFrom(wrapper); + case ValidatedConsensusStatesEntity entity -> (T) convertFrom(entity); + case ValidatedConsensusStateWrapper wrapper -> (T) convertFrom(wrapper); + case BCDNSServiceDO serviceDO -> (T) convertFrom(serviceDO); + case BCDNSServiceEntity entity -> (T) convertFrom(entity); + case DomainSpaceCertWrapper wrapper -> (T) convertFrom(wrapper); + case DomainSpaceCertEntity entity -> (T) convertFrom(entity); + default -> throw new IllegalArgumentException("Unsupported type: " + from.getClass().getName()); + }; + } + + private static TpBtaWrapper convertFrom(TpBtaEntity entity) { + return new TpBtaWrapper( + ThirdPartyBlockchainTrustAnchor.decode(entity.getRawTpBta()) + ); + } + + private static TpBtaEntity convertFrom(TpBtaWrapper wrapper) { + return TpBtaEntity.builder() + .tpbtaVersion(wrapper.getTpbta().getTpbtaVersion()) + .ptcVerifyAnchorVersion(wrapper.getTpbta().getPtcVerifyAnchorVersion().longValue()) + .btaSubjectVersion(wrapper.getTpbta().getBtaSubjectVersion()) + .senderDomain(wrapper.getCrossChainLane().getSenderDomain().getDomain()) + .senderId(ObjectUtil.isNull(wrapper.getCrossChainLane().getSenderId()) ? "" : wrapper.getCrossChainLane().getSenderIdHex()) + .receiverDomain(ObjectUtil.isNull(wrapper.getCrossChainLane().getReceiverDomain()) ? "" : wrapper.getCrossChainLane().getReceiverDomain().getDomain()) + .receiverId(ObjectUtil.isNull(wrapper.getCrossChainLane().getReceiverId()) ? "" : wrapper.getCrossChainLane().getReceiverIdHex()) + .rawTpBta(wrapper.getTpbta().encode()) + .build(); + } + + private static BtaWrapper convertFrom(BtaEntity entity) { + return new BtaWrapper(BlockchainTrustAnchorFactory.createBTA(entity.getRawBta())); + } + + private static BtaEntity convertFrom(BtaWrapper wrapper) { + return BtaEntity.builder() + .domain(wrapper.getDomain()) + .product(wrapper.getProduct()) + .btaVersion(wrapper.getBtaVersion()) + .subjectVersion(wrapper.getSubjectVersion()) + .rawBta(wrapper.getBta().encode()) + .build(); + } + + private static ValidatedConsensusStateWrapper convertFrom(ValidatedConsensusStatesEntity entity) { + return new ValidatedConsensusStateWrapper(ValidatedConsensusState.decode(entity.getRawVcs())); + } + + private static ValidatedConsensusStatesEntity convertFrom(ValidatedConsensusStateWrapper wrapper) { + return ValidatedConsensusStatesEntity.builder() + .csVersion(wrapper.getValidatedConsensusState().getCsVersion()) + .hash(HexUtil.encodeHexStr(wrapper.getValidatedConsensusState().getHash())) + .height(wrapper.getValidatedConsensusState().getHeight().toString()) + .domain(wrapper.getDomain()) + .rawVcs(wrapper.getValidatedConsensusState().encode()) + .parentHash(wrapper.getParentHash()) + .build(); + } + + + private static BCDNSServiceEntity convertFrom(BCDNSServiceDO serviceDO) { + return BCDNSServiceEntity.builder() + .domainSpace(serviceDO.getDomainSpace()) + .type(serviceDO.getType().getCode()) + .parentSpace(serviceDO.getDomainSpaceCertWrapper().getParentDomainSpace()) + .ownerOid(HexUtil.encodeHexStr(serviceDO.getOwnerOid().encode())) + .state(serviceDO.getState()) + .properties(serviceDO.getProperties()) + .build(); + } + + private static BCDNSServiceDO convertFrom(BCDNSServiceEntity entity) { + return new BCDNSServiceDO( + entity.getDomainSpace(), + ObjectIdentity.decode(HexUtil.decodeHex(entity.getOwnerOid())), + null, + BCDNSTypeEnum.parseFromValue(entity.getType()), + entity.getState(), + entity.getProperties() + ); + } + + private static DomainSpaceCertEntity convertFrom(DomainSpaceCertWrapper wrapper) { + return DomainSpaceCertEntity.builder() + .domainSpace(wrapper.getDomainSpace()) + .parentSpace(wrapper.getParentDomainSpace()) + .ownerOidHex(HexUtil.encodeHexStr(wrapper.getOwnerOid().encode())) + .domainSpaceCert(wrapper.getDomainSpaceCert().encode()) + .build(); + } + + private static DomainSpaceCertWrapper convertFrom(DomainSpaceCertEntity entity) { + return new DomainSpaceCertWrapper(CrossChainCertificateFactory.createCrossChainCertificate(entity.getDomainSpaceCert())); + } +} diff --git a/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/dal/entities/BCDNSServiceEntity.java b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/dal/entities/BCDNSServiceEntity.java new file mode 100755 index 00000000..cdb9e546 --- /dev/null +++ b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/dal/entities/BCDNSServiceEntity.java @@ -0,0 +1,49 @@ +/* + * Copyright 2024 Ant Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alipay.antchain.bridge.ptc.committee.monitor.node.dal.entities; + +import com.alipay.antchain.bridge.ptc.committee.monitor.node.commons.enums.BCDNSStateEnum; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +@Getter +@Setter +@Builder +@AllArgsConstructor +@NoArgsConstructor +@TableName("bcdns_service") +public class BCDNSServiceEntity extends BaseEntity { + + @TableField("domain_space") + private String domainSpace; + + @TableField("parent_space") + private String parentSpace; + + @TableField("owner_oid") + private String ownerOid; + + @TableField("type") + private String type; + + @TableField("state") + private BCDNSStateEnum state; + + @TableField("properties") + private byte[] properties; +} diff --git a/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/dal/entities/BaseEntity.java b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/dal/entities/BaseEntity.java new file mode 100755 index 00000000..0b8cb696 --- /dev/null +++ b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/dal/entities/BaseEntity.java @@ -0,0 +1,58 @@ +/* + * Copyright 2024 Ant Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alipay.antchain.bridge.ptc.committee.monitor.node.dal.entities; + +import java.util.Date; + +import com.baomidou.mybatisplus.annotation.FieldStrategy; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.FieldNameConstants; + +@Getter +@Setter +@FieldNameConstants +public class BaseEntity { + + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + /** + * 数据创建时间 + */ + @TableField("gmt_create") + private Date gmtCreate; + + /** + * 数据更新时间 + */ + @TableField(value = "gmt_modified", update = "now()", updateStrategy = FieldStrategy.ALWAYS) + private Date gmtModified; + + public void init() { + Date now = new Date(); + this.setGmtCreate(now); + this.setGmtModified(now); + } + + public void update() { + this.setGmtModified(new Date()); + } +} diff --git a/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/dal/entities/BtaEntity.java b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/dal/entities/BtaEntity.java new file mode 100755 index 00000000..fc27d1bd --- /dev/null +++ b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/dal/entities/BtaEntity.java @@ -0,0 +1,45 @@ +/* + * Copyright 2024 Ant Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alipay.antchain.bridge.ptc.committee.monitor.node.dal.entities; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +@Builder +@TableName("bta") +public class BtaEntity extends BaseEntity { + + @TableField("product") + private String product; + + @TableField("domain") + private String domain; + + @TableField("bta_version") + private int btaVersion; + + @TableField("subject_version") + private int subjectVersion; + + @TableField("raw_bta") + private byte[] rawBta; +} diff --git a/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/dal/entities/DomainSpaceCertEntity.java b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/dal/entities/DomainSpaceCertEntity.java new file mode 100755 index 00000000..6fe98dcb --- /dev/null +++ b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/dal/entities/DomainSpaceCertEntity.java @@ -0,0 +1,45 @@ +/* + * Copyright 2023 Ant Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alipay.antchain.bridge.ptc.committee.monitor.node.dal.entities; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +@TableName("domain_space_cert") +public class DomainSpaceCertEntity extends BaseEntity { + + @TableField("domain_space") + private String domainSpace; + + @TableField("parent_space") + private String parentSpace; + + @TableField("owner_oid_hex") + private String ownerOidHex; + + @TableField("description") + private String description; + + @TableField("domain_space_cert") + private byte[] domainSpaceCert; +} diff --git a/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/dal/entities/SystemConfigEntity.java b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/dal/entities/SystemConfigEntity.java new file mode 100755 index 00000000..ca2e3bc2 --- /dev/null +++ b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/dal/entities/SystemConfigEntity.java @@ -0,0 +1,38 @@ +/* + * Copyright 2024 Ant Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alipay.antchain.bridge.ptc.committee.monitor.node.dal.entities; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +@Getter +@Setter +@NoArgsConstructor +@Builder +@AllArgsConstructor +@TableName("system_config") +public class SystemConfigEntity extends BaseEntity { + @TableField("conf_key") + private String confKey; + + @TableField("conf_value") + private String confValue; +} + + + diff --git a/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/dal/entities/TpBtaEntity.java b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/dal/entities/TpBtaEntity.java new file mode 100755 index 00000000..45c68c3e --- /dev/null +++ b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/dal/entities/TpBtaEntity.java @@ -0,0 +1,57 @@ +/* + * Copyright 2024 Ant Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alipay.antchain.bridge.ptc.committee.monitor.node.dal.entities; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +@Builder +@TableName("tpbta") +public class TpBtaEntity extends BaseEntity { + + @TableField("version") + private int version; + + @TableField("sender_domain") + private String senderDomain; + + @TableField("bta_subject_version") + private int btaSubjectVersion; + + @TableField("sender_id") + private String senderId; + + @TableField("receiver_domain") + private String receiverDomain; + + @TableField("receiver_id") + private String receiverId; + + @TableField("tpbta_version") + private int tpbtaVersion; + + @TableField("ptc_verify_anchor_version") + private long ptcVerifyAnchorVersion; + + @TableField("raw_tpbta") + private byte[] rawTpBta; +} diff --git a/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/dal/entities/ValidatedConsensusStatesEntity.java b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/dal/entities/ValidatedConsensusStatesEntity.java new file mode 100755 index 00000000..0f443f5a --- /dev/null +++ b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/dal/entities/ValidatedConsensusStatesEntity.java @@ -0,0 +1,48 @@ +/* + * Copyright 2024 Ant Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alipay.antchain.bridge.ptc.committee.monitor.node.dal.entities; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +@Builder +@TableName("validated_consensus_states") +public class ValidatedConsensusStatesEntity extends BaseEntity { + + @TableField("cs_version") + private short csVersion; + + @TableField("domain") + private String domain; + + @TableField("height") + private String height; + + @TableField("hash") + private String hash; + + @TableField("parent_hash") + private String parentHash; + + @TableField("raw_vcs") + private byte[] rawVcs; +} diff --git a/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/dal/mapper/BCDNSServiceMapper.java b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/dal/mapper/BCDNSServiceMapper.java new file mode 100755 index 00000000..b05c1512 --- /dev/null +++ b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/dal/mapper/BCDNSServiceMapper.java @@ -0,0 +1,23 @@ +/* + * Copyright 2024 Ant Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alipay.antchain.bridge.ptc.committee.monitor.node.dal.mapper; + +import com.alipay.antchain.bridge.ptc.committee.monitor.node.dal.entities.BCDNSServiceEntity; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +public interface BCDNSServiceMapper extends BaseMapper { +} diff --git a/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/dal/mapper/BtaMapper.java b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/dal/mapper/BtaMapper.java new file mode 100755 index 00000000..f5b615f2 --- /dev/null +++ b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/dal/mapper/BtaMapper.java @@ -0,0 +1,23 @@ +/* + * Copyright 2024 Ant Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alipay.antchain.bridge.ptc.committee.monitor.node.dal.mapper; + +import com.alipay.antchain.bridge.ptc.committee.monitor.node.dal.entities.BtaEntity; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +public interface BtaMapper extends BaseMapper { +} diff --git a/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/dal/mapper/DomainSpaceCertMapper.java b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/dal/mapper/DomainSpaceCertMapper.java new file mode 100755 index 00000000..300107a0 --- /dev/null +++ b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/dal/mapper/DomainSpaceCertMapper.java @@ -0,0 +1,23 @@ +/* + * Copyright 2024 Ant Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alipay.antchain.bridge.ptc.committee.monitor.node.dal.mapper; + +import com.alipay.antchain.bridge.ptc.committee.monitor.node.dal.entities.DomainSpaceCertEntity; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +public interface DomainSpaceCertMapper extends BaseMapper { +} diff --git a/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/dal/mapper/SystemConfigMapper.java b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/dal/mapper/SystemConfigMapper.java new file mode 100755 index 00000000..ce909a22 --- /dev/null +++ b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/dal/mapper/SystemConfigMapper.java @@ -0,0 +1,23 @@ +/* + * Copyright 2024 Ant Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alipay.antchain.bridge.ptc.committee.monitor.node.dal.mapper; + +import com.alipay.antchain.bridge.ptc.committee.monitor.node.dal.entities.SystemConfigEntity; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +public interface SystemConfigMapper extends BaseMapper { +} diff --git a/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/dal/mapper/TpBtaMapper.java b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/dal/mapper/TpBtaMapper.java new file mode 100755 index 00000000..efaf17c7 --- /dev/null +++ b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/dal/mapper/TpBtaMapper.java @@ -0,0 +1,23 @@ +/* + * Copyright 2024 Ant Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alipay.antchain.bridge.ptc.committee.monitor.node.dal.mapper; + +import com.alipay.antchain.bridge.ptc.committee.monitor.node.dal.entities.TpBtaEntity; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +public interface TpBtaMapper extends BaseMapper { +} diff --git a/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/dal/mapper/ValidatedConsensusStatesMapper.java b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/dal/mapper/ValidatedConsensusStatesMapper.java new file mode 100755 index 00000000..bb28695e --- /dev/null +++ b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/dal/mapper/ValidatedConsensusStatesMapper.java @@ -0,0 +1,28 @@ +/* + * Copyright 2024 Ant Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alipay.antchain.bridge.ptc.committee.monitor.node.dal.mapper; + +import com.alipay.antchain.bridge.ptc.committee.monitor.node.dal.entities.ValidatedConsensusStatesEntity; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +public interface ValidatedConsensusStatesMapper extends BaseMapper { + + @Select("SELECT * FROM validated_consensus_states WHERE domain = #{domain} AND height = (SELECT MAX(height) FROM validated_consensus_states WHERE domain = #{domain})") + ValidatedConsensusStatesEntity getLatestValidatedConsensusState(@Param("domain") String domain); +} diff --git a/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/dal/repository/BCDNSRepositoryImpl.java b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/dal/repository/BCDNSRepositoryImpl.java new file mode 100755 index 00000000..92aa2ca6 --- /dev/null +++ b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/dal/repository/BCDNSRepositoryImpl.java @@ -0,0 +1,317 @@ +/* + * Copyright 2024 Ant Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alipay.antchain.bridge.ptc.committee.monitor.node.dal.repository; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import cn.hutool.core.codec.Base64; +import cn.hutool.core.collection.ListUtil; +import cn.hutool.core.util.HexUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import com.alipay.antchain.bridge.commons.bcdns.CrossChainCertificateFactory; +import com.alipay.antchain.bridge.commons.core.base.CrossChainDomain; +import com.alipay.antchain.bridge.commons.core.base.ObjectIdentity; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.commons.enums.BCDNSStateEnum; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.commons.exception.DataAccessLayerException; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.commons.models.BCDNSServiceDO; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.commons.models.DomainSpaceCertWrapper; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.dal.convert.ConvertUtil; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.dal.entities.BCDNSServiceEntity; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.dal.entities.DomainSpaceCertEntity; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.dal.mapper.BCDNSServiceMapper; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.dal.mapper.DomainSpaceCertMapper; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.dal.repository.interfaces.IBCDNSRepository; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +@Component +@Slf4j +public class BCDNSRepositoryImpl implements IBCDNSRepository { + + @Resource + private BCDNSServiceMapper bcdnsServiceMapper; + + @Resource + private DomainSpaceCertMapper domainSpaceCertMapper; + + @Override + public long countBCDNSService() { + return bcdnsServiceMapper.selectCount(null); + } + + @Override + public boolean hasDomainSpaceCert(String domainSpace) { + return domainSpaceCertMapper.exists( + new LambdaQueryWrapper() + .eq(DomainSpaceCertEntity::getDomainSpace, domainSpace) + ); + } + + @Override + public void saveDomainSpaceCert(DomainSpaceCertWrapper domainSpaceCertWrapper) { + if (hasDomainSpaceCert(domainSpaceCertWrapper.getDomainSpace())) { + log.info("domain space cert for {} already exists", domainSpaceCertWrapper.getDomainSpace()); + return; + } + + try { + domainSpaceCertMapper.insert((DomainSpaceCertEntity) ConvertUtil.convertFrom(domainSpaceCertWrapper)); + } catch (Exception e) { + throw new DataAccessLayerException( + e, "save domain space cert for {} failed", domainSpaceCertWrapper.getDomainSpace() + ); + } + } + + @Override + public DomainSpaceCertWrapper getDomainSpaceCert(String domainSpace) { + try { + var entity = domainSpaceCertMapper.selectOne( + new LambdaQueryWrapper() + .select(ListUtil.toList(DomainSpaceCertEntity::getDomainSpaceCert)) + .eq(DomainSpaceCertEntity::getDomainSpace, domainSpace) + ); + if (ObjectUtil.isNull(entity)) { + return null; + } + return new DomainSpaceCertWrapper( + CrossChainCertificateFactory.createCrossChainCertificate(entity.getDomainSpaceCert()) + ); + } catch (Exception e) { + throw new DataAccessLayerException( + e, "get domain space cert for {} failed", domainSpace + ); + } + } + + @Override + public DomainSpaceCertWrapper getDomainSpaceCert(ObjectIdentity ownerOid) { + try { + var entityList = domainSpaceCertMapper.selectList( + new LambdaQueryWrapper() + .eq(DomainSpaceCertEntity::getOwnerOidHex, HexUtil.encodeHexStr(ownerOid.encode())) + ); + if (ObjectUtil.isEmpty(entityList)) { + return null; + } + return ConvertUtil.convertFrom(entityList.getFirst()); + } catch (Exception e) { + throw new DataAccessLayerException( + e, + "failed to get domain space certificate by owner oid {}", + HexUtil.encodeHexStr(ownerOid.encode()) + ); + } + } + + @Override + public Map getDomainSpaceCertChain(String leafDomainSpace) { + Map result = new HashMap<>(); + try { + addDomainSpaceCert(leafDomainSpace, result); + } catch (Exception e) { + throw new DataAccessLayerException( + e, + "failed to get domain space certificate chain for space {}", + leafDomainSpace + ); + } + + return result; + } + + @Override + public List getDomainSpaceChain(String leafDomainSpace) { + List result = ListUtil.toList(leafDomainSpace); + String currDomainSpace = leafDomainSpace; + try { + do { + if (StrUtil.equals(leafDomainSpace, CrossChainDomain.ROOT_DOMAIN_SPACE)) { + break; + } + DomainSpaceCertEntity entity = domainSpaceCertMapper.selectOne( + new LambdaQueryWrapper() + .select(ListUtil.toList(DomainSpaceCertEntity::getParentSpace)) + .eq(DomainSpaceCertEntity::getDomainSpace, leafDomainSpace) + ); + if (ObjectUtil.isNull(entity)) { + throw new RuntimeException(StrUtil.format("none data found for domain space {}", currDomainSpace)); + } + result.add(entity.getParentSpace()); + currDomainSpace = entity.getParentSpace(); + } while (StrUtil.isNotEmpty(currDomainSpace)); + } catch (Exception e) { + throw new DataAccessLayerException( + e, + "failed to get domain space certificate chain for space {}", + leafDomainSpace + ); + } + return result; + } + + @Override + public boolean hasBCDNSService(String domainSpace) { + try { + return bcdnsServiceMapper.exists( + new LambdaQueryWrapper() + .eq(BCDNSServiceEntity::getDomainSpace, domainSpace) + ); + } catch (Exception e) { + throw new DataAccessLayerException( + e, "check if bcdns service for {} exist failed", domainSpace + ); + } + } + + @Override + public BCDNSServiceDO getBCDNSServiceDO(String domainSpace) { + try { + var entity = bcdnsServiceMapper.selectOne( + new LambdaQueryWrapper() + .eq(BCDNSServiceEntity::getDomainSpace, domainSpace) + ); + if (ObjectUtil.isNull(entity)) { + return null; + } + BCDNSServiceDO bcdnsServiceDO = ConvertUtil.convertFrom(entity); + bcdnsServiceDO.setDomainSpaceCertWrapper(getDomainSpaceCert(domainSpace)); + return bcdnsServiceDO; + } catch (Exception e) { + throw new DataAccessLayerException( + e, "get bcdns service for {} failed", domainSpace + ); + } + } + + @Override + @Transactional(rollbackFor = DataAccessLayerException.class) + public void deleteBCDNSServiceDO(String domainSpace) { + try { + domainSpaceCertMapper.delete( + new LambdaQueryWrapper() + .eq(DomainSpaceCertEntity::getDomainSpace, domainSpace) + ); + bcdnsServiceMapper.delete( + new LambdaQueryWrapper() + .eq(BCDNSServiceEntity::getDomainSpace, domainSpace) + ); + } catch (Exception e) { + throw new DataAccessLayerException( + e, + "failed to delete bcdns data for space [{}]", + domainSpace + ); + } + } + + @Override + public List getAllBCDNSDomainSpace() { + try { + var domainSpaceCertEntities = domainSpaceCertMapper.selectList( + new LambdaQueryWrapper() + .select(ListUtil.toList(DomainSpaceCertEntity::getDomainSpace)) + ); + if (ObjectUtil.isEmpty(domainSpaceCertEntities)) { + return new ArrayList<>(); + } + + return domainSpaceCertEntities.stream().map(DomainSpaceCertEntity::getDomainSpace).collect(Collectors.toList()); + } catch (Exception e) { + throw new DataAccessLayerException( + e, + "failed to get all domain space for bcdns" + ); + } + } + + @Override + @Transactional(rollbackFor = DataAccessLayerException.class) + public void saveBCDNSServiceDO(BCDNSServiceDO bcdnsServiceDO) { + try { + bcdnsServiceMapper.insert((BCDNSServiceEntity) ConvertUtil.convertFrom(bcdnsServiceDO)); + domainSpaceCertMapper.insert((DomainSpaceCertEntity) ConvertUtil.convertFrom(bcdnsServiceDO.getDomainSpaceCertWrapper())); + } catch (Exception e) { + throw new DataAccessLayerException( + e, "save bcdns service failed, domainSpace = {}", + bcdnsServiceDO.getDomainSpace() + ); + } + } + + @Override + public void updateBCDNSServiceState(String domainSpace, BCDNSStateEnum stateEnum) { + try { + bcdnsServiceMapper.update( + new LambdaUpdateWrapper() + .eq(BCDNSServiceEntity::getDomainSpace, domainSpace) + .set(BCDNSServiceEntity::getState, stateEnum.getCode()) + ); + } catch (Exception e) { + throw new DataAccessLayerException( + e, "update bcdns service state to {} failed, domainSpace = {}", + stateEnum.name(), domainSpace + ); + } + } + + @Override + public void updateBCDNSServiceProperties(String domainSpace, byte[] rawProp) { + try { + if ( + bcdnsServiceMapper.update( + BCDNSServiceEntity.builder() + .properties(rawProp) + .build(), + new LambdaQueryWrapper() + .eq(BCDNSServiceEntity::getDomainSpace, domainSpace) + ) != 1 + ) { + throw new RuntimeException("failed to update bcdns record"); + } + } catch (Exception e) { + throw new DataAccessLayerException( + e, + "failed to update bcdns properties to {} for space {}", + Base64.encode(rawProp), domainSpace + ); + } + } + + private void addDomainSpaceCert(String currDomainSpace, Map result) { + DomainSpaceCertEntity entity = domainSpaceCertMapper.selectOne( + new LambdaQueryWrapper() + .eq(DomainSpaceCertEntity::getDomainSpace, currDomainSpace) + ); + if (ObjectUtil.isNull(entity)) { + throw new RuntimeException(StrUtil.format("domain space cert not found for {}", currDomainSpace)); + } + result.put(currDomainSpace, ConvertUtil.convertFrom(entity)); + if (!StrUtil.equals(currDomainSpace, CrossChainDomain.ROOT_DOMAIN_SPACE)) { + addDomainSpaceCert(entity.getParentSpace(), result); + } + } +} diff --git a/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/dal/repository/EndorseServiceRepositoryImpl.java b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/dal/repository/EndorseServiceRepositoryImpl.java new file mode 100755 index 00000000..be290dda --- /dev/null +++ b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/dal/repository/EndorseServiceRepositoryImpl.java @@ -0,0 +1,351 @@ +/* + * Copyright 2024 Ant Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alipay.antchain.bridge.ptc.committee.monitor.node.dal.repository; + +import java.math.BigInteger; +import java.util.Comparator; +import java.util.List; + +import cn.hutool.core.collection.ListUtil; +import cn.hutool.core.util.ObjectUtil; +import com.alipay.antchain.bridge.commons.core.base.CrossChainLane; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.commons.exception.DataAccessLayerException; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.commons.models.BtaWrapper; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.commons.models.TpBtaWrapper; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.commons.models.ValidatedConsensusStateWrapper; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.dal.convert.ConvertUtil; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.dal.entities.BtaEntity; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.dal.entities.TpBtaEntity; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.dal.entities.ValidatedConsensusStatesEntity; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.dal.mapper.BtaMapper; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.dal.mapper.TpBtaMapper; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.dal.mapper.ValidatedConsensusStatesMapper; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.dal.repository.interfaces.IEndorseServiceRepository; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Component; + +@Component +public class EndorseServiceRepositoryImpl implements IEndorseServiceRepository { + + @Resource + private BtaMapper btaMapper; + + @Resource + private TpBtaMapper tpBtaMapper; + + @Resource + private ValidatedConsensusStatesMapper validatedConsensusStatesMapper; + + @Override + public TpBtaWrapper getMatchedTpBta(CrossChainLane lane) { + try { + var entityList = searchTpBta(lane, -1); + if (ObjectUtil.isEmpty(entityList)) { + return null; + } + return ConvertUtil.convertFrom( + entityList.stream().max(Comparator.comparingInt(TpBtaEntity::getTpbtaVersion)).get() + ); + + } catch (Exception e) { + throw new DataAccessLayerException( + e, "Failed to get tpbta for lane {}", lane.getLaneKey() + ); + } + } + + @Override + public TpBtaWrapper getMatchedTpBta(CrossChainLane lane, int tpbtaVersion) { + try { + var entityList = searchTpBta(lane, tpbtaVersion); + if (ObjectUtil.isEmpty(entityList)) { + return null; + } + return ConvertUtil.convertFrom( + entityList.stream().max(Comparator.comparingInt(TpBtaEntity::getTpbtaVersion)).get() + ); + } catch (Exception e) { + throw new DataAccessLayerException( + e, "Failed to get tpbta for lane {} and version {}", lane.getLaneKey(), tpbtaVersion + ); + } + } + + @Override + public TpBtaWrapper getExactTpBta(CrossChainLane lane) { + return getExactTpBta(lane, -1); + } + + @Override + public TpBtaWrapper getExactTpBta(CrossChainLane lane, int tpbtaVersion) { + try { + var wrapper = new LambdaQueryWrapper() + .eq(TpBtaEntity::getSenderDomain, lane.getSenderDomain().getDomain()) + .eq(TpBtaEntity::getSenderId, ObjectUtil.isNull(lane.getSenderId()) ? "" : lane.getSenderId().toHex()) + .eq(TpBtaEntity::getReceiverDomain, ObjectUtil.isNull(lane.getReceiverDomain()) ? "" : lane.getReceiverDomain().getDomain()) + .eq(TpBtaEntity::getReceiverId, ObjectUtil.isNull(lane.getReceiverId()) ? "" : lane.getReceiverId().toHex()); + var entityList = tpBtaMapper.selectList( + tpbtaVersion == -1 ? wrapper : wrapper.eq(TpBtaEntity::getTpbtaVersion, tpbtaVersion) + ); + if (ObjectUtil.isEmpty(entityList)) { + return null; + } + return ConvertUtil.convertFrom( + entityList.stream().max(Comparator.comparingInt(TpBtaEntity::getTpbtaVersion)).get() + ); + } catch (Exception e) { + throw new DataAccessLayerException( + e, "Failed to get tpbta for lane {} and version {}", lane.getLaneKey(), tpbtaVersion + ); + } + } + + @Override + public void setTpBta(TpBtaWrapper tpBtaWrapper) { + try { + if (hasTpBta(tpBtaWrapper.getCrossChainLane(), tpBtaWrapper.getTpbta().getTpbtaVersion())) { + throw new RuntimeException("tpBta already exists"); + } + tpBtaMapper.insert((TpBtaEntity) ConvertUtil.convertFrom(tpBtaWrapper)); + } catch (Exception e) { + throw new DataAccessLayerException( + e, "Failed to save tpbta for lane {}", tpBtaWrapper.getCrossChainLane().getLaneKey() + ); + } + } + + @Override + public boolean hasTpBta(CrossChainLane lane, int tpbtaVersion) { + try { + return tpBtaMapper.exists( + new LambdaQueryWrapper() + .eq(TpBtaEntity::getSenderDomain, lane.getSenderDomain().getDomain()) + .eq(TpBtaEntity::getSenderId, ObjectUtil.isNull(lane.getSenderId()) ? "" : lane.getSenderId().toHex()) + .eq(TpBtaEntity::getReceiverDomain, ObjectUtil.isNull(lane.getReceiverDomain()) ? "" : lane.getReceiverDomain().getDomain()) + .eq(TpBtaEntity::getReceiverId, ObjectUtil.isNull(lane.getReceiverId()) ? "" : lane.getReceiverId().toHex()) + .eq(TpBtaEntity::getTpbtaVersion, tpbtaVersion) + ); + } catch (Exception e) { + throw new DataAccessLayerException( + e, "Failed to check if tpbta for lane {} and version {} exist", lane.getLaneKey(), tpbtaVersion + ); + } + } + + @Override + public BtaWrapper getBta(String domain) { + try { + var entityList = btaMapper.selectList( + new LambdaQueryWrapper() + .eq(BtaEntity::getDomain, domain) + ); + if (ObjectUtil.isEmpty(entityList)) { + return null; + } + return ConvertUtil.convertFrom( + entityList.stream().max(Comparator.comparingInt(BtaEntity::getSubjectVersion)).get() + ); + } catch (Exception e) { + throw new DataAccessLayerException( + e, "Failed to get bta for domain {}", domain + ); + } + } + + @Override + public BtaWrapper getBta(String domain, int subjectVersion) { + try { + var entity = btaMapper.selectOne( + new LambdaQueryWrapper() + .eq(BtaEntity::getDomain, domain) + .eq(BtaEntity::getSubjectVersion, subjectVersion) + ); + if (ObjectUtil.isNull(entity)) { + return null; + } + return ConvertUtil.convertFrom(entity); + } catch (Exception e) { + throw new DataAccessLayerException( + e, "Failed to get bta for domain {} and version {}", domain, subjectVersion + ); + } + } + + @Override + public void setBta(BtaWrapper btaWrapper) { + try { + if (hasBta(btaWrapper.getDomain(), btaWrapper.getBtaVersion())) { + throw new RuntimeException("bta already exists"); + } + btaMapper.insert((BtaEntity) ConvertUtil.convertFrom(btaWrapper)); + } catch (Exception e) { + throw new DataAccessLayerException( + e, "Failed to save bta for domain {} and subject version {}", btaWrapper.getDomain(), btaWrapper.getSubjectVersion() + ); + } + } + + @Override + public boolean hasBta(String domain, int subjectVersion) { + try { + return btaMapper.exists( + new LambdaQueryWrapper() + .eq(BtaEntity::getDomain, domain) + .eq(BtaEntity::getSubjectVersion, subjectVersion) + ); + } catch (Exception e) { + throw new DataAccessLayerException( + e, "Failed to check if bta for domain {} and version {} exist", domain, subjectVersion + ); + } + } + + @Override + public ValidatedConsensusStateWrapper getLatestValidatedConsensusState(String domain) { + try { + var entity = validatedConsensusStatesMapper.getLatestValidatedConsensusState(domain); + if (ObjectUtil.isNull(entity)) { + return null; + } + return ConvertUtil.convertFrom(entity); + } catch (Exception e) { + throw new DataAccessLayerException( + e, "Failed to get latest validated consensus state for domain {}", domain + ); + } + } + + @Override + public ValidatedConsensusStateWrapper getValidatedConsensusState(String domain, BigInteger height) { + try { + var entity = validatedConsensusStatesMapper.selectOne( + new LambdaQueryWrapper() + .eq(ValidatedConsensusStatesEntity::getDomain, domain) + .eq(ValidatedConsensusStatesEntity::getHeight, height.toString()) + ); + if (ObjectUtil.isNull(entity)) { + return null; + } + return ConvertUtil.convertFrom(entity); + } catch (Exception e) { + throw new DataAccessLayerException( + e, "Failed to get validated consensus state for domain {} and height {}", domain, height + ); + } + } + + @Override + public ValidatedConsensusStateWrapper getValidatedConsensusState(String domain, String hash) { + try { + var entity = validatedConsensusStatesMapper.selectOne( + new LambdaQueryWrapper() + .eq(ValidatedConsensusStatesEntity::getDomain, domain) + .eq(ValidatedConsensusStatesEntity::getHash, hash) + ); + if (ObjectUtil.isNull(entity)) { + return null; + } + return ConvertUtil.convertFrom(entity); + } catch (Exception e) { + throw new DataAccessLayerException( + e, "Failed to get validated consensus state for domain {} and hash {}", domain, hash + ); + } + } + + @Override + public void setValidatedConsensusState(ValidatedConsensusStateWrapper validatedConsensusStateWrapper) { + try { + if (hasValidatedConsensusState(validatedConsensusStateWrapper.getDomain(), validatedConsensusStateWrapper.getHeight())) { + throw new RuntimeException("validated consensus state already exists"); + } + validatedConsensusStatesMapper.insert((ValidatedConsensusStatesEntity) ConvertUtil.convertFrom(validatedConsensusStateWrapper)); + } catch (Exception e) { + throw new DataAccessLayerException( + e, "Failed to save validated consensus state for domain {} and height {}", + validatedConsensusStateWrapper.getDomain(), + validatedConsensusStateWrapper.getHeight() + ); + } + } + + @Override + public boolean hasValidatedConsensusState(String domain, BigInteger height) { + try { + return validatedConsensusStatesMapper.exists( + new LambdaQueryWrapper() + .eq(ValidatedConsensusStatesEntity::getDomain, domain) + .eq(ValidatedConsensusStatesEntity::getHeight, height.toString()) + ); + } catch (Exception e) { + throw new DataAccessLayerException( + e, "Failed to check if validated consensus state for domain {} and height {} exist", domain, height + ); + } + } + + private List searchTpBta(CrossChainLane lane, int tpbtaVersion) { + // search the blockchain level first + var wrapper = new LambdaQueryWrapper() + .eq(TpBtaEntity::getSenderDomain, lane.getSenderDomain().getDomain()) + .eq(TpBtaEntity::getReceiverDomain, "") + .eq(TpBtaEntity::getSenderId, "") + .eq(TpBtaEntity::getReceiverId, ""); + var entityList = tpBtaMapper.selectList( + tpbtaVersion == -1 ? wrapper : wrapper.eq(TpBtaEntity::getTpbtaVersion, tpbtaVersion) + ); + if (ObjectUtil.isNotEmpty(entityList)) { + return entityList; + } + + if (ObjectUtil.isNull(lane.getReceiverDomain()) || ObjectUtil.isEmpty(lane.getReceiverDomain().getDomain())) { + return ListUtil.empty(); + } + // search the channel level + wrapper = new LambdaQueryWrapper() + .eq(TpBtaEntity::getSenderDomain, lane.getSenderDomain().getDomain()) + .eq(TpBtaEntity::getReceiverDomain, lane.getReceiverDomain().getDomain()) + .eq(TpBtaEntity::getSenderId, "") + .eq(TpBtaEntity::getReceiverId, ""); + entityList = tpBtaMapper.selectList( + tpbtaVersion == -1 ? wrapper : wrapper.eq(TpBtaEntity::getTpbtaVersion, tpbtaVersion) + ); + if (ObjectUtil.isNotEmpty(entityList)) { + return entityList; + } + + if (ObjectUtil.isNull(lane.getSenderId()) || ObjectUtil.isNull(lane.getReceiverId()) + || ObjectUtil.isEmpty(lane.getSenderId().getRawID()) || ObjectUtil.isEmpty(lane.getReceiverId().getRawID())) { + return ListUtil.empty(); + } + // search the lane level + wrapper = new LambdaQueryWrapper() + .eq(TpBtaEntity::getSenderDomain, lane.getSenderDomain().getDomain()) + .eq(TpBtaEntity::getSenderId, lane.getSenderId().toHex()) + .eq(TpBtaEntity::getReceiverDomain, lane.getReceiverDomain().getDomain()) + .eq(TpBtaEntity::getReceiverId, lane.getReceiverId().toHex()); + entityList = tpBtaMapper.selectList( + tpbtaVersion == -1 ? wrapper : wrapper.eq(TpBtaEntity::getTpbtaVersion, tpbtaVersion) + ); + if (ObjectUtil.isNotEmpty(entityList)) { + return entityList; + } + + return ListUtil.empty(); + } +} diff --git a/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/dal/repository/SystemConfigRepository.java b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/dal/repository/SystemConfigRepository.java new file mode 100755 index 00000000..16811b14 --- /dev/null +++ b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/dal/repository/SystemConfigRepository.java @@ -0,0 +1,138 @@ +/* + * Copyright 2024 Ant Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alipay.antchain.bridge.ptc.committee.monitor.node.dal.repository; + +import java.math.BigInteger; +import java.util.Map; + +import cn.hutool.core.codec.Base64; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSON; +import com.alipay.antchain.bridge.commons.core.ptc.PTCTrustRoot; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.commons.exception.DataAccessLayerException; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.dal.entities.SystemConfigEntity; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.dal.mapper.SystemConfigMapper; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.dal.repository.interfaces.ISystemConfigRepository; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import jakarta.annotation.Resource; +import lombok.Synchronized; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +@Component +public class SystemConfigRepository implements ISystemConfigRepository { + + private static final String CURRENT_PTC_ANCHOR_VERSION = "current_ptc_anchor_version"; + + private static final String CURRENT_PTC_TRUST_ROOT = "current_ptc_trust_root"; + + @Resource + private SystemConfigMapper systemConfigMapper; + + @Override + public String getSystemConfig(String key) { + try { + var entity = systemConfigMapper.selectOne( + new LambdaQueryWrapper() + .eq(SystemConfigEntity::getConfKey, key) + ); + if (ObjectUtil.isNull(entity)) { + return null; + } + return entity.getConfValue(); + } catch (Exception e) { + throw new DataAccessLayerException( + e, "Failed to get system config with key: {}", key + ); + } + } + + @Override + public boolean hasSystemConfig(String key) { + try { + return systemConfigMapper.exists( + new LambdaQueryWrapper() + .eq(SystemConfigEntity::getConfKey, key) + ); + } catch (Exception e) { + throw new DataAccessLayerException( + e, "Failed to check existence of system config with key: {}", key + ); + } + } + + @Override + public void setSystemConfig(Map configs) { + try { + configs.forEach((key, value) -> { + systemConfigMapper.insert( + SystemConfigEntity.builder() + .confKey(key) + .confValue(value) + .build() + ); + }); + } catch (Exception e) { + throw new DataAccessLayerException( + e, "Failed to set system config with key: {}", JSON.toJSONString(configs) + ); + } + } + + @Override + @Synchronized + public void setSystemConfig(String key, String value) { + try { + systemConfigMapper.insert( + SystemConfigEntity.builder() + .confKey(key) + .confValue(value) + .build() + ); + } catch (Exception e) { + throw new DataAccessLayerException( + e, "Failed to set system config with key: {}", key + ); + } + } + + @Override + public BigInteger queryCurrentPtcAnchorVersion() { + String curr = getSystemConfig(CURRENT_PTC_ANCHOR_VERSION); + return new BigInteger(StrUtil.isEmpty(curr) ? "-1" : curr); + } + + @Override + public void setCurrentPtcAnchorVersion(BigInteger version) { + setSystemConfig(CURRENT_PTC_ANCHOR_VERSION, version.toString()); + } + + @Override + @Transactional(rollbackFor = DataAccessLayerException.class) + public void setPtcTrustRoot(PTCTrustRoot ptcTrustRoot) { + setSystemConfig(CURRENT_PTC_TRUST_ROOT, Base64.encode(ptcTrustRoot.encode())); + setCurrentPtcAnchorVersion( + ptcTrustRoot.getVerifyAnchorMap().keySet().stream().max(BigInteger::compareTo).orElse(BigInteger.ZERO) + ); + } + + @Override + public PTCTrustRoot getPtcTrustRoot() { + return PTCTrustRoot.decode(Base64.decode(getSystemConfig(CURRENT_PTC_TRUST_ROOT))); + } +} diff --git a/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/dal/repository/interfaces/IBCDNSRepository.java b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/dal/repository/interfaces/IBCDNSRepository.java new file mode 100755 index 00000000..230d915d --- /dev/null +++ b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/dal/repository/interfaces/IBCDNSRepository.java @@ -0,0 +1,56 @@ +/* + * Copyright 2024 Ant Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alipay.antchain.bridge.ptc.committee.monitor.node.dal.repository.interfaces; + +import java.util.List; +import java.util.Map; + +import com.alipay.antchain.bridge.commons.core.base.ObjectIdentity; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.commons.enums.BCDNSStateEnum; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.commons.models.BCDNSServiceDO; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.commons.models.DomainSpaceCertWrapper; + +public interface IBCDNSRepository { + + long countBCDNSService(); + + boolean hasDomainSpaceCert(String domainSpace); + + void saveDomainSpaceCert(DomainSpaceCertWrapper domainSpaceCertWrapper); + + DomainSpaceCertWrapper getDomainSpaceCert(String domainSpace); + + DomainSpaceCertWrapper getDomainSpaceCert(ObjectIdentity ownerOid); + + Map getDomainSpaceCertChain(String leafDomainSpace); + + List getDomainSpaceChain(String leafDomainSpace); + + boolean hasBCDNSService(String domainSpace); + + BCDNSServiceDO getBCDNSServiceDO(String domainSpace); + + void deleteBCDNSServiceDO(String domainSpace); + + List getAllBCDNSDomainSpace(); + + void saveBCDNSServiceDO(BCDNSServiceDO bcdnsServiceDO); + + void updateBCDNSServiceState(String domainSpace, BCDNSStateEnum stateEnum); + + void updateBCDNSServiceProperties(String domainSpace, byte[] rawProp); +} diff --git a/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/dal/repository/interfaces/IEndorseServiceRepository.java b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/dal/repository/interfaces/IEndorseServiceRepository.java new file mode 100755 index 00000000..31badc48 --- /dev/null +++ b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/dal/repository/interfaces/IEndorseServiceRepository.java @@ -0,0 +1,57 @@ +/* + * Copyright 2024 Ant Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alipay.antchain.bridge.ptc.committee.monitor.node.dal.repository.interfaces; + +import java.math.BigInteger; + +import com.alipay.antchain.bridge.commons.core.base.CrossChainLane; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.commons.models.BtaWrapper; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.commons.models.TpBtaWrapper; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.commons.models.ValidatedConsensusStateWrapper; + +public interface IEndorseServiceRepository { + + TpBtaWrapper getMatchedTpBta(CrossChainLane lane); + + TpBtaWrapper getMatchedTpBta(CrossChainLane lane, int tpbtaVersion); + + TpBtaWrapper getExactTpBta(CrossChainLane lane); + + TpBtaWrapper getExactTpBta(CrossChainLane lane, int tpbtaVersion); + + void setTpBta(TpBtaWrapper tpBtaWrapper); + + boolean hasTpBta(CrossChainLane lane, int tpbtaVersion); + + BtaWrapper getBta(String domain); + + BtaWrapper getBta(String domain, int subjectVersion); + + void setBta(BtaWrapper btaWrapper); + + boolean hasBta(String domain, int subjectVersion); + + ValidatedConsensusStateWrapper getLatestValidatedConsensusState(String domain); + + ValidatedConsensusStateWrapper getValidatedConsensusState(String domain, BigInteger height); + + ValidatedConsensusStateWrapper getValidatedConsensusState(String domain, String hash); + + void setValidatedConsensusState(ValidatedConsensusStateWrapper validatedConsensusStateWrapper); + + boolean hasValidatedConsensusState(String domain, BigInteger height); +} diff --git a/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/dal/repository/interfaces/ISystemConfigRepository.java b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/dal/repository/interfaces/ISystemConfigRepository.java new file mode 100755 index 00000000..6e2cfae1 --- /dev/null +++ b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/dal/repository/interfaces/ISystemConfigRepository.java @@ -0,0 +1,41 @@ +/* + * Copyright 2024 Ant Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alipay.antchain.bridge.ptc.committee.monitor.node.dal.repository.interfaces; + +import java.math.BigInteger; +import java.util.Map; + +import com.alipay.antchain.bridge.commons.core.ptc.PTCTrustRoot; + +public interface ISystemConfigRepository { + + String getSystemConfig(String key); + + boolean hasSystemConfig(String key); + + void setSystemConfig(Map configs); + + void setSystemConfig(String key, String value); + + BigInteger queryCurrentPtcAnchorVersion(); + + void setCurrentPtcAnchorVersion(BigInteger version); + + void setPtcTrustRoot(PTCTrustRoot ptcTrustRoot); + + PTCTrustRoot getPtcTrustRoot(); +} diff --git a/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/server/AdminServiceImpl.java b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/server/AdminServiceImpl.java new file mode 100755 index 00000000..0e15cdb6 --- /dev/null +++ b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/server/AdminServiceImpl.java @@ -0,0 +1,171 @@ +/* + * Copyright 2024 Ant Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alipay.antchain.bridge.ptc.committee.monitor.node.server; + +import cn.hutool.core.codec.Base64; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSON; +import com.alipay.antchain.bridge.bcdns.service.BCDNSTypeEnum; +import com.alipay.antchain.bridge.commons.bcdns.utils.CrossChainCertificateUtil; +import com.alipay.antchain.bridge.commons.core.ptc.PTCTrustRoot; +import com.alipay.antchain.bridge.commons.core.ptc.PTCTypeEnum; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.dal.repository.interfaces.ISystemConfigRepository; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.server.grpc.*; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.service.IBCDNSManageService; +import io.grpc.stub.StreamObserver; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +@Component +@Slf4j +public class AdminServiceImpl extends AdminServiceGrpc.AdminServiceImplBase { + + @Resource + private IBCDNSManageService bcdnsManageService; + + @Resource + private ISystemConfigRepository systemConfigRepository; + + @Override + public void registerBcdnsService(RegisterBcdnsServiceRequest request, StreamObserver responseObserver) { + try { + log.info("register bcdns service {}", request.getDomainSpace()); + bcdnsManageService.registerBCDNSService( + request.getDomainSpace(), + BCDNSTypeEnum.parseFromValue(request.getBcdnsType()), + request.getConfig().toByteArray(), + StrUtil.isEmpty(request.getBcdnsRootCert()) ? null : CrossChainCertificateUtil.readCrossChainCertificateFromPem(request.getBcdnsRootCert().getBytes()) + ); + responseObserver.onNext(Response.newBuilder().setCode(0).build()); + } catch (Throwable t) { + log.error("register bcdns service {} failed", request.getDomainSpace(), t); + responseObserver.onNext(Response.newBuilder().setCode(-1).setErrorMsg(t.getMessage()).build()); + } finally { + responseObserver.onCompleted(); + } + } + + @Override + public void getBcdnsServiceInfo(GetBcdnsServiceInfoRequest request, StreamObserver responseObserver) { + try { + log.info("get bcdns service info {}", request.getDomainSpace()); + var bcdnsServiceDO = bcdnsManageService.getBCDNSServiceData(request.getDomainSpace()); + responseObserver.onNext( + Response.newBuilder() + .setCode(0) + .setGetBcdnsServiceInfoResp( + GetBcdnsServiceInfoResp.newBuilder() + .setInfoJson(ObjectUtil.isNull(bcdnsServiceDO) ? "not found" : JSON.toJSONString(bcdnsServiceDO)) + ).build() + ); + } catch (Throwable t) { + log.error("get bcdns service info {} failed", request.getDomainSpace(), t); + responseObserver.onNext(Response.newBuilder().setCode(-1).setErrorMsg(t.getMessage()).build()); + } finally { + responseObserver.onCompleted(); + } + } + + @Override + public void deleteBcdnsService(DeleteBcdnsServiceRequest request, StreamObserver responseObserver) { + try { + log.info("delete bcdns service {}", request.getDomainSpace()); + bcdnsManageService.deleteBCDNSServiceDate(request.getDomainSpace()); + responseObserver.onNext(Response.newBuilder().setCode(0).build()); + } catch (Throwable t) { + log.error("delete bcdns service {} failed", request.getDomainSpace(), t); + responseObserver.onNext(Response.newBuilder().setCode(-1).setErrorMsg(t.getMessage()).build()); + } finally { + responseObserver.onCompleted(); + } + } + + @Override + public void getBcdnsCertificate(GetBcdnsCertificateRequest request, StreamObserver responseObserver) { + try { + log.info("get bcdns service cert {}", request.getDomainSpace()); + String cert = ""; + var domainSpaceCertWrapper = bcdnsManageService.getDomainSpaceCert(request.getDomainSpace()); + if (ObjectUtil.isNull(domainSpaceCertWrapper) || ObjectUtil.isNull(domainSpaceCertWrapper.getDomainSpaceCert())) { + cert = "not found"; + } else { + cert = CrossChainCertificateUtil.formatCrossChainCertificateToPem(domainSpaceCertWrapper.getDomainSpaceCert()); + } + responseObserver.onNext( + Response.newBuilder() + .setCode(0) + .setGetBcdnsCertificateResp( + GetBcdnsCertificateResp.newBuilder() + .setCertificate(cert) + ).build() + ); + } catch (Throwable t) { + log.error("get bcdns service cert {} failed", request.getDomainSpace(), t); + responseObserver.onNext(Response.newBuilder().setCode(-1).setErrorMsg(t.getMessage()).build()); + } finally { + responseObserver.onCompleted(); + } + } + + @Override + public void stopBcdnsService(StopBcdnsServiceRequest request, StreamObserver responseObserver) { + try { + log.info("stop bcdns service {}", request.getDomainSpace()); + bcdnsManageService.stopBCDNSService(request.getDomainSpace()); + responseObserver.onNext(Response.newBuilder().setCode(0).build()); + } catch (Throwable t) { + log.error("stop bcdns service {} failed", request.getDomainSpace(), t); + responseObserver.onNext(Response.newBuilder().setCode(-1).setErrorMsg(t.getMessage()).build()); + } finally { + responseObserver.onCompleted(); + } + } + + @Override + public void restartBcdnsService(RestartBcdnsServiceRequest request, StreamObserver responseObserver) { + try { + log.info("restart bcdns service {}", request.getDomainSpace()); + bcdnsManageService.restartBCDNSService(request.getDomainSpace()); + responseObserver.onNext(Response.newBuilder().setCode(0).build()); + } catch (Throwable t) { + log.error("restart bcdns service {} failed", request.getDomainSpace(), t); + responseObserver.onNext(Response.newBuilder().setCode(-1).setErrorMsg(t.getMessage()).build()); + } finally { + responseObserver.onCompleted(); + } + } + + @Override + public void addPtcTrustRoot(AddPtcTrustRootRequest request, StreamObserver responseObserver) { + try { + log.info("adding ptc trust root"); + var trustRoot = PTCTrustRoot.decode(request.getRawTrustRoot().toByteArray()); + if (trustRoot.getPtcCredentialSubject().getType() != PTCTypeEnum.COMMITTEE) { + throw new RuntimeException("ptc trust root type must be committee"); + } + systemConfigRepository.setPtcTrustRoot(trustRoot); + responseObserver.onNext(Response.newBuilder().setCode(0).build()); + } catch (Throwable t) { + log.error("add ptc root failed: {}", Base64.encode(request.getRawTrustRoot().toByteArray()), t); + responseObserver.onNext(Response.newBuilder().setCode(-1).setErrorMsg(t.getMessage()).build()); + } finally { + responseObserver.onCompleted(); + } + } +} diff --git a/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/server/MonitorNodeServiceImpl.java b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/server/MonitorNodeServiceImpl.java new file mode 100755 index 00000000..0e7a2a4f --- /dev/null +++ b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/server/MonitorNodeServiceImpl.java @@ -0,0 +1,510 @@ +/* + * Copyright 2024 Ant Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alipay.antchain.bridge.ptc.committee.monitor.node.server; + +import java.math.BigInteger; + +import cn.hutool.core.util.HexUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import com.alipay.antchain.bridge.commons.bcdns.AbstractCrossChainCertificate; +import com.alipay.antchain.bridge.commons.bcdns.CrossChainCertificateFactory; +import com.alipay.antchain.bridge.commons.core.am.AuthMessageFactory; +import com.alipay.antchain.bridge.commons.core.am.IAuthMessage; +import com.alipay.antchain.bridge.commons.core.base.*; +import com.alipay.antchain.bridge.commons.core.bta.BlockchainTrustAnchorFactory; +import com.alipay.antchain.bridge.commons.core.bta.IBlockchainTrustAnchor; +import com.alipay.antchain.bridge.commons.core.monitor.IMonitorMessage; +import com.alipay.antchain.bridge.commons.core.monitor.MonitorMessageFactory; +import com.alipay.antchain.bridge.commons.core.sdp.ISDPMessage; +import com.alipay.antchain.bridge.commons.core.sdp.SDPMessageFactory; +import com.alipay.antchain.bridge.commons.utils.crypto.HashAlgoEnum; +import com.alipay.antchain.bridge.ptc.committee.grpc.*; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.commons.enums.MonitorTypeEnum; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.commons.exception.*; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.commons.models.TpBtaWrapper; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.dal.repository.interfaces.IEndorseServiceRepository; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.server.interceptor.RequestTraceInterceptor; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.service.IEndorserService; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.service.IHcdvsPluginService; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.service.IMonitorService; +import com.alipay.antchain.bridge.ptc.committee.types.basic.CommitteeNodeProof; +import com.google.protobuf.ByteString; +import io.grpc.stub.StreamObserver; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import net.devh.boot.grpc.server.service.GrpcService; +import org.springframework.beans.factory.annotation.Value; + +@Slf4j +@GrpcService(interceptors = RequestTraceInterceptor.class) +public class MonitorNodeServiceImpl extends CommitteeNodeServiceGrpc.CommitteeNodeServiceImplBase { + + @Value("${committee.id}") + private String committeeId; + + @Value("${committee.node.id}") + private String nodeId; + + @Resource + private IHcdvsPluginService hcdvsPluginService; + + @Resource + private IEndorserService endorserService; + + @Resource + private IEndorseServiceRepository endorseServiceRepository; + + @Override + public void heartbeat(HeartbeatRequest request, StreamObserver responseObserver) { + try { + responseObserver.onNext( + Response.newBuilder().setHeartbeatResp( + HeartbeatResponse.newBuilder() + .setCommitteeId(committeeId) + .setNodeId(nodeId) + .addAllProducts(hcdvsPluginService.getAvailableProducts()) + ).build() + + ); + } catch (CommitteeNodeException e) { + responseObserver.onNext( + Response.newBuilder() + .setCode(e.getCodeNum()) + .setErrorMsg(e.getMsg()) + .build() + ); + } catch (Throwable t) { + log.error("process heartbeat failed", t); + responseObserver.onNext( + Response.newBuilder() + .setCode(CommitteeNodeErrorCodeEnum.UNKNOWN_INTERNAL_ERROR.getErrorCodeNum()) + .setErrorMsg(CommitteeNodeErrorCodeEnum.UNKNOWN_INTERNAL_ERROR.getShortMsg()) + .build() + ); + } finally { + responseObserver.onCompleted(); + } + } + + @Override + public void queryTpBta(QueryTpBtaRequest request, StreamObserver responseObserver) { + try { + log.info("query tpbta with lane (sender_domain:{}, receiver_domain:{}, sender_id:{}, receiver_id:{})", + request.getSenderDomain(), request.getReceiverDomain(), request.getSenderId(), request.getReceiverId()); + + if (StrUtil.isEmpty(request.getSenderDomain())) { + throw new InvalidRequestException("sender domain must not be empty"); + } + TpBtaWrapper tpBtaWrapper = endorserService.queryMatchedTpBta( + new CrossChainLane( + new CrossChainDomain(request.getSenderDomain()), + StrUtil.isEmpty(request.getReceiverDomain()) ? null : new CrossChainDomain(request.getReceiverDomain()), + StrUtil.isEmpty(request.getSenderId()) ? null : CrossChainIdentity.fromHexStr(request.getSenderId()), + StrUtil.isEmpty(request.getReceiverId()) ? null : CrossChainIdentity.fromHexStr(request.getReceiverId()) + ) + ); + responseObserver.onNext( + Response.newBuilder() + .setCode(0) + .setQueryTpBtaResp( + QueryTpBtaResponse.newBuilder() + .setRawTpBta(ByteString.copyFrom(tpBtaWrapper.getTpbta().encode())) + .build() + ).build() + ); + } catch (CommitteeNodeException e) { + log.error("query tpbta for lane (sender_domain:{}, receiver_domain:{}, sender_id:{}, receiver_id:{}) failed", + request.getSenderDomain(), request.getReceiverDomain(), request.getSenderId(), request.getReceiverId(), e); + responseObserver.onNext( + Response.newBuilder() + .setCode(e.getCodeNum()) + .setErrorMsg(e.getMsg()) + .build() + ); + } catch (Throwable t) { + log.error("query tpbta for lane (sender_domain:{}, receiver_domain:{}, sender_id:{}, receiver_id:{}) failed with unexpected error: ", + request.getSenderDomain(), request.getReceiverDomain(), request.getSenderId(), request.getReceiverId(), t); + responseObserver.onNext( + Response.newBuilder() + .setCode(CommitteeNodeErrorCodeEnum.UNKNOWN_INTERNAL_ERROR.getErrorCodeNum()) + .setErrorMsg(CommitteeNodeErrorCodeEnum.UNKNOWN_INTERNAL_ERROR.getShortMsg()) + .build() + ); + } finally { + responseObserver.onCompleted(); + } + } + + @Override + public void verifyBta(VerifyBtaRequest request, StreamObserver responseObserver) { + IBlockchainTrustAnchor bta = null; + try { + AbstractCrossChainCertificate domainCert = CrossChainCertificateFactory.createCrossChainCertificate(request.getRawDomainCert().toByteArray()); + if (ObjectUtil.isNull(domainCert)) { + throw new InvalidRequestException("domain cert is null"); + } + bta = BlockchainTrustAnchorFactory.createBTA(request.getRawBta().toByteArray()); + if (ObjectUtil.isNull(bta)) { + throw new InvalidRequestException("bta is null"); + } + + var tpBtaWrapper = endorserService.verifyBta(domainCert, bta); + responseObserver.onNext( + Response.newBuilder() + .setCode(0) + .setVerifyBtaResp( + VerifyBtaResponse.newBuilder() + .setRawTpBta(ByteString.copyFrom(tpBtaWrapper.getTpbta().encode())) + .build() + ).build() + ); + } catch (InvalidBtaException e) { + log.error("bta for domain {} can't pass the verification: ", + ObjectUtil.isNull(bta) || ObjectUtil.isNull(bta.getDomain()) ? "unknown" : bta.getDomain().getDomain(), e); + responseObserver.onNext( + Response.newBuilder() + .setCode(e.getCodeNum()) + .setErrorMsg(e.getMessage()) + .build() + ); + } catch (CommitteeNodeException e) { + log.error("verify bta for domain {} failed", + ObjectUtil.isNull(bta) || ObjectUtil.isNull(bta.getDomain()) ? "unknown" : bta.getDomain().getDomain(), e); + responseObserver.onNext( + Response.newBuilder() + .setCode(e.getCodeNum()) + .setErrorMsg(e.getMsg()) + .build() + ); + } catch (Throwable t) { + log.error("verify bta for domain {} failed with unexpected error: ", + ObjectUtil.isNull(bta) || ObjectUtil.isNull(bta.getDomain()) ? "unknown" : bta.getDomain().getDomain(), t); + responseObserver.onNext( + Response.newBuilder() + .setCode(CommitteeNodeErrorCodeEnum.UNKNOWN_INTERNAL_ERROR.getErrorCodeNum()) + .setErrorMsg(CommitteeNodeErrorCodeEnum.UNKNOWN_INTERNAL_ERROR.getShortMsg()) + .build() + ); + } finally { + responseObserver.onCompleted(); + } + } + + @Override + public void commitAnchorState(CommitAnchorStateRequest request, StreamObserver responseObserver) { + String domain = null; + BigInteger height = null; + String hash = null; + try { + var anchorState = ConsensusState.decode(request.getRawAnchorState().toByteArray()); + if (ObjectUtil.isNull(anchorState)) { + throw new InvalidRequestException("anchor state is null"); + } + var crossChainLane = CrossChainLane.decode(request.getCrossChainLane().toByteArray()); + if (ObjectUtil.isNull(crossChainLane)) { + throw new InvalidRequestException("crossChainLane is null"); + } + + domain = anchorState.getDomain().getDomain(); + height = anchorState.getHeight(); + hash = HexUtil.encodeHexStr(anchorState.getHash()); + log.info("commit anchor state with height {} and hash {} for domain {}", height, hash, domain); + + var vcs = endorserService.commitAnchorState(crossChainLane, anchorState); + + responseObserver.onNext( + Response.newBuilder() + .setCode(0) + .setCommitAnchorStateResp( + CommitAnchorStateResponse.newBuilder() + .setRawValidatedConsensusState(ByteString.copyFrom(vcs.encode())) + .build() + ).build() + ); + } catch (CommitteeNodeException e) { + log.error("commit anchor state with height {} and hash {} for domain {} failed", height, hash, domain, e); + responseObserver.onNext( + Response.newBuilder() + .setCode(e.getCodeNum()) + .setErrorMsg(e.getMsg()) + .build() + ); + } catch (Throwable t) { + log.error("commit anchor state with height {} and hash {} for domain {} failed with unexpected error: ", + height, hash, domain, t); + responseObserver.onNext( + Response.newBuilder() + .setCode(CommitteeNodeErrorCodeEnum.UNKNOWN_INTERNAL_ERROR.getErrorCodeNum()) + .setErrorMsg(CommitteeNodeErrorCodeEnum.UNKNOWN_INTERNAL_ERROR.getShortMsg()) + .build() + ); + } finally { + responseObserver.onCompleted(); + } + } + + @Override + public void commitConsensusState(CommitConsensusStateRequest request, StreamObserver responseObserver) { + String domain = null; + BigInteger height = null; + String hash = null; + try { + var currState = ConsensusState.decode(request.getRawConsensusState().toByteArray()); + if (ObjectUtil.isNull(currState)) { + throw new InvalidRequestException("consensus state is null"); + } + var crossChainLane = CrossChainLane.decode(request.getCrossChainLane().toByteArray()); + if (ObjectUtil.isNull(crossChainLane)) { + throw new InvalidRequestException("crossChainLane is null"); + } + + domain = currState.getDomain().getDomain(); + height = currState.getHeight(); + hash = HexUtil.encodeHexStr(currState.getHash()); + log.info("commit consensus state with height {} and hash {} for domain {}", height, hash, domain); + + var vcs = endorserService.commitConsensusState(crossChainLane, currState); + + responseObserver.onNext( + Response.newBuilder() + .setCode(0) + .setCommitConsensusStateResp( + CommitConsensusStateResponse.newBuilder() + .setRawValidatedConsensusState(ByteString.copyFrom(vcs.encode())) + .build() + ).build() + ); + } catch (InvalidConsensusStateException e) { + log.error("verify consensus state with height {} and hash {} for domain {} failed: {}", + height, hash, domain, e.getMessage()); + responseObserver.onNext( + Response.newBuilder() + .setCode(e.getCodeNum()) + .setErrorMsg(e.getMessage()) + .build() + ); + } catch (CommitteeNodeException e) { + log.error("commit consensus state with height {} and hash {} for domain {} failed", height, hash, domain, e); + responseObserver.onNext( + Response.newBuilder() + .setCode(e.getCodeNum()) + .setErrorMsg(e.getMsg()) + .build() + ); + } catch (Throwable t) { + log.error("commit anchor state with height {} and hash {} for domain {} failed with unexpected error: ", + height, hash, domain, t); + responseObserver.onNext( + Response.newBuilder() + .setCode(CommitteeNodeErrorCodeEnum.UNKNOWN_INTERNAL_ERROR.getErrorCodeNum()) + .setErrorMsg(CommitteeNodeErrorCodeEnum.UNKNOWN_INTERNAL_ERROR.getShortMsg()) + .build() + ); + } finally { + responseObserver.onCompleted(); + } + } + + @Override + public void verifyCrossChainMessage(VerifyCrossChainMessageRequest request, StreamObserver responseObserver) { + String domain = null; + CrossChainMessage.CrossChainMessageType msgType = null; + String msgKey = null; + try { + var ucp = UniformCrosschainPacket.decode(request.getRawUcp().toByteArray()); + if (ObjectUtil.isNull(ucp)) { + throw new InvalidRequestException("ucp is null"); + } + var crossChainLane = CrossChainLane.decode(request.getCrossChainLane().toByteArray()); + if (ObjectUtil.isNull(crossChainLane)) { + throw new InvalidRequestException("crossChainLane is null"); + } + + domain = ucp.getSrcDomain().getDomain(); + msgType = ucp.getSrcMessage().getType(); + msgKey = ObjectUtil.isNull(ucp.getCrossChainLane()) ? + HexUtil.encodeHexStr(ucp.getMessageHash(HashAlgoEnum.KECCAK_256)) + : ucp.getCrossChainLane().getLaneKey(); + log.info("verify crosschain message (type: {}, msg: {}) from domain {}", msgType, msgKey, domain); + + IAuthMessage authMessage = AuthMessageFactory.createAuthMessage(ucp.getSrcMessage().getMessage()); + ISDPMessage sdpMessage = SDPMessageFactory.createSDPMessage(authMessage.getPayload()); + IMonitorMessage monitorMessage = MonitorMessageFactory.createMonitorMessage(sdpMessage.getPayload()); + + CommitteeNodeProof proof = null; + + // 根据监管字段内容 判断是否需要监管 + if (monitorMessage.getMonitorType() == MonitorTypeEnum.MONITOR_OPEN.getCode()) { + log.info("crosschain message: need monitor"); + proof = endorserService.verifyUcpWithMonitorSystem(crossChainLane, ucp); + } else { + // 不监管 把跨链消息发给监管系统即可 + log.info("crosschain message: don't need monitor"); + endorserService.relayUcpToMonitorSystem(ucp); + proof = endorserService.verifyUcp(crossChainLane, ucp); + } + + responseObserver.onNext( + Response.newBuilder() + .setCode(0) + .setVerifyCrossChainMessageResp( + VerifyCrossChainMessageResponse.newBuilder() + .setRawNodeProof(ByteString.copyFrom(proof.encode())) + .build() + ).build() + ); + } catch (InvalidCrossChainMessageException e) { + log.error("crosschain message (type: {}, msg: {}) from domain {} 's verification not passed: {}", + msgType, msgKey, domain, e.getMessage()); + responseObserver.onNext( + Response.newBuilder() + .setCode(e.getCodeNum()) + .setErrorMsg(e.getMessage()) + .build() + ); + } catch (CommitteeNodeException e) { + log.error("verify crosschain message (type: {}, msg: {}) from domain {} failed", msgType, msgKey, domain, e); + responseObserver.onNext( + Response.newBuilder() + .setCode(e.getCodeNum()) + .setErrorMsg(e.getMsg()) + .build() + ); + } catch (Throwable t) { + log.error("verify crosschain message (type: {}, msg: {}) from domain {} failed with unexpected error: ", + msgType, msgKey, domain, t); + responseObserver.onNext( + Response.newBuilder() + .setCode(CommitteeNodeErrorCodeEnum.UNKNOWN_INTERNAL_ERROR.getErrorCodeNum()) + .setErrorMsg(CommitteeNodeErrorCodeEnum.UNKNOWN_INTERNAL_ERROR.getShortMsg()) + .build() + ); + } finally { + responseObserver.onCompleted(); + } + } + + @Override + public void querySupportedBlockchainProducts(QuerySupportedBlockchainProductsRequest request, StreamObserver responseObserver) { + try { + responseObserver.onNext( + Response.newBuilder() + .setCode(0) + .setQuerySupportedBlockchainProductsResp( + QuerySupportedBlockchainProductsResponse.newBuilder() + .addAllProducts(hcdvsPluginService.getAvailableProducts()) + .build() + ).build() + ); + } catch (Throwable t) { + log.error("query supported blockchain products failed with unexpected error: ", t); + responseObserver.onNext( + Response.newBuilder() + .setCode(CommitteeNodeErrorCodeEnum.UNKNOWN_INTERNAL_ERROR.getErrorCodeNum()) + .setErrorMsg(CommitteeNodeErrorCodeEnum.UNKNOWN_INTERNAL_ERROR.getShortMsg()) + .build() + ); + } finally { + responseObserver.onCompleted(); + } + } + + @Override + public void queryBlockState(QueryBlockStateRequest request, StreamObserver responseObserver) { + try { + log.info("query latest block state for {}", request.getDomain()); + + var wrapper = endorseServiceRepository.getLatestValidatedConsensusState(request.getDomain()); + if (ObjectUtil.isNotNull(wrapper)) { + log.debug("get latest block state {} for {}", wrapper.getHeight().toString(), request.getDomain()); + responseObserver.onNext( + Response.newBuilder().setQueryBlockStateResp( + QueryBlockStateResponse.newBuilder().setRawValidatedBlockState(ByteString.copyFrom( + new BlockState( + wrapper.getValidatedConsensusState().getDomain(), + wrapper.getValidatedConsensusState().getHash(), + wrapper.getValidatedConsensusState().getHeight(), + wrapper.getValidatedConsensusState().getStateTimestamp() + ).encode() + )) + ).build() + + ); + } + } catch (CommitteeNodeException e) { + responseObserver.onNext( + Response.newBuilder() + .setCode(e.getCodeNum()) + .setErrorMsg(e.getMsg()) + .build() + ); + } catch (Throwable t) { + log.error("process queryBlockState failed", t); + responseObserver.onNext( + Response.newBuilder() + .setCode(CommitteeNodeErrorCodeEnum.UNKNOWN_INTERNAL_ERROR.getErrorCodeNum()) + .setErrorMsg(CommitteeNodeErrorCodeEnum.UNKNOWN_INTERNAL_ERROR.getShortMsg()) + .build() + ); + } finally { + responseObserver.onCompleted(); + } + } + + @Override + public void endorseBlockState(EndorseBlockStateRequest request, StreamObserver responseObserver) { + String receiverDomain; + String laneKey = null; + BigInteger height = null; + try { + receiverDomain = request.getReceiverDomain(); + var tpbtaLane = CrossChainLane.decode(request.getCrossChainLane().toByteArray()); + height = new BigInteger(request.getHeight()); + laneKey = tpbtaLane.getLaneKey(); + + log.info("endorse block state {} for {} with receiver domain {}", height, laneKey, receiverDomain); + + var resp = endorserService.endorseBlockState(tpbtaLane, receiverDomain, height); + responseObserver.onNext( + Response.newBuilder().setEndorseBlockStateResp( + EndorseBlockStateResponse.newBuilder() + .setBlockStateAuthMsg(ByteString.copyFrom(resp.getBlockStateAuthMsg().encode())) + .setCommitteeNodeProof(ByteString.copyFrom(resp.getCommitteeNodeProof().encode())) + .build() + ).build() + ); + } catch (CommitteeNodeException e) { + responseObserver.onNext( + Response.newBuilder() + .setCode(e.getCodeNum()) + .setErrorMsg(e.getMsg()) + .build() + ); + } catch (Throwable t) { + log.error("process endorseBlockState failed for {} height {}", laneKey, StrUtil.toString(height), t); + responseObserver.onNext( + Response.newBuilder() + .setCode(CommitteeNodeErrorCodeEnum.UNKNOWN_INTERNAL_ERROR.getErrorCodeNum()) + .setErrorMsg(CommitteeNodeErrorCodeEnum.UNKNOWN_INTERNAL_ERROR.getShortMsg()) + .build() + ); + } finally { + responseObserver.onCompleted(); + } + } +} diff --git a/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/server/MonitorOrderServiceImpl.java b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/server/MonitorOrderServiceImpl.java new file mode 100755 index 00000000..9d62fcb3 --- /dev/null +++ b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/server/MonitorOrderServiceImpl.java @@ -0,0 +1,73 @@ +package com.alipay.antchain.bridge.ptc.committee.monitor.node.server; + +import cn.hutool.core.util.ObjectUtil; +import com.alipay.antchain.bridge.commons.core.monitor.MonitorOrderV1; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.commons.exception.CommitteeNodeErrorCodeEnum; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.commons.exception.InvalidRequestException; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.server.interceptor.RequestTraceInterceptor; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.service.IMonitorService; +import com.alipay.antchain.bridge.ptc.committee.monitor.system.grpc.*; +import io.grpc.stub.StreamObserver; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import net.devh.boot.grpc.server.service.GrpcService; + +@Slf4j +@GrpcService(interceptors = RequestTraceInterceptor.class) +public class MonitorOrderServiceImpl extends MonitorOrderServiceGrpc.MonitorOrderServiceImplBase { + + @Resource + private IMonitorService monitorService; + + @Override + public void recvMonitorOrder(RecvMonitorOrderRequest request, StreamObserver responseObserver) { + try { + MonitorOrderV1 monitorOrder = convertFromGRpcMonitorOrder(request.getMonitorOrder()); + if (ObjectUtil.isNull(monitorOrder)) { + throw new InvalidRequestException("monitorOrder is null"); + } + log.info("receive monitor order: " + + "product: {}, domain: {}, monitor_order_type: {}, sender_domain: {}, sender_identity: {}, " + + "receive_domain: {}, receive_identity: {}, transaction_content: {}, extra: {}", + monitorOrder.getProduct(), monitorOrder.getDomain(), monitorOrder.getMonitorOrderType(), + monitorOrder.getSenderDomain(), monitorOrder.getFromAddress(), + monitorOrder.getReceiverDomain(), monitorOrder.getToAddress(), + monitorOrder.getTransactionContent(), monitorOrder.getExtra()); + + monitorService.recvMonitorOrder(monitorOrder); + + responseObserver.onNext( + RecvMonitorOrderResponse.newBuilder() + .setCode(0) + .setErrorMsg("") + .build() + ); + + } catch (Throwable t) { + log.error("query supported blockchain products failed with unexpected error: ", t); + responseObserver.onNext( + RecvMonitorOrderResponse.newBuilder() + .setCode(CommitteeNodeErrorCodeEnum.UNKNOWN_INTERNAL_ERROR.getErrorCodeNum()) + .setErrorMsg(CommitteeNodeErrorCodeEnum.UNKNOWN_INTERNAL_ERROR.getShortMsg()) + .build() + ); + } finally { + responseObserver.onCompleted(); + } + } + + private static MonitorOrderV1 convertFromGRpcMonitorOrder(com.alipay.antchain.bridge.ptc.committee.monitor.system.grpc.MonitorOrder grpcMonitorOrder) { + MonitorOrderV1 monitorOrder = new MonitorOrderV1(); + monitorOrder.setProduct(grpcMonitorOrder.getProduct()); + monitorOrder.setDomain(grpcMonitorOrder.getDomain()); + monitorOrder.setMonitorOrderType(grpcMonitorOrder.getMonitorOrderType()); + monitorOrder.setSenderDomain(grpcMonitorOrder.getSenderDomain()); + monitorOrder.setFromAddress(grpcMonitorOrder.getFromAddress()); + monitorOrder.setReceiverDomain(grpcMonitorOrder.getReceiverDomain()); + monitorOrder.setToAddress(grpcMonitorOrder.getToAddress()); + monitorOrder.setTransactionContent(grpcMonitorOrder.getTransactionContent()); + monitorOrder.setExtra(grpcMonitorOrder.getExtra()); + return monitorOrder; + } + +} diff --git a/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/server/interceptor/RequestTraceInterceptor.java b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/server/interceptor/RequestTraceInterceptor.java new file mode 100755 index 00000000..e905f057 --- /dev/null +++ b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/server/interceptor/RequestTraceInterceptor.java @@ -0,0 +1,33 @@ +package com.alipay.antchain.bridge.ptc.committee.monitor.node.server.interceptor; + +import java.net.InetSocketAddress; + +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import io.grpc.*; +import lombok.extern.slf4j.Slf4j; + +// gRPC服务器端的拦截器,拦截gRPC请求并记录客户端的请求来源(IP地址和端口),主要用于请求追踪和日志记录。 +@Slf4j(topic = "req-trace") +public class RequestTraceInterceptor implements ServerInterceptor { + + @Override + public ServerCall.Listener interceptCall(ServerCall call, Metadata headers, ServerCallHandler next) { + InetSocketAddress clientAddr = (InetSocketAddress) call.getAttributes().get(Grpc.TRANSPORT_ATTR_REMOTE_ADDR); + if (StrUtil.equalsIgnoreCase(call.getMethodDescriptor().getBareMethodName(), "heartbeat")) { + if (ObjectUtil.isNull(clientAddr)) { + log.info("heartbeat from client without address found"); + } else { + log.info("heartbeat from client {}:{}", clientAddr.getHostString(), clientAddr.getPort()); + } + } else { + if (ObjectUtil.isNull(clientAddr)) { + log.debug("crosschain biz call from client without address found"); + } else { + log.debug("crosschain biz call from client {}:{}", clientAddr.getHostString(), clientAddr.getPort()); + } + } + + return next.startCall(call, headers); + } +} diff --git a/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/service/IBCDNSManageService.java b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/service/IBCDNSManageService.java new file mode 100755 index 00000000..90f6f121 --- /dev/null +++ b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/service/IBCDNSManageService.java @@ -0,0 +1,71 @@ +/* + * Copyright 2024 Ant Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alipay.antchain.bridge.ptc.committee.monitor.node.service; + +import java.util.List; +import java.util.Map; + +import com.alipay.antchain.bridge.bcdns.service.BCDNSTypeEnum; +import com.alipay.antchain.bridge.bcdns.service.IBlockChainDomainNameService; +import com.alipay.antchain.bridge.commons.bcdns.AbstractCrossChainCertificate; +import com.alipay.antchain.bridge.commons.core.base.ObjectIdentity; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.commons.models.BCDNSServiceDO; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.commons.models.DomainSpaceCertWrapper; + +public interface IBCDNSManageService { + + long countBCDNSService(); + + IBlockChainDomainNameService getBCDNSClient(String domainSpace); + + void registerBCDNSService(String domainSpace, BCDNSTypeEnum bcdnsType, byte[] config, AbstractCrossChainCertificate bcdnsRootCert); + + IBlockChainDomainNameService startBCDNSService(BCDNSServiceDO bcdnsServiceDO); + + void restartBCDNSService(String domainSpace); + + void stopBCDNSService(String domainSpace); + + void saveBCDNSServiceData(BCDNSServiceDO bcdnsServiceDO); + + BCDNSServiceDO getBCDNSServiceData(String domainSpace); + + void deleteBCDNSServiceDate(String domainSpace); + + List getAllBCDNSDomainSpace(); + + boolean hasBCDNSServiceData(String domainSpace); + + Map getTrustRootCertChain(String domainSpace); + + List getDomainSpaceChain(String domainSpace); + + AbstractCrossChainCertificate getTrustRootCertForRootDomain(); + + boolean validateCrossChainCertificate(AbstractCrossChainCertificate certificate); + + boolean validateCrossChainCertificate( + AbstractCrossChainCertificate certificate, + Map domainSpaceCertPath + ); + + DomainSpaceCertWrapper getDomainSpaceCert(String domainSpace); + + DomainSpaceCertWrapper getDomainSpaceCert(ObjectIdentity ownerOid); + + void saveDomainSpaceCerts(Map domainSpaceCerts); +} diff --git a/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/service/IEndorserService.java b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/service/IEndorserService.java new file mode 100755 index 00000000..debddff4 --- /dev/null +++ b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/service/IEndorserService.java @@ -0,0 +1,49 @@ +/* + * Copyright 2024 Ant Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alipay.antchain.bridge.ptc.committee.monitor.node.service; + +import java.math.BigInteger; + +import com.alipay.antchain.bridge.commons.bcdns.AbstractCrossChainCertificate; +import com.alipay.antchain.bridge.commons.core.base.ConsensusState; +import com.alipay.antchain.bridge.commons.core.base.CrossChainLane; +import com.alipay.antchain.bridge.commons.core.base.UniformCrosschainPacket; +import com.alipay.antchain.bridge.commons.core.bta.IBlockchainTrustAnchor; +import com.alipay.antchain.bridge.commons.core.ptc.ValidatedConsensusState; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.commons.exception.InvalidBtaException; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.commons.models.TpBtaWrapper; +import com.alipay.antchain.bridge.ptc.committee.types.basic.CommitteeNodeProof; +import com.alipay.antchain.bridge.ptc.committee.types.basic.EndorseBlockStateResp; + +public interface IEndorserService { + + TpBtaWrapper queryMatchedTpBta(CrossChainLane lane); + + TpBtaWrapper verifyBta(AbstractCrossChainCertificate domainCert, IBlockchainTrustAnchor bta) throws InvalidBtaException; + + ValidatedConsensusState commitAnchorState(CrossChainLane crossChainLane, ConsensusState anchorState); + + ValidatedConsensusState commitConsensusState(CrossChainLane crossChainLane, ConsensusState currState); + + CommitteeNodeProof verifyUcp(CrossChainLane crossChainLane, UniformCrosschainPacket ucp); + + CommitteeNodeProof verifyUcpWithMonitorSystem(CrossChainLane crossChainLane, UniformCrosschainPacket ucp); + + void relayUcpToMonitorSystem(UniformCrosschainPacket ucp); + + EndorseBlockStateResp endorseBlockState(CrossChainLane crossChainLane, String receiverDomain, BigInteger height); +} diff --git a/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/service/IHcdvsPluginService.java b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/service/IHcdvsPluginService.java new file mode 100755 index 00000000..f9c92c7d --- /dev/null +++ b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/service/IHcdvsPluginService.java @@ -0,0 +1,52 @@ +/* + * Copyright 2024 Ant Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alipay.antchain.bridge.ptc.committee.monitor.node.service; + +import com.alipay.antchain.bridge.plugins.manager.core.IAntChainBridgePlugin; +import com.alipay.antchain.bridge.plugins.spi.ptc.IHeteroChainDataVerifierService; + +import java.util.List; + +public interface IHcdvsPluginService { + void loadPlugins(); + + void startPlugins(); + + void loadPlugin(String path); + + void startPlugin(String path); + + void stopPlugin(String product); + + void startPluginFromStop(String product); + + void reloadPlugin(String product); + + void reloadPlugin(String product, String path); + + IAntChainBridgePlugin getPlugin(String product); + + boolean hasPlugin(String product); + + IHeteroChainDataVerifierService createHCDVSService(String product); + + IHeteroChainDataVerifierService getHCDVSService(String product); + + boolean hasProduct(String product); + + List getAvailableProducts(); +} diff --git a/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/service/IMonitorService.java b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/service/IMonitorService.java new file mode 100755 index 00000000..f3a18c4f --- /dev/null +++ b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/service/IMonitorService.java @@ -0,0 +1,9 @@ +package com.alipay.antchain.bridge.ptc.committee.monitor.node.service; + +import com.alipay.antchain.bridge.commons.core.monitor.MonitorOrderV1; + +public interface IMonitorService { + + void recvMonitorOrder(MonitorOrderV1 monitorOrder); + +} diff --git a/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/service/IScheduledTaskService.java b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/service/IScheduledTaskService.java new file mode 100755 index 00000000..1c2a601f --- /dev/null +++ b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/service/IScheduledTaskService.java @@ -0,0 +1,22 @@ +/* + * Copyright 2024 Ant Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alipay.antchain.bridge.ptc.committee.monitor.node.service; + +public interface IScheduledTaskService { + + void listenPtcTrustRoot(); +} diff --git a/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/service/impl/BCDNSManageService.java b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/service/impl/BCDNSManageService.java new file mode 100755 index 00000000..38fb9d0a --- /dev/null +++ b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/service/impl/BCDNSManageService.java @@ -0,0 +1,359 @@ +/* + * Copyright 2024 Ant Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alipay.antchain.bridge.ptc.committee.monitor.node.service.impl; + +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +import cn.hutool.core.util.HexUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import com.alipay.antchain.bridge.bcdns.factory.BlockChainDomainNameServiceFactory; +import com.alipay.antchain.bridge.bcdns.service.BCDNSTypeEnum; +import com.alipay.antchain.bridge.bcdns.service.IBlockChainDomainNameService; +import com.alipay.antchain.bridge.bcdns.types.resp.QueryBCDNSTrustRootCertificateResponse; +import com.alipay.antchain.bridge.commons.bcdns.AbstractCrossChainCertificate; +import com.alipay.antchain.bridge.commons.bcdns.utils.CrossChainCertificateUtil; +import com.alipay.antchain.bridge.commons.core.base.CrossChainDomain; +import com.alipay.antchain.bridge.commons.core.base.ObjectIdentity; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.commons.enums.BCDNSStateEnum; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.commons.exception.CommitteeNodeException; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.commons.exception.CommitteeNodeInternalException; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.commons.models.BCDNSServiceDO; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.commons.models.DomainSpaceCertWrapper; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.dal.repository.interfaces.IBCDNSRepository; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.service.IBCDNSManageService; +import jakarta.annotation.Resource; +import lombok.Synchronized; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +@Component +@Slf4j +public class BCDNSManageService implements IBCDNSManageService { + + @Resource + private IBCDNSRepository bcdnsRepository; + + private final Map bcdnsClientMap = new ConcurrentHashMap<>(); + + @Override + public long countBCDNSService() { + return bcdnsRepository.countBCDNSService(); + } + + @Override + public IBlockChainDomainNameService getBCDNSClient(String domainSpace) { + if (bcdnsClientMap.containsKey(domainSpace)) { + return bcdnsClientMap.get(domainSpace); + } + + BCDNSServiceDO bcdnsServiceDO = getBCDNSServiceData(domainSpace); + if (ObjectUtil.isNull(bcdnsServiceDO)) { + log.warn("none bcdns data found for domain space {}", domainSpace); + return null; + } + if (bcdnsServiceDO.getState() != BCDNSStateEnum.WORKING) { + throw new CommitteeNodeInternalException("BCDNS with domain space {} is not working now", domainSpace); + } + + return startBCDNSService(bcdnsServiceDO); + } + + @Override + @Synchronized + @Transactional(rollbackFor = CommitteeNodeException.class) + public void registerBCDNSService(String domainSpace, BCDNSTypeEnum bcdnsType, byte[] config, AbstractCrossChainCertificate bcdnsRootCert) { + try { + if (hasBCDNSServiceData(domainSpace)) { + throw new RuntimeException("bcdns already registered"); + } + + var bcdnsServiceDO = new BCDNSServiceDO(); + bcdnsServiceDO.setType(bcdnsType); + bcdnsServiceDO.setDomainSpace(domainSpace); + bcdnsServiceDO.setProperties(config); + + if (ObjectUtil.isNotNull(bcdnsRootCert)) { + if (CrossChainCertificateUtil.isBCDNSTrustRoot(bcdnsRootCert) + && !StrUtil.equals(domainSpace, CrossChainDomain.ROOT_DOMAIN_SPACE)) { + throw new RuntimeException("the space name of bcdns trust root certificate supposed to have the root space name bug got : " + domainSpace); + } else if (!CrossChainCertificateUtil.isBCDNSTrustRoot(bcdnsRootCert) + && !CrossChainCertificateUtil.isDomainSpaceCert(bcdnsRootCert)) { + throw new RuntimeException("expected bcdns trust root or domain space type certificate bug got : " + bcdnsRootCert.getType().name()); + } + bcdnsServiceDO.setDomainSpaceCertWrapper( + new DomainSpaceCertWrapper(bcdnsRootCert) + ); + bcdnsServiceDO.setOwnerOid(bcdnsRootCert.getCredentialSubjectInstance().getApplicant()); + } + + startBCDNSService(bcdnsServiceDO); + saveBCDNSServiceData(bcdnsServiceDO); + } catch (CommitteeNodeException e) { + throw e; + } catch (Exception e) { + throw new CommitteeNodeInternalException( + e, + "failed to register bcdns service client for [{}]", + domainSpace + ); + } + } + + @Override + @Synchronized + public IBlockChainDomainNameService startBCDNSService(BCDNSServiceDO bcdnsServiceDO) { + log.info("starting the bcdns service ( type: {}, domain_space: {} )", + bcdnsServiceDO.getType().getCode(), bcdnsServiceDO.getDomainSpace()); + try { + IBlockChainDomainNameService service = BlockChainDomainNameServiceFactory.create(bcdnsServiceDO.getType(), bcdnsServiceDO.getProperties()); + if (ObjectUtil.isNull(service)) { + throw new CommitteeNodeInternalException("bcdns {} start failed", bcdnsServiceDO.getDomainSpace()); + } + if ( + ObjectUtil.isNull(bcdnsServiceDO.getDomainSpaceCertWrapper()) + || ObjectUtil.isNull(bcdnsServiceDO.getDomainSpaceCertWrapper().getDomainSpaceCert()) + ) { + QueryBCDNSTrustRootCertificateResponse response = service.queryBCDNSTrustRootCertificate(); + if (ObjectUtil.isNull(response) || ObjectUtil.isNull(response.getBcdnsTrustRootCertificate())) { + throw new CommitteeNodeInternalException("query empty root cert from bcdns {}", bcdnsServiceDO.getDomainSpace()); + } + bcdnsServiceDO.setOwnerOid(response.getBcdnsTrustRootCertificate().getCredentialSubjectInstance().getApplicant()); + bcdnsServiceDO.setDomainSpaceCertWrapper( + new DomainSpaceCertWrapper(response.getBcdnsTrustRootCertificate()) + ); + } + bcdnsServiceDO.setState(BCDNSStateEnum.WORKING); + bcdnsClientMap.put(bcdnsServiceDO.getDomainSpace(), service); + + return service; + } catch (CommitteeNodeException e) { + throw e; + } catch (Exception e) { + throw new CommitteeNodeInternalException(e, "failed to start bcdns service client for [{}]", bcdnsServiceDO.getDomainSpace()); + } + } + + @Override + @Transactional(rollbackFor = CommitteeNodeException.class) + @Synchronized + public void restartBCDNSService(String domainSpace) { + log.info("restarting the bcdns service ( domain_space: {} )", domainSpace); + try { + var bcdnsServiceDO = getBCDNSServiceData(domainSpace); + if (ObjectUtil.isNull(bcdnsServiceDO)) { + throw new RuntimeException(StrUtil.format("bcdns {} not exist", domainSpace)); + } + if (bcdnsServiceDO.getState() != BCDNSStateEnum.FROZEN) { + throw new RuntimeException(StrUtil.format("bcdns {} already in state {}", domainSpace, bcdnsServiceDO.getState().getCode())); + } + var service = BlockChainDomainNameServiceFactory.create(bcdnsServiceDO.getType(), bcdnsServiceDO.getProperties()); + if (ObjectUtil.isNull(service)) { + throw new CommitteeNodeInternalException("bcdns {} start failed", bcdnsServiceDO.getDomainSpace()); + } + bcdnsRepository.updateBCDNSServiceState(domainSpace, BCDNSStateEnum.WORKING); + bcdnsClientMap.put(bcdnsServiceDO.getDomainSpace(), service); + + } catch (CommitteeNodeException e) { + throw e; + } catch (Exception e) { + throw new CommitteeNodeInternalException( + e, + "failed to restart bcdns service client for {}", + domainSpace + ); + } + } + + @Override + @Transactional(rollbackFor = CommitteeNodeException.class) + @Synchronized + public void stopBCDNSService(String domainSpace) { + log.info("stopping the bcdns service ( domain_space: {} )", domainSpace); + try { + if (!hasBCDNSServiceData(domainSpace)) { + throw new RuntimeException("bcdns not exist for " + domainSpace); + } + bcdnsRepository.updateBCDNSServiceState(domainSpace, BCDNSStateEnum.FROZEN); + bcdnsClientMap.remove(domainSpace); + } catch (CommitteeNodeException e) { + throw e; + } catch (Exception e) { + throw new CommitteeNodeInternalException( + e, + "failed to stop bcdns service client for {}", + domainSpace + ); + } + } + + @Override + public void saveBCDNSServiceData(BCDNSServiceDO bcdnsServiceDO) { + if (bcdnsRepository.hasBCDNSService(bcdnsServiceDO.getDomainSpace())) { + throw new CommitteeNodeInternalException( + "bcdns {} not exist or data incomplete", + bcdnsServiceDO.getDomainSpace() + ); + } + bcdnsRepository.saveBCDNSServiceDO(bcdnsServiceDO); + } + + @Override + public BCDNSServiceDO getBCDNSServiceData(String domainSpace) { + return bcdnsRepository.getBCDNSServiceDO(domainSpace); + } + + @Override + @Synchronized + public void deleteBCDNSServiceDate(String domainSpace) { + bcdnsRepository.deleteBCDNSServiceDO(domainSpace); + bcdnsClientMap.remove(domainSpace); + } + + @Override + public List getAllBCDNSDomainSpace() { + return bcdnsRepository.getAllBCDNSDomainSpace(); + } + + @Override + public boolean hasBCDNSServiceData(String domainSpace) { + return bcdnsRepository.hasBCDNSService(domainSpace); + } + + @Override + public Map getTrustRootCertChain(String domainSpace) { + return bcdnsRepository.getDomainSpaceCertChain(domainSpace).entrySet().stream() + .collect(Collectors.toMap( + Map.Entry::getKey, + entry -> entry.getValue().getDomainSpaceCert() + )); + } + + @Override + public List getDomainSpaceChain(String domainSpace) { + return bcdnsRepository.getDomainSpaceChain(domainSpace); + } + + @Override + public AbstractCrossChainCertificate getTrustRootCertForRootDomain() { + DomainSpaceCertWrapper wrapper = bcdnsRepository.getDomainSpaceCert(CrossChainDomain.ROOT_DOMAIN_SPACE); + if (ObjectUtil.isNull(wrapper)) { + return null; + } + return wrapper.getDomainSpaceCert(); + } + + @Override + public boolean validateCrossChainCertificate(AbstractCrossChainCertificate certificate) { + DomainSpaceCertWrapper trustRootCert = bcdnsRepository.getDomainSpaceCert(certificate.getIssuer()); + if (ObjectUtil.isNull(trustRootCert)) { + log.warn( + "none trust root found for {} to verify for relayer cert: {}", + HexUtil.encodeHexStr(certificate.getIssuer().encode()), + CrossChainCertificateUtil.formatCrossChainCertificateToPem(certificate) + ); + return false; + } + return trustRootCert.getDomainSpaceCert().getCredentialSubjectInstance().verifyIssueProof( + certificate.getEncodedToSign(), + certificate.getProof() + ); + } + + @Override + public DomainSpaceCertWrapper getDomainSpaceCert(String domainSpace) { + return bcdnsRepository.getDomainSpaceCert(domainSpace); + } + + @Override + public DomainSpaceCertWrapper getDomainSpaceCert(ObjectIdentity ownerOid) { + return bcdnsRepository.getDomainSpaceCert(ownerOid); + } + + @Override + public boolean validateCrossChainCertificate(AbstractCrossChainCertificate certificate, Map domainSpaceCertPath) { + try { + List sequentialCerts = domainSpaceCertPath.entrySet().stream().sorted( + Comparator.comparing(o -> StrUtil.reverse(o.getKey())) + ).map(Map.Entry::getValue) + .collect(Collectors.toList()); + + DomainSpaceCertWrapper bcdnsRootCert = bcdnsRepository.getDomainSpaceCert(CrossChainDomain.ROOT_DOMAIN_SPACE); + if (ObjectUtil.isNull(bcdnsRootCert)) { + throw new RuntimeException("none root domain space cert set in DB"); + } + sequentialCerts.set(0, bcdnsRootCert.getDomainSpaceCert()); + + if (sequentialCerts.size() > 1) { + verifyCertPath(sequentialCerts, 0); + saveDomainSpaceCerts(domainSpaceCertPath); + } + + return sequentialCerts.get(sequentialCerts.size() - 1) + .getCredentialSubjectInstance().verifyIssueProof( + certificate.getEncodedToSign(), + certificate.getProof() + ); + } catch (Exception e) { + log.error("failed to verify crosschain cert (type: {}, cert_id: {})", certificate.getType().name(), certificate.getId(), e); + return false; + } + } + + @Override + public void saveDomainSpaceCerts(Map domainSpaceCerts) { + for (Map.Entry entry : domainSpaceCerts.entrySet()) { + try { + if (bcdnsRepository.hasDomainSpaceCert(entry.getKey())) { + log.info("DomainSpace {} already exists", entry.getKey()); + continue; + } + bcdnsRepository.saveDomainSpaceCert(new DomainSpaceCertWrapper(entry.getValue())); + log.info("successful to save domain space cert for {}", entry.getKey()); + } catch (Exception e) { + log.error("failed to save domain space certs for space {} : ", entry.getKey(), e); + } + } + } + + private void verifyCertPath(List certPath, int currIndex) { + if (currIndex == certPath.size() - 2) { + if ( + !certPath.get(currIndex).getCredentialSubjectInstance().verifyIssueProof( + certPath.get(currIndex + 1).getEncodedToSign(), + certPath.get(currIndex + 1).getProof() + ) + ) { + throw new RuntimeException( + StrUtil.format( + "failed to verify {} cert with its parent {}", + CrossChainCertificateUtil.getCrossChainDomain(certPath.get(currIndex)), + CrossChainCertificateUtil.getCrossChainDomain(certPath.get(currIndex + 1)) + ) + ); + } + return; + } + + verifyCertPath(certPath, currIndex + 1); + } +} diff --git a/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/service/impl/EndorserServiceImpl.java b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/service/impl/EndorserServiceImpl.java new file mode 100755 index 00000000..9171ac34 --- /dev/null +++ b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/service/impl/EndorserServiceImpl.java @@ -0,0 +1,505 @@ +/* + * Copyright 2024 Ant Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alipay.antchain.bridge.ptc.committee.monitor.node.service.impl; + +import java.math.BigInteger; +import java.security.PrivateKey; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.ListUtil; +import cn.hutool.core.util.*; +import com.alipay.antchain.bridge.commons.bcdns.AbstractCrossChainCertificate; +import com.alipay.antchain.bridge.commons.bcdns.DomainNameCredentialSubject; +import com.alipay.antchain.bridge.commons.bcdns.PTCCredentialSubject; +import com.alipay.antchain.bridge.commons.core.am.AuthMessageFactory; +import com.alipay.antchain.bridge.commons.core.am.IAuthMessage; +import com.alipay.antchain.bridge.commons.core.base.*; +import com.alipay.antchain.bridge.commons.core.bta.BlockchainTrustAnchorV1; +import com.alipay.antchain.bridge.commons.core.bta.IBlockchainTrustAnchor; +import com.alipay.antchain.bridge.commons.core.monitor.IMonitorMessage; +import com.alipay.antchain.bridge.commons.core.monitor.MonitorMessageFactory; +import com.alipay.antchain.bridge.commons.core.ptc.*; +import com.alipay.antchain.bridge.commons.core.sdp.ISDPMessage; +import com.alipay.antchain.bridge.commons.core.sdp.SDPMessageFactory; +import com.alipay.antchain.bridge.commons.exception.IllegalCrossChainCertException; +import com.alipay.antchain.bridge.commons.utils.crypto.HashAlgoEnum; +import com.alipay.antchain.bridge.commons.utils.crypto.SignAlgoEnum; +import com.alipay.antchain.bridge.plugins.spi.ptc.core.VerifyResult; +import com.alipay.antchain.bridge.pluginserver.service.CallBBCRequest; +import com.alipay.antchain.bridge.pluginserver.service.CrossChainServiceGrpc; +import com.alipay.antchain.bridge.pluginserver.service.Response; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.client.CrossChainServiceGrpcClientManager; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.client.MonitorSystemGrpcClientManager; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.commons.exception.*; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.commons.models.BtaWrapper; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.commons.models.TpBtaWrapper; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.commons.models.ValidatedConsensusStateWrapper; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.dal.repository.interfaces.IBCDNSRepository; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.dal.repository.interfaces.IEndorseServiceRepository; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.dal.repository.interfaces.ISystemConfigRepository; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.service.IEndorserService; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.service.IHcdvsPluginService; +import com.alipay.antchain.bridge.ptc.committee.monitor.system.grpc.*; +import com.alipay.antchain.bridge.ptc.committee.types.basic.CommitteeEndorseProof; +import com.alipay.antchain.bridge.ptc.committee.types.basic.CommitteeNodeProof; +import com.alipay.antchain.bridge.ptc.committee.types.basic.EndorseBlockStateResp; +import com.alipay.antchain.bridge.ptc.committee.types.tpbta.CommitteeEndorseRoot; +import com.alipay.antchain.bridge.ptc.committee.types.tpbta.VerifyBtaExtension; +import com.google.protobuf.ByteString; +import jakarta.annotation.Resource; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +@Service +@Slf4j +public class EndorserServiceImpl implements IEndorserService { + + @Resource + private IHcdvsPluginService hcdvsPluginService; + + @Resource + private IEndorseServiceRepository endorseServiceRepository; + + @Resource + private ISystemConfigRepository systemConfigRepository; + + @Resource + private IBCDNSRepository bcdnsRepository; + + @Resource + private PrivateKey nodeKey; + + @Resource + private AbstractCrossChainCertificate ptcCrossChainCert; + + @Resource + private MonitorSystemGrpcClientManager monitorSystemGrpcClientManager; + + @Resource + private CrossChainServiceGrpcClientManager crossChainServiceGrpcClientManager; + + @Value("${committee.node.endorse.ucp_hash_algo:KECCAK_256}") + private HashAlgoEnum ucpHashAlgo; + + @Value("${committee.node.endorse.sign_algo:KECCAK256_WITH_SECP256K1}") + private SignAlgoEnum nodeSignAlgo; + + @Value("${committee.node.id}") + private String committeeNodeId; + + @Value("${committee.id}") + private String committeeId; + + @Override + public TpBtaWrapper queryMatchedTpBta(CrossChainLane lane) { + return endorseServiceRepository.getMatchedTpBta(lane); + } + + @Override + public TpBtaWrapper verifyBta(AbstractCrossChainCertificate domainCert, IBlockchainTrustAnchor bta) throws InvalidBtaException { + log.info("verify BTA for domain {} now", bta.getDomain().getDomain()); + var credentialSubject = (DomainNameCredentialSubject) domainCert.getCredentialSubjectInstance(); + + var domainSpaceCertWrapper = bcdnsRepository.getDomainSpaceCert(credentialSubject.getParentDomainSpace().getDomain()); + if (ObjectUtil.isNull(domainSpaceCertWrapper)) { + throw new InvalidBtaException("domain space cert for {} not found", credentialSubject.getParentDomainSpace().getDomain()); + } + if (!ArrayUtil.equals(domainSpaceCertWrapper.getOwnerOid().getRawId(), domainCert.getIssuer().getRawId())) { + throw new InvalidBtaException("illegal domain space cert issuer for {}", credentialSubject.getParentDomainSpace().getDomain()); + } + + try { + domainCert.validate(domainSpaceCertWrapper.getDomainSpaceCert().getCredentialSubjectInstance()); + } catch (IllegalCrossChainCertException e) { + throw new InvalidBtaException("domain cert validation failed: {}", e.getMessage()); + } + + if (!bta.getDomain().equals(credentialSubject.getDomainName())) { + throw new InvalidBtaException("domain name mismatch"); + } + if (!ArrayUtil.equals(credentialSubject.getSubjectPublicKey().getEncoded(), bta.getBcOwnerPublicKeyObj().getEncoded())) { + throw new InvalidBtaException("owner public key mismatch"); + } + if (ObjectUtil.isEmpty(bta.getAmId())) { + throw new InvalidBtaException("am id is empty"); + } + if (!bta.validate()) { + throw new InvalidBtaException("bta sig verification failed"); + } + + var verifyBtaExtension = VerifyBtaExtension.decode(bta.getExtension()); + if (ObjectUtil.isNull(verifyBtaExtension)) { + throw new InvalidBtaException("extension decode failed"); + } + if (!verifyBtaExtension.getCrossChainLane().isValidated()) { + throw new InvalidBtaException("cross chain lane is invalid"); + } + if (!checkIfTpBTAIntersection(verifyBtaExtension.getCrossChainLane())) { + throw new InvalidBtaException("tpbta intersection check failed"); + } + + var latestTpBta = endorseServiceRepository.getExactTpBta(verifyBtaExtension.getCrossChainLane()); + var tpbta = new ThirdPartyBlockchainTrustAnchorV1( + ObjectUtil.isNull(latestTpBta) ? 1 : latestTpBta.getTpbta().getTpbtaVersion() + 1, + systemConfigRepository.queryCurrentPtcAnchorVersion(), + (PTCCredentialSubject) ptcCrossChainCert.getCredentialSubjectInstance(), + verifyBtaExtension.getCrossChainLane(), + bta.getSubjectVersion(), + ucpHashAlgo, + verifyBtaExtension.getCommitteeEndorseRoot().encode(), + new byte[]{} + ); + tpbta.setEndorseProof( + CommitteeEndorseProof.builder() + .committeeId(committeeId) + .sigs(ListUtil.toList( + CommitteeNodeProof.builder() + .nodeId(committeeNodeId) + .signAlgo(nodeSignAlgo) + .signature(nodeSignAlgo.getSigner().sign(nodeKey, tpbta.getEncodedToSign())) + .build() + )).build().encode() + ); + var tpBtaWrapper = new TpBtaWrapper(tpbta); + endorseServiceRepository.setBta(new BtaWrapper(bta)); + endorseServiceRepository.setTpBta(tpBtaWrapper); + + return tpBtaWrapper; + } + + private boolean checkIfTpBTAIntersection(CrossChainLane tpbtaLane) { + var wrapper = endorseServiceRepository.getMatchedTpBta(tpbtaLane); + if (ObjectUtil.isEmpty(wrapper) || wrapper.getCrossChainLane().equals(tpbtaLane)) { + return true; + } + return wrapper.getTpbta().type().ordinal() > ThirdPartyBlockchainTrustAnchor.TypeEnum.parseFrom(tpbtaLane).ordinal(); + } + + @Override + public ValidatedConsensusState commitAnchorState(CrossChainLane crossChainLane, ConsensusState anchorState) { + var tpbta = endorseServiceRepository.getMatchedTpBta(crossChainLane); + if (ObjectUtil.isNull(tpbta)) { + throw new InvalidConsensusStateException("tpbta not found for {}", crossChainLane.getLaneKey()); + } + var bta = endorseServiceRepository.getBta(crossChainLane.getSenderDomain().getDomain(), tpbta.getTpbta().getBtaSubjectVersion()); + if (ObjectUtil.isNull(bta)) { + throw new InvalidConsensusStateException("bta not found for {}", crossChainLane.getSenderDomain().getDomain()); + } + + var hcdvs = hcdvsPluginService.getHCDVSService(bta.getProduct()); + if (ObjectUtil.isNull(hcdvs)) { + throw new CommitteeNodeInternalException("hcdvs not found for {}", bta.getProduct()); + } + + if (!bta.getBta().getInitHeight().equals(anchorState.getHeight())) { + throw new InvalidConsensusStateException("invalid height: bta's is {} and yours {}", + bta.getBta().getInitHeight().toString(), anchorState.getHeight().toString()); + } + if (!ArrayUtil.equals(bta.getBta().getInitBlockHash(), anchorState.getHash())) { + throw new InvalidConsensusStateException("invalid block hash: bta's is {} and yours {}", + HexUtil.encodeHexStr(bta.getBta().getInitBlockHash()), anchorState.getHashHex()); + } + + return processValidatedConsensusState(anchorState, tpbta, hcdvs.verifyAnchorConsensusState(bta.getBta(), anchorState)); + } + + @Override + public ValidatedConsensusState commitConsensusState(CrossChainLane crossChainLane, ConsensusState currState) { + var tpbta = endorseServiceRepository.getMatchedTpBta(crossChainLane); + if (ObjectUtil.isNull(tpbta)) { + throw new InvalidConsensusStateException("tpbta not found for {}", crossChainLane.getLaneKey()); + } + var bta = endorseServiceRepository.getBta(crossChainLane.getSenderDomain().getDomain(), tpbta.getTpbta().getBtaSubjectVersion()); + if (ObjectUtil.isNull(bta)) { + throw new InvalidConsensusStateException("bta not found for {}", crossChainLane.getSenderDomain().getDomain()); + } + var parentConsensusState = endorseServiceRepository.getValidatedConsensusState( + currState.getDomain().getDomain(), + currState.getHeight().subtract(BigInteger.ONE) + ); + if (ObjectUtil.isNull(parentConsensusState)) { + throw new InvalidConsensusStateException("parent consensus state not found for {}", currState.getParentHashHex()); + } + + var hcdvs = hcdvsPluginService.getHCDVSService(bta.getProduct()); + if (ObjectUtil.isNull(hcdvs)) { + throw new CommitteeNodeInternalException("hcdvs not found for {}", bta.getProduct()); + } + + return processValidatedConsensusState(currState, tpbta, hcdvs.verifyConsensusState(currState, parentConsensusState.getValidatedConsensusState())); + } + + @Override + public CommitteeNodeProof verifyUcp(CrossChainLane crossChainLane, UniformCrosschainPacket ucp) { + var tpbta = endorseServiceRepository.getExactTpBta(crossChainLane); + if (ObjectUtil.isNull(tpbta)) { + throw new InvalidCrossChainMessageException("tpbta not found for {}", crossChainLane.getLaneKey()); + } + var bta = endorseServiceRepository.getBta(crossChainLane.getSenderDomain().getDomain(), tpbta.getTpbta().getBtaSubjectVersion()); + if (ObjectUtil.isNull(bta)) { + throw new InvalidCrossChainMessageException("bta not found for {}", crossChainLane.getSenderDomain().getDomain()); + } + var consensusState = endorseServiceRepository.getValidatedConsensusState( + ucp.getSrcDomain().getDomain(), + ucp.getSrcMessage().getProvableData().getBlockHashHex() + ); + if (ObjectUtil.isNull(consensusState)) { + throw new InvalidCrossChainMessageException("consensus state not found for {}", ucp.getSrcMessage().getProvableData().getBlockHashHex()); + } + if (!ArrayUtil.equals(consensusState.getValidatedConsensusState().getHash(), ucp.getSrcMessage().getProvableData().getBlockHash())) { + throw new InvalidCrossChainMessageException("expected block hash {} but get {}", + consensusState.getValidatedConsensusState().getHash(), ucp.getSrcMessage().getProvableData().getBlockHash()); + } + if (!ObjectUtil.equals(consensusState.getValidatedConsensusState().getHeight(), ucp.getSrcMessage().getProvableData().getHeightVal())) { + throw new InvalidCrossChainMessageException("expected block height {} but get {}", + consensusState.getValidatedConsensusState().getHeight(), ucp.getSrcMessage().getProvableData().getHeightVal()); + } + + var hcdvs = hcdvsPluginService.getHCDVSService(bta.getProduct()); + if (ObjectUtil.isNull(hcdvs)) { + throw new CommitteeNodeInternalException("hcdvs not found for {}", bta.getProduct()); + } + if (!ArrayUtil.equals( + ucp.getSrcMessage().getMessage(), + hcdvs.parseMessageFromLedgerData(ucp.getSrcMessage().getProvableData().getLedgerData()) + )) { + throw new InvalidCrossChainMessageException("message decoded from ledger data not equal to message inside UCP"); + } + + var verifyResult = hcdvs.verifyCrossChainMessage(ucp.getSrcMessage(), consensusState.getValidatedConsensusState()); + if (ObjectUtil.isNull(verifyResult) || !verifyResult.isSuccess()) { + throw new InvalidCrossChainMessageException("cross chain message verification failed: {}", verifyResult.getErrorMsg()); + } + + return CommitteeNodeProof.builder() + .nodeId(committeeNodeId) + .signAlgo(nodeSignAlgo) + .signature(nodeSignAlgo.getSigner().sign( + nodeKey, + ThirdPartyProof.create( + tpbta.getTpbta().getTpbtaVersion(), + ucp.getSrcMessage().getMessage(), + crossChainLane + ).getEncodedToSign() + )).build(); + } + + @Override + public CommitteeNodeProof verifyUcpWithMonitorSystem(CrossChainLane crossChainLane, UniformCrosschainPacket ucp) { + + var tpbta = endorseServiceRepository.getExactTpBta(crossChainLane); + if (ObjectUtil.isNull(tpbta)) { + throw new InvalidCrossChainMessageException("tpbta not found for {}", crossChainLane.getLaneKey()); + } + var bta = endorseServiceRepository.getBta(crossChainLane.getSenderDomain().getDomain(), tpbta.getTpbta().getBtaSubjectVersion()); + if (ObjectUtil.isNull(bta)) { + throw new InvalidCrossChainMessageException("bta not found for {}", crossChainLane.getSenderDomain().getDomain()); + } + + log.info("verify ucp with monitor system for domain {} now", bta.getDomain()); + // log.info("crosslane[senderDomain-recvDomain-senderID-recvID]: {}, {}, {}, {}", + // crossChainLane.getSenderDomain().getDomain(), crossChainLane.getReceiverDomain().getDomain(), + // crossChainLane.getSenderIdHex(), crossChainLane.getReceiverIdHex()); +// log.info("verify ucp with monitor system for domain {} now", crossChainLane.getSenderDomain().getDomain()); + + MonitorSystemServiceGrpc.MonitorSystemServiceBlockingStub monitorSystemServiceBlockingStub = monitorSystemGrpcClientManager.getStub("monitor-system"); + + MonitorSystemResponse responseFromMonitorSystem = monitorSystemServiceBlockingStub.verifyCrossChainMessageInMonitorSystem( + VerifyCrossChainMessageInMonitorSystemRequest.newBuilder() + .setRawUcp(ByteString.copyFrom(ucp.encode())) + .build() + ); + + if (ObjectUtil.isNull(responseFromMonitorSystem)) { + throw new RuntimeException("null response from monitor system"); + } + if (responseFromMonitorSystem.getCode() != 0) { + throw new RuntimeException(String.format("[MonitorSystemGRpcClient] verifyCrossChainMessageInMonitorSystem request failed for plugin server: %s", + responseFromMonitorSystem.getErrorMsg())); + } + if (responseFromMonitorSystem.getVerifyCrossChainMessageInMonitorSystemResp().getResult() == 0) { + // 监管通过 流程正常 +// log.info("verify ucp with monitor system for domain {}: success", bta.getDomain()); + log.info("verify ucp with monitor system for domain {}: success", crossChainLane.getSenderDomain().getDomain()); + return CommitteeNodeProof.builder() + .nodeId(committeeNodeId) + .signAlgo(nodeSignAlgo) + .signature(nodeSignAlgo.getSigner().sign( + nodeKey, + ThirdPartyProof.create( + tpbta.getTpbta().getTpbtaVersion(), + ucp.getSrcMessage().getMessage(), + crossChainLane + ).getEncodedToSign() + )).build(); + } else { + // [监管回滚的v1版本逻辑] + // 监管未通过 直接向监管合约发送回滚交易 并且不跑出异常 而是返回一个签名 + // 目前是返回一个正确的签名, 保证在监管不通过时系统的稳定运行; 在8~9月开发的最终版本中会返回一个空签名, 实现完整的逻辑 + log.info("verify ucp with monitor system for domain {}: failure", bta.getDomain()); + // log.info("verify ucp with monitor system for domain {}: failure", crossChainLane.getSenderDomain().getDomain()); + // IAuthMessage authMessage = AuthMessageFactory.createAuthMessage(ucp.getSrcMessage().getMessage()); + // ISDPMessage sdpMessage = SDPMessageFactory.createSDPMessage(authMessage.getPayload()); + // byte[] monitorMessage = sdpMessage.getPayload(); + + // CrossChainServiceGrpc.CrossChainServiceBlockingStub crossChainServiceBlockingStub = crossChainServiceGrpcClientManager.getStub("plugin-server"); + // Response responseFromPS = crossChainServiceBlockingStub.bbcCall( + // CallBBCRequest.newBuilder() + // .setProduct(bta.getProduct()) + // .setDomain(bta.getDomain()) + // .setRelayMonitorRollbackMessageReq( + // RelayMonitorRollbackMessageRequest.newBuilder() + // .setReceiverDomain(ucp.getSrcDomain().getDomain()) + // .setToAddress(ucp.getCrossChainLane().getSenderIdHex()) + // // .setToAddress(authMessage.getIdentity().toHex()) + // .setRawMessage(ByteString.copyFrom(monitorMessage)) + // ).build() + // ); + + // if (ObjectUtil.isNull(responseFromPS)) { + // throw new RuntimeException("null response from plugin server"); + // } + // if (responseFromPS.getCode() != 0) { + // throw new RuntimeException( + // String.format("[GRpcBBCServiceClient (domain: %s, product: %s)] relayMonitorRollbackMessage request failed for plugin server: %s", + // bta.getDomain(), bta.getProduct(), responseFromPS.getErrorMsg()) + // ); + // } + + // CrossChainMessageReceipt crossChainMessageReceipt = convertFromGRpcCrossChainMessageReceipt(responseFromPS.getBbcResp().getRelayMonitorRollbackMessageResp().getReceipt()); + + // 如果回滚消息未成功上链 目前仅抛出异常 + // if (!crossChainMessageReceipt.isSuccessful()) { + // throw new RuntimeException(StrUtil.format("failed to commit monitor rollback message: ( error_msg: {})", + // crossChainMessageReceipt.getErrorMsg())); + // } + + // [监管回滚的v2版本逻辑] + // 返回一个ethereum格式(65字节)的空签名 由目的链的监管合约验证签名时识别为监管失败 构造监管回滚消息 + return CommitteeNodeProof.builder() + .nodeId(committeeNodeId) + .signAlgo(nodeSignAlgo) + .signature(new byte[65]).build(); + + // throw new InvalidCrossChainMessageException("[monitor system] illegal crosschain message(block hash: {}): {}", + // ucp.getSrcMessage().getProvableData().getBlockHashHex(), responseFromMonitorSystem.getVerifyCrossChainMessageInMonitorSystemResp().getMsg()); + } + } + + @Override + public void relayUcpToMonitorSystem(UniformCrosschainPacket ucp) { + MonitorSystemServiceGrpc.MonitorSystemServiceBlockingStub monitorSystemServiceBlockingStub = monitorSystemGrpcClientManager.getStub("monitor-system"); + + MonitorSystemResponse responseFromMonitorSystem = monitorSystemServiceBlockingStub.relayUcpToMonitorSystem( + RelayUcpToMonitorSystemRequest.newBuilder() + .setRawUcp(ByteString.copyFrom(ucp.encode())) + .build() + ); + + if (ObjectUtil.isNull(responseFromMonitorSystem)) { + throw new RuntimeException("null response from monitor system"); + } + if (responseFromMonitorSystem.getCode() != 0) { + throw new RuntimeException(String.format("[MonitorSystemGRpcClient] relayUcpToMonitorSystem request failed: %s", + responseFromMonitorSystem.getErrorMsg())); + } + } + + @Override + public EndorseBlockStateResp endorseBlockState(CrossChainLane crossChainLane, String receiverDomain, BigInteger height) { + var tpbta = endorseServiceRepository.getExactTpBta(crossChainLane); + if (ObjectUtil.isNull(tpbta)) { + throw new InvalidCrossChainMessageException("tpbta not found for {}", crossChainLane.getLaneKey()); + } + + if (!endorseServiceRepository.hasValidatedConsensusState(crossChainLane.getSenderDomain().toString(), height)) { + throw new BlockStateNotValidatedYetException("no block validated for height {}", height.toString()); + } + + var vcs = endorseServiceRepository.getValidatedConsensusState(crossChainLane.getSenderDomain().toString(), height); + IAuthMessage am = AuthMessageFactory.createAuthMessage( + 1, + CrossChainIdentity.ZERO_ID.getRawID(), + 0, + SDPMessageFactory.createValidatedBlockStateSDPMsg( + new CrossChainDomain(receiverDomain), + new BlockState( + crossChainLane.getSenderDomain(), + vcs.getValidatedConsensusState().getHash(), + vcs.getHeight(), + vcs.getValidatedConsensusState().getStateTimestamp() + ) + ).encode() + ); + return new EndorseBlockStateResp( + am, + CommitteeNodeProof.builder() + .nodeId(committeeNodeId) + .signAlgo(nodeSignAlgo) + .signature(nodeSignAlgo.getSigner().sign( + nodeKey, + ThirdPartyProof.create( + tpbta.getTpbta().getTpbtaVersion(), + am.encode(), + crossChainLane + ).getEncodedToSign() + )).build() + ); + } + + @NonNull + private ValidatedConsensusState processValidatedConsensusState(ConsensusState currState, TpBtaWrapper tpbta, VerifyResult verifyResult) { + if (ObjectUtil.isNull(verifyResult) || !verifyResult.isSuccess()) { + throw new InvalidConsensusStateException("consensus state verification failed: {}", verifyResult.getErrorMsg()); + } + + var vcs = BeanUtil.copyProperties(currState, ValidatedConsensusStateV1.class); + vcs.setPtcOid(ptcCrossChainCert.getCredentialSubjectInstance().getApplicant()); + vcs.setTpbtaVersion(tpbta.getTpbta().getTpbtaVersion()); + vcs.setPtcType(PTCTypeEnum.COMMITTEE); + + if (!endorseServiceRepository.hasValidatedConsensusState(currState.getDomain().getDomain(), currState.getHeight())) { + endorseServiceRepository.setValidatedConsensusState(new ValidatedConsensusStateWrapper(vcs)); + } + + var nodeProof = CommitteeNodeProof.builder() + .nodeId(committeeNodeId) + .signAlgo(nodeSignAlgo) + .signature(nodeSignAlgo.getSigner().sign(nodeKey, vcs.getEncodedToSign())) + .build(); + var proof = new CommitteeEndorseProof(); + proof.setCommitteeId(committeeId); + proof.setSigs(ListUtil.toList(nodeProof)); + vcs.setPtcProof(proof.encode()); + + return vcs; + } + + private static CrossChainMessageReceipt convertFromGRpcCrossChainMessageReceipt(com.alipay.antchain.bridge.pluginserver.service.CrossChainMessageReceipt crossChainMessageReceipt) { + CrossChainMessageReceipt receipt = new CrossChainMessageReceipt(); + receipt.setConfirmed(crossChainMessageReceipt.getConfirmed()); + receipt.setSuccessful(crossChainMessageReceipt.getSuccessful()); + receipt.setTxhash(crossChainMessageReceipt.getTxhash()); + receipt.setErrorMsg(crossChainMessageReceipt.getErrorMsg()); + receipt.setTxTimestamp(crossChainMessageReceipt.getTxTimestamp()); + receipt.setRawTx(crossChainMessageReceipt.getRawTx().toByteArray()); + + return receipt; + } +} diff --git a/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/service/impl/HcdvsPluginServiceImpl.java b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/service/impl/HcdvsPluginServiceImpl.java new file mode 100755 index 00000000..3043295b --- /dev/null +++ b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/service/impl/HcdvsPluginServiceImpl.java @@ -0,0 +1,227 @@ +/* + * Copyright 2024 Ant Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alipay.antchain.bridge.ptc.committee.monitor.node.service.impl; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.*; + +import ch.qos.logback.classic.AsyncAppender; +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.LoggerContext; +import ch.qos.logback.classic.encoder.PatternLayoutEncoder; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.rolling.RollingFileAppender; +import ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy; +import ch.qos.logback.core.util.FileSize; +import cn.hutool.core.collection.ListUtil; +import cn.hutool.core.util.ObjectUtil; +import com.alipay.antchain.bridge.plugins.manager.AntChainBridgePluginManagerFactory; +import com.alipay.antchain.bridge.plugins.manager.core.IAntChainBridgePlugin; +import com.alipay.antchain.bridge.plugins.manager.core.IAntChainBridgePluginManager; +import com.alipay.antchain.bridge.plugins.spi.ptc.IHeteroChainDataVerifierService; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.service.IHcdvsPluginService; +import lombok.extern.slf4j.Slf4j; +import org.pf4j.ClassLoadingStrategy; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +public class HcdvsPluginServiceImpl implements IHcdvsPluginService { + + private final IAntChainBridgePluginManager manager; + + private final String hcdvsLoggerDir; + + @Value("${committee.plugin.log.hcdvs.max_history:3}") + private int maxHCDVSLogHistory; + + @Value("${committee.plugin.log.hcdvs.max_file_size:30mb}") + private String maxHCDVSLogFileSize; + + @Value("${committee.plugin.log.hcdvs.level:info}") + private String hcdvsLogLevel; + + @Value("${committee.plugin.log.hcdvs.on:true}") + private boolean isHCDVSLogOn; + + private final Map hcdvsLoggerMap = new HashMap<>(); + + public HcdvsPluginServiceImpl( + @Value("${committee.plugin.repo}") String path, + @Value("${logging.file.path}") String hcdvsLogDir, + @Value("${committee.plugin.policy.classloader.resource.ban-with-prefix.APPLICATION:}") String[] resourceBannedPrefixOnAppLevel + ) { + log.info("plugins path: {}", Paths.get(path).toAbsolutePath()); + this.hcdvsLoggerDir = Paths.get(hcdvsLogDir, "hcdvs").toAbsolutePath().toString(); + log.info("hcdvs logger base dir: {}", Paths.get(hcdvsLoggerDir).toAbsolutePath()); + + this.manager = AntChainBridgePluginManagerFactory.createPluginManager( + path, + ObjectUtil.defaultIfNull(convertPathPrefixBannedMap(resourceBannedPrefixOnAppLevel), new HashMap<>()) + ); + loadPlugins(); + startPlugins(); + } + + private Map> convertPathPrefixBannedMap( + String[] resourceBannedPrefixOnAppLevel + ) { + Map> result = new HashMap<>(); + + Set appSet = new HashSet<>(ListUtil.of(resourceBannedPrefixOnAppLevel)); + result.put(ClassLoadingStrategy.Source.APPLICATION, appSet); + + return result; + } + + @Override + public void loadPlugins() { + manager.loadPlugins(); + } + + @Override + public void startPlugins() { + manager.startPlugins(); + } + + @Override + public void loadPlugin(String path) { + manager.loadPlugin(Paths.get(path)); + } + + @Override + public void startPlugin(String path) { + manager.startPlugin(Paths.get(path)); + } + + @Override + public void stopPlugin(String product) { + manager.stopPlugin(product); + } + + @Override + public void startPluginFromStop(String product) { + manager.startPluginFromStop(product); + } + + @Override + public void reloadPlugin(String product) { + manager.reloadPlugin(product); + } + + @Override + public void reloadPlugin(String product, String path) { + manager.reloadPlugin(product, Paths.get(path)); + } + + @Override + public IAntChainBridgePlugin getPlugin(String product) { + return manager.getPlugin(product); + } + + @Override + public boolean hasPlugin(String product) { + return manager.hasPlugin(product); + } + + @Override + public IHeteroChainDataVerifierService createHCDVSService(String product) { + return manager.createHCDVSService(product, createHCDVSServiceLogger(product)); + } + + @Override + public IHeteroChainDataVerifierService getHCDVSService(String product) { + var service = manager.getHCDVSService(product); + if (ObjectUtil.isNotNull(service)) { + return service; + } + return createHCDVSService(product); + } + + private Logger createHCDVSServiceLogger(String product) { + if (!isHCDVSLogOn) { + return null; + } + String loggerName = product; + if (hcdvsLoggerMap.containsKey(loggerName) && ObjectUtil.isNotNull(hcdvsLoggerMap.get(loggerName))) { + return hcdvsLoggerMap.get(loggerName); + } + Path logFile = Paths.get(hcdvsLoggerDir, product + ".log"); + Logger logger = LoggerFactory.getLogger(loggerName); + if (logger instanceof ch.qos.logback.classic.Logger) { + log.debug("using logback for hcdvs logger"); + + LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory(); + + PatternLayoutEncoder encoder = new PatternLayoutEncoder(); + encoder.setContext(context); + encoder.setPattern("%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n"); + encoder.setCharset(StandardCharsets.UTF_8); + encoder.start(); + + SizeAndTimeBasedRollingPolicy rollingPolicy = new SizeAndTimeBasedRollingPolicy<>(); + rollingPolicy.setContext(context); + rollingPolicy.setFileNamePattern(logFile + ".%d{yyyy-MM-dd}.%i"); + rollingPolicy.setMaxHistory(maxHCDVSLogHistory); + rollingPolicy.setMaxFileSize(FileSize.valueOf(maxHCDVSLogFileSize)); + + RollingFileAppender appender = new RollingFileAppender<>(); + appender.setContext(context); + appender.setEncoder(encoder); + appender.setFile(logFile.toString()); + appender.setRollingPolicy(rollingPolicy); + + rollingPolicy.setParent(appender); + rollingPolicy.start(); + appender.start(); + + AsyncAppender asyncAppender = new AsyncAppender(); + asyncAppender.setContext(context); + asyncAppender.setName(loggerName); + asyncAppender.addAppender(appender); + asyncAppender.start(); + + ch.qos.logback.classic.Logger loggerLogback = (ch.qos.logback.classic.Logger) logger; + loggerLogback.setLevel(Level.toLevel(hcdvsLogLevel)); + loggerLogback.setAdditive(false); + loggerLogback.addAppender(asyncAppender); + + hcdvsLoggerMap.put(loggerName, loggerLogback); + log.info("hcdvs logger {} created", loggerName); + + return loggerLogback; + } + + log.debug("logger library not support for now"); + return null; + } + + @Override + public boolean hasProduct(String product) { + return manager.hasProduct(product); + } + + @Override + public List getAvailableProducts() { + return manager.allSupportProducts(); + } +} diff --git a/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/service/impl/MonitorServiceImpl.java b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/service/impl/MonitorServiceImpl.java new file mode 100755 index 00000000..5c259a6a --- /dev/null +++ b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/service/impl/MonitorServiceImpl.java @@ -0,0 +1,110 @@ +package com.alipay.antchain.bridge.ptc.committee.monitor.node.service.impl; + +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import com.alipay.antchain.bridge.commons.core.base.CrossChainMessageReceipt; +import com.alipay.antchain.bridge.commons.core.monitor.MonitorOrderV1; +import com.alipay.antchain.bridge.pluginserver.service.*; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.client.CrossChainServiceGrpcClientManager; +import com.alipay.antchain.bridge.ptc.committee.monitor.system.grpc.*; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.service.IMonitorService; +import com.alipay.antchain.bridge.commons.utils.crypto.SignAlgoEnum; +import com.alipay.antchain.bridge.ptc.committee.types.basic.CommitteeNodeProof; +import com.google.protobuf.ByteString; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import net.devh.boot.grpc.client.inject.GrpcClient; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.slf4j.Logger; +import org.slf4j.helpers.NOPLogger; + +import java.security.PrivateKey; +import java.util.Map; +import java.util.concurrent.*; + +@Service +@Slf4j +public class MonitorServiceImpl implements IMonitorService { + + @Value("${committee.id}") + private String committeeId; + + //Keccak256WithSecp256k1 + @Value("${committee.node.endorse.sign_algo:KECCAK256_WITH_SECP256K1}") + private SignAlgoEnum nodeSignAlgo; + + @Resource + private PrivateKey nodeKey; + + @Resource + private CrossChainServiceGrpcClientManager crossChainServiceGrpcClientManager; + + @Override + public void recvMonitorOrder(MonitorOrderV1 monitorOrder) { + + // sign + byte[] encodedMonitorOrder = monitorOrder.encode(); + byte[] signature = nodeSignAlgo.getSigner().sign(nodeKey, encodedMonitorOrder); + + CrossChainServiceGrpc.CrossChainServiceBlockingStub crossChainServiceBlockingStub = crossChainServiceGrpcClientManager.getStub("plugin-server"); + Response responseFromPS = crossChainServiceBlockingStub.bbcCall( + CallBBCRequest.newBuilder() + .setProduct(monitorOrder.getProduct()) + .setDomain(monitorOrder.getDomain()) + .setRelayMonitorOrderReq( + RelayMonitorOrderRequest.newBuilder() + .setCommitteeId(committeeId) + .setSignAlgo(nodeSignAlgo.getName()) + .setRawProof(ByteString.copyFrom(signature)) + .setRawMonitorOrder(ByteString.copyFrom(encodedMonitorOrder)) + ) + .build() + ); +// Response responseFromPS = crossChainServiceBlockingStub.bbcCall( +// CallBBCRequest.newBuilder() +// .setProduct(monitorOrder.getProduct()) +// .setDomain(monitorOrder.getDomain()) +// .setRelayMonitorOrderReq( +// RelayMonitorOrderRequest.newBuilder() +// .setMonitorOrderType(monitorOrder.getMonitorOrderType()) +// .setSenderDomain(monitorOrder.getSenderDomain()) +// .setFromAddress(monitorOrder.getFromAddress()) +// .setReceiverDomain(monitorOrder.getReceiveDomain()) +// .setToAddress(monitorOrder.getToAddress()) +// .setTransactionContent(monitorOrder.getTransactionContent()) +// .setExtra(monitorOrder.getExtra()) +// ).build() +// ); + + if (ObjectUtil.isNull(responseFromPS)) { + throw new RuntimeException("null response from plugin server"); + } + if (responseFromPS.getCode() != 0) { + throw new RuntimeException( + String.format("[GRpcBBCServiceClient (domain: %s, product: %s)] relayMonitorOrder request failed for plugin server: %s", + monitorOrder.getDomain(), monitorOrder.getProduct(), responseFromPS.getErrorMsg()) + ); + } + + CrossChainMessageReceipt crossChainMessageReceipt = convertFromGRpcCrossChainMessageReceipt(responseFromPS.getBbcResp().getRelayMonitorOrderResp().getReceipt()); + + // 如果监管指令消息未成功上链 目前仅抛出异常 + if (!crossChainMessageReceipt.isSuccessful()) { + throw new RuntimeException(StrUtil.format("failed to commit monitor order: (error_msg: {})", + crossChainMessageReceipt.getErrorMsg())); + } + } + + private static CrossChainMessageReceipt convertFromGRpcCrossChainMessageReceipt(com.alipay.antchain.bridge.pluginserver.service.CrossChainMessageReceipt crossChainMessageReceipt) { + CrossChainMessageReceipt receipt = new CrossChainMessageReceipt(); + receipt.setConfirmed(crossChainMessageReceipt.getConfirmed()); + receipt.setSuccessful(crossChainMessageReceipt.getSuccessful()); + receipt.setTxhash(crossChainMessageReceipt.getTxhash()); + receipt.setErrorMsg(crossChainMessageReceipt.getErrorMsg()); + receipt.setTxTimestamp(crossChainMessageReceipt.getTxTimestamp()); + receipt.setRawTx(crossChainMessageReceipt.getRawTx().toByteArray()); + + return receipt; + } +} diff --git a/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/service/impl/ScheduledTaskServiceImpl.java b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/service/impl/ScheduledTaskServiceImpl.java new file mode 100755 index 00000000..80120dc0 --- /dev/null +++ b/acb-committeeptc/monitor-node/src/main/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/service/impl/ScheduledTaskServiceImpl.java @@ -0,0 +1,81 @@ +/* + * Copyright 2024 Ant Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alipay.antchain.bridge.ptc.committee.monitor.node.service.impl; + +import java.math.BigInteger; + +import cn.hutool.core.util.HexUtil; +import cn.hutool.core.util.ObjectUtil; +import com.alipay.antchain.bridge.commons.bcdns.AbstractCrossChainCertificate; +import com.alipay.antchain.bridge.commons.core.ptc.PTCTrustRoot; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.commons.exception.CommitteeNodeInternalException; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.commons.models.DomainSpaceCertWrapper; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.dal.repository.interfaces.ISystemConfigRepository; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.service.IBCDNSManageService; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.service.IScheduledTaskService; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +@Component +@Slf4j +public class ScheduledTaskServiceImpl implements IScheduledTaskService { + + @Resource + private IBCDNSManageService bcdnsManageService; + + @Resource + private AbstractCrossChainCertificate ptcCrossChainCert; + + @Resource + private ISystemConfigRepository systemConfigRepository; + + @Override + @Scheduled(fixedDelayString = "${committee.node.schedule.ptc-trust-root-listen.fixed-delay:60000}") + public void listenPtcTrustRoot() { + try { + if (bcdnsManageService.countBCDNSService() == 0) { + log.info("have no bcdns service, please add one at least"); + return; + } + + DomainSpaceCertWrapper domainSpaceCertWrapper = bcdnsManageService.getDomainSpaceCert(ptcCrossChainCert.getIssuer()); + if (ObjectUtil.isNull(domainSpaceCertWrapper)) { + throw new CommitteeNodeInternalException( + "No domain space cert found for issuer {}", HexUtil.encodeHexStr(ptcCrossChainCert.getIssuer().encode()) + ); + } + PTCTrustRoot ptcTrustRoot = bcdnsManageService.getBCDNSClient(domainSpaceCertWrapper.getDomainSpace()) + .queryPTCTrustRoot(ptcCrossChainCert.getCredentialSubjectInstance().getApplicant()); + if (ObjectUtil.isNull(ptcTrustRoot)) { + throw new CommitteeNodeInternalException("No ptc trust root found"); + } + + BigInteger currVer = systemConfigRepository.queryCurrentPtcAnchorVersion(); + BigInteger verOnBcdns = ptcTrustRoot.getVerifyAnchorMap().keySet().stream().max(BigInteger::compareTo).orElse(BigInteger.ZERO); + if (currVer.compareTo(verOnBcdns) >= 0) { + log.debug("No new ptc trust root found"); + return; + } + log.info("New ptc trust root found, new verify anchors from version {} to {}", currVer, verOnBcdns); + systemConfigRepository.setPtcTrustRoot(ptcTrustRoot); + } catch (Throwable t) { + log.error("Failed to listen ptc trust root", t); + } + } +} diff --git a/acb-committeeptc/monitor-node/src/main/proto/admingrpc.proto b/acb-committeeptc/monitor-node/src/main/proto/admingrpc.proto new file mode 100755 index 00000000..a0fc11eb --- /dev/null +++ b/acb-committeeptc/monitor-node/src/main/proto/admingrpc.proto @@ -0,0 +1,74 @@ +syntax = "proto3"; + +option java_multiple_files = true; +option java_package = "com.alipay.antchain.bridge.ptc.committee.monitor.node.server.grpc"; +option java_outer_classname = "AdminGrpcServerOuter"; +option objc_class_prefix = "AdminGrpcServer"; + +package acb.ptc.committee.node.admin; + +message Response { + uint32 code = 1; + string errorMsg = 2; + oneof response { + GetBcdnsServiceInfoResp getBcdnsServiceInfoResp = 3; + GetBcdnsCertificateResp getBcdnsCertificateResp = 4; + } +} + +message RegisterBcdnsServiceRequest { + string domainSpace = 1; + string bcdnsType = 2; + bytes config = 3; + optional string bcdnsRootCert = 4; +} + +message GetBcdnsServiceInfoRequest { + string domainSpace = 1; +} + +message GetBcdnsServiceInfoResp { + string infoJson = 1; +} + +message DeleteBcdnsServiceRequest { + string domainSpace = 1; +} + +message GetBcdnsCertificateRequest { + string domainSpace = 1; +} + +message GetBcdnsCertificateResp { + string certificate = 1; +} + +message StopBcdnsServiceRequest { + string domainSpace = 1; +} + +message RestartBcdnsServiceRequest { + string domainSpace = 1; +} + +message AddPtcTrustRootRequest { + bytes rawTrustRoot = 1; +} + +// The greeting service definition. +service AdminService { + + rpc registerBcdnsService(RegisterBcdnsServiceRequest) returns (Response) {} + + rpc getBcdnsServiceInfo(GetBcdnsServiceInfoRequest) returns (Response) {} + + rpc deleteBcdnsService(DeleteBcdnsServiceRequest) returns (Response) {} + + rpc getBcdnsCertificate(GetBcdnsCertificateRequest) returns (Response) {} + + rpc stopBcdnsService(StopBcdnsServiceRequest) returns (Response) {} + + rpc restartBcdnsService(RestartBcdnsServiceRequest) returns (Response) {} + + rpc addPtcTrustRoot(AddPtcTrustRootRequest) returns (Response) {} +} diff --git a/acb-committeeptc/monitor-node/src/main/proto/monitorSystemgrpc.proto b/acb-committeeptc/monitor-node/src/main/proto/monitorSystemgrpc.proto new file mode 100755 index 00000000..be14f86d --- /dev/null +++ b/acb-committeeptc/monitor-node/src/main/proto/monitorSystemgrpc.proto @@ -0,0 +1,71 @@ +syntax = "proto3"; + +option java_multiple_files = true; +option java_package = "com.alipay.antchain.bridge.ptc.committee.monitor.system.grpc"; +option java_outer_classname = "MonitorGrpcServerOuter"; +option objc_class_prefix = "MonitorGrpcServer"; + +package acb.ptc.committee.monitor.node; + +// 服务a: 监管节点 --> 监管系统 +service MonitorSystemService { + + rpc heartbeat(Empty) returns (MonitorSystemResponse) {} + + rpc verifyCrossChainMessageInMonitorSystem(VerifyCrossChainMessageInMonitorSystemRequest) returns (MonitorSystemResponse) {} + + rpc relayUcpToMonitorSystem(RelayUcpToMonitorSystemRequest) returns (MonitorSystemResponse) {} +} + +message Empty {} + +// 监管节点作为客户端向监管系统请求验证ucp的合法性(VerifyCrossChainMessage方法中调用) +message VerifyCrossChainMessageInMonitorSystemRequest { + bytes rawUcp = 1; +} + +// 接收无需监管的跨链消息,并转发给课题四监管系统供其分析(VerifyCrossChainMessage方法中调用) +message RelayUcpToMonitorSystemRequest { + bytes rawUcp = 1; +} + +message MonitorSystemResponse { + uint32 code = 1; + string errorMsg = 2; + oneof response { + VerifyCrossChainMessageInMonitorSystemResponse verifyCrossChainMessageInMonitorSystemResp = 3; + } +} + +message VerifyCrossChainMessageInMonitorSystemResponse { + uint32 result = 1; + string msg = 2; +} + +// 服务b: 监管系统 --> 监管节点 +service MonitorOrderService { + + rpc recvMonitorOrder(RecvMonitorOrderRequest) returns (RecvMonitorOrderResponse) {} +} + +// 接收课题四监管系统发送的监管指令 +message RecvMonitorOrderRequest { + MonitorOrder monitorOrder = 1; +} + +message RecvMonitorOrderResponse { + uint32 code = 1; + string errorMsg = 2; +} + +message MonitorOrder { + string product = 1; + string domain = 2; + uint64 monitorOrderType = 3; + string senderDomain = 4; + string fromAddress = 5; + string receiverDomain = 6; + string toAddress = 7; + string transactionContent = 8; + string extra = 9; +} diff --git a/acb-committeeptc/monitor-node/src/main/proto/pluginserver.proto b/acb-committeeptc/monitor-node/src/main/proto/pluginserver.proto new file mode 100755 index 00000000..a7f48eda --- /dev/null +++ b/acb-committeeptc/monitor-node/src/main/proto/pluginserver.proto @@ -0,0 +1,432 @@ +syntax = "proto3"; + +package com.alipay.antchain.bridge.pluginserver.service; + +option java_multiple_files = true; +option java_package = "com.alipay.antchain.bridge.pluginserver.service"; +option java_outer_classname = "PluginRpcServer"; + +// just empty +message Empty {} + +service CrossChainService { + // Relayer would call this interface to communicate with the `BBCService` object + rpc bbcCall(CallBBCRequest) returns (Response) {} + + // handle heartbeat requests from relayers + rpc heartbeat(Empty) returns (Response) {} + + // return if these blockchain products support or not + rpc ifProductSupport(IfProductSupportRequest) returns (Response) {} + + // return if these blockchain domains alive or not + rpc ifDomainAlive(IfDomainAliveRequest) returns (Response) {} +} + +// heartbeat response +message HeartbeatResponse { + repeated string products = 1; + repeated string domains = 2; +} + +message IfProductSupportRequest { + repeated string products = 1; +} + +message IfProductSupportResponse { + // key : which product + // value : support or not + map results = 1; +} + +message IfDomainAliveRequest { + repeated string domains = 1; +} + +message IfDomainAliveResponse { + // key : which domain + // value : alive or not + map results = 1; +} + +// wrapper for all responses +message Response { + uint32 code = 1; + string errorMsg = 2; + oneof response { + CallBBCResponse bbcResp = 3; + HeartbeatResponse heartbeatResp = 4; + IfProductSupportResponse ifProductSupportResp = 5; + IfDomainAliveResponse ifDomainAliveResp = 6; + } +} + +// messages for `bbcCall` requests +message CallBBCRequest { + // which kind of blockchain for plugin to load + string product = 1; + + // which domain of the blockchain for the `BBCService` to connect with + string domain = 2; + + // biz request for `BBCService` + // basically, evey interface of `BBCService` has a request message defined here. + oneof request { + StartUpRequest startUpReq = 3; + GetContextRequest getContextReq = 4; + ShutdownRequest shutdownReq = 5; + SetupAuthMessageContractRequest setupAuthMessageContractReq = 6; + SetupSDPMessageContractRequest setupSDPMessageContractReq = 7; + SetProtocolRequest setProtocolReq = 8; + RelayAuthMessageRequest relayAuthMessageReq = 9; + SetAmContractRequest setAmContractReq = 10; + ReadCrossChainMessageReceiptRequest readCrossChainMessageReceiptReq = 11; + ReadCrossChainMessagesByHeightRequest readCrossChainMessagesByHeightReq = 12; + QuerySDPMessageSeqRequest querySDPMessageSeqReq = 13; + QueryLatestHeightRequest queryLatestHeightReq = 14; + SetLocalDomainRequest setLocalDomainReq = 15; + ReadConsensusStateRequest readConsensusStateReq = 16; + HasTpBtaRequest hasTpBtaReq = 17; + GetTpBtaRequest getTpBtaReq = 18; + GetSupportedPTCTypeRequest getSupportedPTCTypeReq = 19; + GetPTCTrustRootRequest getPTCTrustRootReq = 20; + HasPTCTrustRootRequest hasPTCTrustRootReq = 21; + GetPTCVerifyAnchorRequest getPTCVerifyAnchorReq = 22; + HasPTCVerifyAnchorRequest hasPTCVerifyAnchorReq = 23; + SetupPTCContractRequest setupPTCContractReq = 24; + UpdatePTCTrustRootRequest updatePTCTrustRootReq = 25; + AddTpBtaRequest addTpBtaReq = 26; + SetPtcContractRequest setPtcContractReq = 27; + QueryValidatedBlockStateRequest queryValidatedBlockStateRequest = 28; + RecvOffChainExceptionRequest recvOffChainExceptionRequest = 29; + ReliableRetryRequest reliableRetryRequest = 30; + SetupMonitorMessageContractRequest setupMonitorMessageContractReq=31; + SetMonitorContractRequest setMonitorContractReq = 32; + SetProtocolInMonitorRequest setProtocolInMonitorReq = 33; + SetMonitorControlRequest setMonitorControlReq = 34; + SetPtcHubInMonitorVerifierRequest setPtcHubInMonitorVerifierReq = 35; + RelayMonitorOrderRequest relayMonitorOrderReq =36; + } +} + +message StartUpRequest { + bytes rawContext = 1; +} + +message GetContextRequest { + // stay empty body for now, maybe fill some stuff in future +} + +message ShutdownRequest { + // stay empty body for now, maybe fill some stuff in future +} + +message SetupAuthMessageContractRequest { + // stay empty body for now, maybe fill some stuff in future +} + +message SetupSDPMessageContractRequest { + // stay empty body for now, maybe fill some stuff in future +} + +message SetupMonitorMessageContractRequest { + // stay empty body for now, maybe fill some stuff in future +} + +message SetMonitorContractRequest { + string contractAddress = 1; +} + +message SetProtocolInMonitorRequest { + string protocolAddress = 1; +} + +message SetMonitorControlRequest { + uint32 monitorType = 1; +} + +message SetPtcHubInMonitorVerifierRequest { + string contractAddress = 1; +} + +message SetProtocolRequest { + string protocolAddress = 1; + string protocolType = 2; +} + +message SetPtcContractRequest { + string ptcContractAddress = 1; +} + +message RelayAuthMessageRequest { + bytes rawMessage = 1; +} + +message SetAmContractRequest { + string contractAddress = 1; +} + +message ReadCrossChainMessageReceiptRequest { + string txhash = 1; +} + +message ReadCrossChainMessagesByHeightRequest { + uint64 height = 1; +} + +message QuerySDPMessageSeqRequest { + string senderDomain = 1; + string fromAddress = 2; + string receiverDomain = 3; + string toAddress = 4; +} + +message QueryLatestHeightRequest { + // stay empty body for now, maybe fill some stuff in future +} + +message SetLocalDomainRequest { + string domain = 1; +} + +message ReadConsensusStateRequest { + string height = 1; +} + +message HasTpBtaRequest { + string tpbtaLane = 1; + uint32 tpBtaVersion = 2; +} + +message GetTpBtaRequest { + string tpbtaLane = 1; + uint32 tpBtaVersion = 2; +} + +message GetSupportedPTCTypeRequest { + // stay empty body for now, maybe fill some stuff in future +} + +message GetPTCTrustRootRequest { + bytes ptcOwnerOid = 1; +} + +message HasPTCTrustRootRequest { + bytes ptcOwnerOid = 1; +} + +message GetPTCVerifyAnchorRequest { + bytes ptcOwnerOid = 1; + string verifyAnchorVersion = 2; +} + +message HasPTCVerifyAnchorRequest { + bytes ptcOwnerOid = 1; + string verifyAnchorVersion = 2; +} + +message SetupPTCContractRequest { +} + +message UpdatePTCTrustRootRequest { + bytes ptcTrustRoot = 1; +} + +message AddTpBtaRequest { + bytes tpBta = 1; +} + +message QueryValidatedBlockStateRequest { + string receiverDomain = 1; +} + +message RecvOffChainExceptionRequest { + string exceptionMsgAuthor = 1; + bytes exceptionMsgPkg = 2; +} + +message ReliableRetryRequest { + bytes reliableCrossChainMessageData = 1; +} + +message RelayMonitorOrderRequest { + string committeeId = 1; + string signAlgo = 2; + bytes rawProof = 3; + bytes rawMonitorOrder = 4; +} + +// basic messages. +// same as project `antchain-bridge-commons` +message CrossChainMessageReceipt { + string txhash = 1; + bool confirmed = 2; + bool successful = 3; + string errorMsg = 4; + uint64 txTimestamp = 5; + bytes rawTx = 6; +} + +enum CrossChainMessageType { + AUTH_MSG = 0; + DEVELOPER_DESIGN = 1; +} + +message ProvableLedgerData { + uint64 height = 1; + bytes blockHash = 2; + uint64 timestamp = 3; + bytes ledgerData = 4; + bytes proof = 5; + bytes txHash = 6; +} + +message CrossChainMessage { + CrossChainMessageType type = 1; + bytes message = 2; + ProvableLedgerData provableData = 3; +} + +// messages for `bbcCall` responses +message CallBBCResponse { + oneof response { + GetContextResponse getContextResp = 1; + SetupAuthMessageContractResponse setupAMResp = 2; + SetupSDPMessageContractResponse setupSDPResp = 3; + ReadCrossChainMessageReceiptResponse readCrossChainMessageReceiptResp = 4; + ReadCrossChainMessagesByHeightResponse readCrossChainMessagesByHeightResp = 5; + QuerySDPMessageSeqResponse querySDPMsgSeqResp = 6; + RelayAuthMessageResponse relayAuthMessageResponse = 7; + QueryLatestHeightResponse queryLatestHeightResponse = 8; + ReadConsensusStateResponse readConsensusStateResponse = 9; + HasTpBtaResponse hasTpBtaResp = 10; + GetTpBtaResponse getTpBtaResp = 11; + GetSupportedPTCTypeResponse getSupportedPTCTypeResp = 12; + GetPTCTrustRootResponse getPTCTrustRootResp = 13; + HasPTCTrustRootResponse hasPTCTrustRootResp = 14; + GetPTCVerifyAnchorResponse getPTCVerifyAnchorResp = 15; + HasPTCVerifyAnchorResponse hasPTCVerifyAnchorResp = 16; + SetupPtcContractResponse setupPtcContractResp = 17; + QueryValidatedBlockStateResponse queryValidatedBlockStateResponse = 18; + RecvOffChainExceptionResponse recvOffChainExceptionResponse = 19; + ReliableRetryResponse reliableRetryResponse = 20; + SetupMonitorMessageContractResponse setupMonitorResp = 21; + RelayMonitorOrderResponse relayMonitorOrderResp = 22; + } +} + +message GetContextResponse { + bytes rawContext = 1; +} + +enum ContractStatusEnum { + INIT = 0; + CONTRACT_DEPLOYED = 1; + CONTRACT_READY = 2; + CONTRACT_FREEZE = 3; +} + +message AuthMessageContract { + string contractAddress = 1; + ContractStatusEnum status = 2; +} + +message SetupAuthMessageContractResponse { + AuthMessageContract amContract = 1; +} + +message SDPMessageContract { + string contractAddress = 1; + ContractStatusEnum status = 2; +} + +message SetupSDPMessageContractResponse { + SDPMessageContract sdpContract = 1; +} + +message MonitorMessageContract { + string contractAddress = 1; + ContractStatusEnum status = 2; +} + + +message SetupMonitorMessageContractResponse { + MonitorMessageContract monitorContract = 1; +} + +message PtcContract { + string contractAddress = 1; + ContractStatusEnum status = 2; +} + +message SetupPtcContractResponse { + PtcContract ptcContract = 1; +} + +message ReadCrossChainMessageReceiptResponse { + CrossChainMessageReceipt receipt = 1; +} + +message ReadCrossChainMessagesByHeightResponse { + repeated CrossChainMessage messageList = 1; +} + +message QuerySDPMessageSeqResponse { + uint64 sequence = 1; +} + +message RelayAuthMessageResponse { + CrossChainMessageReceipt receipt = 1; +} + +message QueryLatestHeightResponse { + uint64 height = 1; +} + +message ReadConsensusStateResponse { + bytes consensusState = 1; +} + +message HasTpBtaResponse { + bool result = 1; +} + +message GetTpBtaResponse { + bytes tpBta = 1; +} + +message GetSupportedPTCTypeResponse { + repeated string ptcTypes = 1; +} + +message GetPTCTrustRootResponse { + bytes ptcTrustRoot = 1; +} + +message HasPTCTrustRootResponse { + bool result = 1; +} + +message GetPTCVerifyAnchorResponse { + bytes ptcVerifyAnchor = 1; +} + +message HasPTCVerifyAnchorResponse { + bool result = 1; +} + +message QueryValidatedBlockStateResponse { + bytes blockStateData = 1; +} + +message RecvOffChainExceptionResponse { + CrossChainMessageReceipt receipt = 1; +} + +message ReliableRetryResponse { + CrossChainMessageReceipt receipt = 1; +} + +message RelayMonitorOrderResponse { + CrossChainMessageReceipt receipt = 1; +} \ No newline at end of file diff --git a/acb-committeeptc/monitor-node/src/main/resources/application.yml b/acb-committeeptc/monitor-node/src/main/resources/application.yml new file mode 100755 index 00000000..9f2e8935 --- /dev/null +++ b/acb-committeeptc/monitor-node/src/main/resources/application.yml @@ -0,0 +1,59 @@ +spring: + application: + name: committee-ptc + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://localhost:3306/committee_node?serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true + password: YOUR_PWD + username: root +logging: + file: + path: ./logs + level: + app: INFO +# setting for committee +committee: + id: default + node: + id: monitor-node + credential: + sign-algo: "Keccak256WithSecp256k1" + private-key-file: "file:private_key.pem" + cert-file: "file:ptc.crt" + plugin: + # where to load the hetero-chain plugins + repo: ./plugins + policy: + # limit actions of the plugin classloader + classloader: + resource: + ban-with-prefix: + # the plugin classloader will not read the resource file starting with the prefix below + APPLICATION: "META-INF/services/io.grpc." +grpc: + server: + port: 10081 + security: + # enable tls mode + enabled: true + # server certificate + certificate-chain: file:tls_certs/server.crt + # server key + private-key: file:tls_certs/server.key + # Mutual Certificate Authentication + trustCertCollection: file:tls_certs/trust.crt + # clientAuth: REQUIRE + clients: + # monitor system + monitor-system: + host: localhost + port: 50051 + # ps-server + plugin-server: + host: localhost + port: 9090 + ps-id: ps01 + security: + certificate-chain: file:tls_certs/server.crt + private-key: file:tls_certs/server.key + pluginServerCert: file:tls_certs/pluginServer.crt \ No newline at end of file diff --git a/acb-committeeptc/monitor-node/src/main/resources/banner.txt b/acb-committeeptc/monitor-node/src/main/resources/banner.txt new file mode 100755 index 00000000..cfcc6a90 --- /dev/null +++ b/acb-committeeptc/monitor-node/src/main/resources/banner.txt @@ -0,0 +1,7 @@ + __ __ ___ _ _ ___ _____ ___ ___ _ _ ___ ___ ___ +| \/ | / _ \ | \| | |_ _| |_ _| / _ \ | _ \ ___ | \| | / _ \ | \ | __| +| |\/| | | (_) | | .` | | | | | | (_) | | / |___| | .` | | (_) | | |) | | _| +|_| |_| \___/ |_|\_| |___| _|_|_ \___/ |_|_\ |_|\_| \___/ |___/ |___| + +${AnsiStyle.BOLD} @project.version@ +${AnsiStyle.NORMAL} \ No newline at end of file diff --git a/acb-committeeptc/monitor-node/src/main/resources/ddl.sql b/acb-committeeptc/monitor-node/src/main/resources/ddl.sql new file mode 100755 index 00000000..f23a7b6c --- /dev/null +++ b/acb-committeeptc/monitor-node/src/main/resources/ddl.sql @@ -0,0 +1,115 @@ +/* + * Copyright 2024 Ant Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +CREATE DATABASE IF NOT EXISTS committee_node; +USE committee_node; + +DROP TABLE IF EXISTS system_config; +CREATE TABLE IF NOT EXISTS `system_config` +( + `id` INT(11) NOT NULL AUTO_INCREMENT, + `conf_key` VARCHAR(128) DEFAULT NULL, + `conf_value` VARCHAR(15000) DEFAULT NULL, + `gmt_create` DATETIME DEFAULT CURRENT_TIMESTAMP, + `gmt_modified` DATETIME DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `conf_key` (`conf_key`) +) ENGINE = InnoDB + ROW_FORMAT = DYNAMIC; + +DROP TABLE IF EXISTS `bcdns_service`; +CREATE TABLE IF NOT EXISTS `bcdns_service` +( + `id` INT(11) PRIMARY KEY NOT NULL AUTO_INCREMENT, + `domain_space` VARCHAR(128) BINARY NOT NULL, + `parent_space` VARCHAR(128) BINARY, + `owner_oid` VARCHAR(255) NOT NULL, + `type` VARCHAR(32) NOT NULL, + `state` INT NOT NULL, + `properties` BLOB, + `gmt_create` DATETIME DEFAULT CURRENT_TIMESTAMP, + `gmt_modified` DATETIME DEFAULT CURRENT_TIMESTAMP +); +CREATE UNIQUE INDEX bcdns_network_id_domain_space + ON bcdns_service (domain_space); + +DROP TABLE IF EXISTS `domain_space_cert`; +CREATE TABLE `domain_space_cert` +( + `id` INT(11) NOT NULL AUTO_INCREMENT, + `domain_space` VARCHAR(128) BINARY DEFAULT NULL, + `parent_space` VARCHAR(128) BINARY DEFAULT NULL, + `owner_oid_hex` VARCHAR(255) NOT NULL, + `description` VARCHAR(128) DEFAULT NULL, + `domain_space_cert` LONGBLOB, + `gmt_create` DATETIME DEFAULT CURRENT_TIMESTAMP, + `gmt_modified` DATETIME DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `domain_space` (`domain_space`) +) ENGINE = InnoDB; +CREATE INDEX domain_space_cert_owner_oid_hex + ON domain_space_cert (owner_oid_hex); + +DROP TABLE IF EXISTS `bta`; +CREATE TABLE IF NOT EXISTS `bta` +( + `id` INT(11) PRIMARY KEY NOT NULL AUTO_INCREMENT, + `product` VARCHAR(64) DEFAULT NULL, + `domain` VARCHAR(128) BINARY NOT NULL, + `bta_version` INT(11) DEFAULT NULL, + `subject_version` INT(11) DEFAULT NULL, + `raw_bta` BLOB, + `gmt_create` DATETIME DEFAULT CURRENT_TIMESTAMP, + `gmt_modified` DATETIME DEFAULT CURRENT_TIMESTAMP +); +CREATE UNIQUE INDEX bta_unique_key_domain_subject_version + ON bta (domain, subject_version); + +DROP TABLE IF EXISTS `tpbta`; +CREATE TABLE IF NOT EXISTS `tpbta` +( + `id` INT(11) PRIMARY KEY NOT NULL AUTO_INCREMENT, + `version` INT(11) DEFAULT NULL, + `sender_domain` VARCHAR(128) BINARY NOT NULL, + `bta_subject_version` INT(11) DEFAULT NULL, + `sender_id` VARCHAR(64) DEFAULT NULL, + `receiver_domain` VARCHAR(128) DEFAULT NULL, + `receiver_id` VARCHAR(64) DEFAULT NULL, + `tpbta_version` INT(11) DEFAULT NULL, + `ptc_verify_anchor_version` INT(11) DEFAULT NULL, + `raw_tpbta` BLOB, + `gmt_create` DATETIME DEFAULT CURRENT_TIMESTAMP, + `gmt_modified` DATETIME DEFAULT CURRENT_TIMESTAMP +); +CREATE UNIQUE INDEX tpbta_unique_key_crosschain_lane_and_version + ON tpbta (sender_domain, sender_id, receiver_domain, receiver_id, tpbta_version); + +CREATE TABLE IF NOT EXISTS `validated_consensus_states` +( + `id` INT(11) PRIMARY KEY NOT NULL AUTO_INCREMENT, + `cs_version` INT(11) NOT NULL, + `domain` VARCHAR(128) BINARY NOT NULL, + `height` BIGINT UNSIGNED NOT NULL, + `hash` VARCHAR(64) DEFAULT NULL, + `parent_hash` VARCHAR(64) DEFAULT NULL, + `raw_vcs` LONGBLOB, + `gmt_create` DATETIME DEFAULT CURRENT_TIMESTAMP, + `gmt_modified` DATETIME DEFAULT CURRENT_TIMESTAMP +); +CREATE UNIQUE INDEX committee_node_vcs_unique_key + ON validated_consensus_states (domain, height); +CREATE INDEX committee_node_vcs_unique_key_hash + ON validated_consensus_states (domain, hash); \ No newline at end of file diff --git a/acb-committeeptc/monitor-node/src/main/resources/logback-spring.xml b/acb-committeeptc/monitor-node/src/main/resources/logback-spring.xml new file mode 100755 index 00000000..372baddf --- /dev/null +++ b/acb-committeeptc/monitor-node/src/main/resources/logback-spring.xml @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + ${logging.path}/${APP_NAME}/error.log + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - + %msg%n + + UTF-8 + + + ${logging.path}/${APP_NAME}/error.log.%d{yyyy-MM-dd}.%i + 7 + 100MB + 2GB + + + ERROR + ACCEPT + DENY + + + + + 0 + + 512 + + + + + + + ${logging.path}/${APP_NAME}/application.log + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - + %msg%n + + UTF-8 + + + ${logging.path}/${APP_NAME}/application.log.%d{yyyy-MM-dd}.%i + 7 + 50MB + 2GB + true + + + ERROR + DENY + ACCEPT + + + + + 0 + + 512 + + + + + + + + + + %clr(%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx} + + + + INFO + + + + + + ${logging.path}/${APP_NAME}/req_trace.log + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - + %msg%n + + UTF-8 + + + ${logging.path}/${APP_NAME}/req_trace.log.%d{yyyy-MM-dd}.%i + 7 + 50MB + 500MB + true + + + + + 0 + + 256 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/acb-committeeptc/monitor-node/src/main/resources/scripts/init_tls_certs.sh b/acb-committeeptc/monitor-node/src/main/resources/scripts/init_tls_certs.sh new file mode 100755 index 00000000..e621409e --- /dev/null +++ b/acb-committeeptc/monitor-node/src/main/resources/scripts/init_tls_certs.sh @@ -0,0 +1,51 @@ +#!/bin/bash + +# +# Copyright 2023 Ant Group +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +CURR_DIR="$(cd `dirname $0`; pwd)" +source ${CURR_DIR}/print.sh + +print_title + +if [ ! -d ${CURR_DIR}/../tls_certs ]; then + mkdir -p ${CURR_DIR}/../tls_certs +fi + +openssl genrsa -out ${CURR_DIR}/../tls_certs/server.key 2048 > /dev/null 2>&1 +if [ $? -ne 0 ]; then + log_error "failed to generate server.key" + exit 1 +fi +openssl pkcs8 -topk8 -inform pem -in ${CURR_DIR}/../tls_certs/server.key -nocrypt -out ${CURR_DIR}/../tls_certs/server_pkcs8.key +if [ $? -ne 0 ]; then + log_error "failed to generate pkcs8 server.key" + exit 1 +fi +mv ${CURR_DIR}/../tls_certs/server_pkcs8.key ${CURR_DIR}/../tls_certs/server.key +log_info "generate server.key successfully" + +openssl req -new -x509 -days 36500 -key ${CURR_DIR}/../tls_certs/server.key -out ${CURR_DIR}/../tls_certs/server.crt -subj "/C=CN/ST=mykey/L=mykey/O=mykey/OU=mykey/CN=MONITOR-NODE" +if [ $? -ne 0 ]; then + log_error "failed to generate server.crt" + exit 1 +fi +log_info "generate server.crt successfully" + +if [ ! -f "trust.crt" ]; then + cp ${CURR_DIR}/../tls_certs/server.crt ${CURR_DIR}/../tls_certs/trust.crt + log_info "generate trust.crt successfully" +fi diff --git a/acb-committeeptc/monitor-node/src/main/resources/scripts/monitor-node.service b/acb-committeeptc/monitor-node/src/main/resources/scripts/monitor-node.service new file mode 100755 index 00000000..5b8b60c5 --- /dev/null +++ b/acb-committeeptc/monitor-node/src/main/resources/scripts/monitor-node.service @@ -0,0 +1,28 @@ +# +# Copyright 2023 Ant Group +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +[Unit] +Description=start shell script +StartLimitIntervalSec=0 + +[Service] +ExecStart=@@START_CMD@@ +Restart=always +RestartSec=5 +WorkingDirectory=@@WORKING_DIR@@ + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/acb-committeeptc/monitor-node/src/main/resources/scripts/print.sh b/acb-committeeptc/monitor-node/src/main/resources/scripts/print.sh new file mode 100755 index 00000000..bc347af3 --- /dev/null +++ b/acb-committeeptc/monitor-node/src/main/resources/scripts/print.sh @@ -0,0 +1,73 @@ +# +# Copyright 2023 Ant Group +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +RED='\033[0;31m' +GREEN='\033[0;32m' +BLUE='\033[0;34m' +YELLOW='\033[1;33m' +WHITE='\033[1;37m' +NC='\033[0m' +LIGHT_GRAY='\033[0;37m' + +function print_blue() { + printf "${BLUE}%s${NC}\n" "$1" +} + +function print_red() { + printf "${RED}%s${NC}\n" "$1" +} + +function print_green() { + printf "${GREEN}%s${NC}\n" "$1" +} + +function print_hint() { + printf "${WHITE}\033[4m%s${NC}" "$1" +} + +function log_info() { + NOW=$(date "+%Y-%m-%d %H:%M:%S.%s" | cut -b 1-23) + INFO_PREFIX=$(printf "${GREEN}\033[4m[ INFO ]${NC}") + + INFO=$(printf "_${LIGHT_GRAY}[ %s ]${NC} : %s" "${NOW}" "$1") + echo "${INFO_PREFIX}${INFO}" +} + +function log_warn() { + NOW=$(date "+%Y-%m-%d %H:%M:%S.%s" | cut -b 1-23) + WARN_PREFIX=$(printf "${YELLOW}\033[4m[ WARN ]${NC}") + + INFO=$(printf "_${LIGHT_GRAY}[ %s ]${NC} : %s" "${NOW}" "$1") + echo "${WARN_PREFIX}${INFO}" +} + +function log_error() { + NOW=$(date "+%Y-%m-%d %H:%M:%S.%s" | cut -b 1-23) + ERROR_PREFIX=$(printf "${RED}\033[4m[ ERROR ]${NC}") + + INFO=$(printf "_${LIGHT_GRAY}[ %s ]${NC} : %s" "${NOW}" "$1") + echo "${ERROR_PREFIX}${INFO}" +} + +function print_title() { + echo ' ___ __ ______ __ _ ____ _ __' + echo ' / | ____ / /_ / ____// /_ ____ _ (_)____ / __ ) _____ (_)____/ /____ _ ___' + echo ' / /| | / __ \ / __// / / __ \ / __ `// // __ \ / __ |/ ___// // __ // __ `// _ \' + echo ' / ___ | / / / // /_ / /___ / / / // /_/ // // / / / / /_/ // / / // /_/ // /_/ // __/' + echo '/_/ |_|/_/ /_/ \__/ \____//_/ /_/ \__,_//_//_/ /_/ /_____//_/ /_/ \__,_/ \__, / \___/' + echo ' /____/ ' + echo +} diff --git a/acb-committeeptc/monitor-node/src/main/resources/scripts/start.sh b/acb-committeeptc/monitor-node/src/main/resources/scripts/start.sh new file mode 100755 index 00000000..e46ee30e --- /dev/null +++ b/acb-committeeptc/monitor-node/src/main/resources/scripts/start.sh @@ -0,0 +1,139 @@ +#!/bin/bash + +# +# Copyright 2023 Ant Group +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +Help=$( + cat <<-"HELP" + + start.sh - Start the monitor Node + + Usage: + start.sh + + Examples: + 1. start in system service mode: + start.sh -s + 2. start in application mode: + start.sh + 3. start with configuration encrypted: + start.sh -P your_jasypt_password + + Options: + -s run in system service mode. + -P your jasypt password. + -h print help information. + +HELP +) + +CURR_DIR="$( + cd $(dirname $0) + pwd +)" + +while getopts "hsP:" opt; do + case "$opt" in + "h") + echo "$Help" + exit 0 + ;; + "s") + IF_SYS_MODE="on" + ;; + "P") + JASYPT_PASSWD=$OPTARG + ;; + "?") + echo "invalid arguments. " + exit 1 + ;; + *) + echo "Unknown error while processing options" + exit 1 + ;; + esac +done + +source ${CURR_DIR}/print.sh + +print_title + +JAR_FILE=$(ls ${CURR_DIR}/../lib/ | grep '.jar') + +if [[ -n "${JASYPT_PASSWD}" ]]; then + JASYPT_FLAG="--jasypt.encryptor.password=${JASYPT_PASSWD}" +fi + +if [ "$IF_SYS_MODE" == "on" ]; then + if [[ "$OSTYPE" == "darwin"* ]]; then + log_error "${OSTYPE} not support running in system service mode" + exit 1 + fi + + touch /usr/lib/systemd/system/test123 >/dev/null && rm -f /usr/lib/systemd/system/test123 + if [ $? -ne 0 ]; then + log_error "Your account on this OS must have authority to access /usr/lib/systemd/system/" + exit 1 + fi + + log_info "running in system service mode" + + JAVA_BIN=$(which java) + if [ -z "$JAVA_BIN" ]; then + log_error "install jdk before start" + exit 1 + fi + START_CMD="${JAVA_BIN} -jar -Dlogging.file.path=${CURR_DIR}/../log ${CURR_DIR}/../lib/${JAR_FILE} --spring.config.location=file:${CURR_DIR}/../config/application.yml ${JASYPT_FLAG}" + WORK_DIR="$( + cd ${CURR_DIR}/.. + pwd + )" + + sed -i -e "s#@@START_CMD@@#${START_CMD}#g" ${CURR_DIR}/monitor-node.service + sed -i -e "s#@@WORKING_DIR@@#${WORK_DIR}#g" ${CURR_DIR}/monitor-node.service + + cp -f ${CURR_DIR}/monitor-node.service /usr/lib/systemd/system/ + if [ $? -ne 0 ]; then + log_error "failed to cp monitor-node.service to /usr/lib/systemd/system/" + exit 1 + fi + + systemctl daemon-reload && systemctl enable monitor-node.service + if [ $? -ne 0 ]; then + log_error "failed to enable monitor-node.service" + exit 1 + fi + + systemctl start monitor-node + if [ $? -ne 0 ]; then + log_error "failed to start monitor-node.service" + exit 1 + fi + +else + log_info "running in app mode" + log_info "start monitor-node now..." + + cd ${CURR_DIR}/.. + java -jar -Dlogging.file.path=${CURR_DIR}/../log ${CURR_DIR}/../lib/${JAR_FILE} --spring.config.location=file:${CURR_DIR}/../config/application.yml ${JASYPT_FLAG} >/dev/null 2>&1 & + if [ $? -ne 0 ]; then + log_error "failed to start monitor-node" + exit 1 + fi +fi + +log_info "monitor-node started successfully" diff --git a/acb-committeeptc/monitor-node/src/main/resources/scripts/stop.sh b/acb-committeeptc/monitor-node/src/main/resources/scripts/stop.sh new file mode 100755 index 00000000..f50e775f --- /dev/null +++ b/acb-committeeptc/monitor-node/src/main/resources/scripts/stop.sh @@ -0,0 +1,35 @@ +#!/bin/bash + +# +# Copyright 2023 Ant Group +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +CURR_DIR="$( + cd $(dirname $0) + pwd +)" +source ${CURR_DIR}/print.sh + +print_title + +log_info "stop monitor-node now..." + +ps -ewf | grep -e "monitor-node-.*\.jar" | grep -v grep | awk '{print $2}' | xargs kill +if [ $? -ne 0 ]; then + log_error "failed to stop monitor-node" + exit 1 +fi + +log_info "monitor-node stopped successfully" diff --git a/acb-committeeptc/monitor-node/src/test/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/TestBase.java b/acb-committeeptc/monitor-node/src/test/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/TestBase.java new file mode 100755 index 00000000..f1861f1d --- /dev/null +++ b/acb-committeeptc/monitor-node/src/test/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/TestBase.java @@ -0,0 +1,192 @@ +/* + * Copyright 2024 Ant Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alipay.antchain.bridge.ptc.committee.monitor.node; + + +import java.io.ByteArrayInputStream; +import java.security.PrivateKey; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.crypto.PemUtil; +import com.alipay.antchain.bridge.bcdns.service.IBlockChainDomainNameService; +import com.alipay.antchain.bridge.bcdns.types.base.DomainRouter; +import com.alipay.antchain.bridge.bcdns.types.exception.AntChainBridgeBCDNSException; +import com.alipay.antchain.bridge.bcdns.types.req.*; +import com.alipay.antchain.bridge.bcdns.types.resp.*; +import com.alipay.antchain.bridge.commons.bcdns.AbstractCrossChainCertificate; +import com.alipay.antchain.bridge.commons.bcdns.utils.CrossChainCertificateUtil; +import com.alipay.antchain.bridge.commons.core.base.ObjectIdentity; +import com.alipay.antchain.bridge.commons.core.ptc.PTCTrustRoot; +import com.alipay.antchain.bridge.commons.core.ptc.ThirdPartyBlockchainTrustAnchor; +import com.alipay.antchain.bridge.commons.utils.crypto.SignAlgoEnum; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.service.IScheduledTaskService; +import org.junit.runner.RunWith; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.context.junit4.SpringRunner; + +@ActiveProfiles("test") +@RunWith(SpringRunner.class) +@SpringBootTest(classes = NodeApplication.class, webEnvironment = SpringBootTest.WebEnvironment.NONE) +@Sql(scripts = {"classpath:data/ddl.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) +@Sql(scripts = "classpath:data/drop_all.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) +public abstract class TestBase { + + public static final String BCDNS_ROOT_CERT = """ + -----BEGIN BCDNS TRUST ROOT CERTIFICATE----- + AADWAQAAAAABAAAAMQEABAAAAHRlc3QCAAEAAAAAAwBrAAAAAABlAAAAAAABAAAA + AAEAWAAAADBWMBAGByqGSM49AgEGBSuBBAAKA0IABPSyWJiXGQUhIzdqzRq7hdcy + CKuSS40qpcGUNsTXJtky9Ka1hXWqbdAVawAqWsNDIrSp2I5HL9eqpvl1GxSvxN8E + AAgAAADSJb9mAAAAAAUACAAAAFJZoGgAAAAABgCGAAAAAACAAAAAAAADAAAAYmlm + AQBrAAAAAABlAAAAAAABAAAAAAEAWAAAADBWMBAGByqGSM49AgEGBSuBBAAKA0IA + BPSyWJiXGQUhIzdqzRq7hdcyCKuSS40qpcGUNsTXJtky9Ka1hXWqbdAVawAqWsND + IrSp2I5HL9eqpvl1GxSvxN8CAAAAAAAHAJ8AAAAAAJkAAAAAAAoAAABLRUNDQUst + MjU2AQAgAAAAvSTYE3fohb8st2Hu6eGR0uR+HI+Fr+ig4A/wR/c7ahMCABYAAABL + ZWNjYWsyNTZXaXRoU2VjcDI1NmsxAwBBAAAAsGsuR7geJEPmaO9udja1wW+da1ex + KNVhpk7oi66g3UNNpYSoJK3wzibTKBj/cRfZCY/FkZdp95j6mMcK2oHsAAA= + -----END BCDNS TRUST ROOT CERTIFICATE----- + """; + + public static final String DOT_COM_DOMAIN_SPACE_CERT = """ + -----BEGIN DOMAIN NAME CERTIFICATE----- + AADtAQAAAAABAAAAMQEABAAAAC5jb20CAAEAAAABAwBrAAAAAABlAAAAAAABAAAA + AAEAWAAAADBWMBAGByqGSM49AgEGBSuBBAAKA0IABPSyWJiXGQUhIzdqzRq7hdcy + CKuSS40qpcGUNsTXJtky9Ka1hXWqbdAVawAqWsNDIrSp2I5HL9eqpvl1GxSvxN8E + AAgAAADSJb9mAAAAAAUACAAAAFJZoGgAAAAABgCdAAAAAACXAAAAAAADAAAAMS4w + AQABAAAAAQIAAAAAAAMABAAAAC5jb20EAGsAAAAAAGUAAAAAAAEAAAAAAQBYAAAA + MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAE9LJYmJcZBSEjN2rNGruF1zIIq5JLjSql + wZQ2xNcm2TL0prWFdapt0BVrACpaw0MitKnYjkcv16qm+XUbFK/E3wUAAAAAAAcA + nwAAAAAAmQAAAAAACgAAAEtFQ0NBSy0yNTYBACAAAADTK+miwHYLK8NTN2okHMfo + mEShYXWhzkrjivLNXDGt/wIAFgAAAEtlY2NhazI1NldpdGhTZWNwMjU2azEDAEEA + AAAWu0d+MaWZfLOUVBnDT2/uC+IxKUyZqxdjsNXy2x7n7zJYSgof+ujJWE7r8qWT + 1tBHkbDC/YHXA8QLgVPC2NfMAQ== + -----END DOMAIN NAME CERTIFICATE----- + """; + + public static final String ANTCHAIN_DOT_COM_CERT = """ + -----BEGIN DOMAIN NAME CERTIFICATE----- + AAD/AQAAAAABAAAAMQEACgAAAHRlc3Rkb21haW4CAAEAAAABAwBrAAAAAABlAAAA + AAABAAAAAAEAWAAAADBWMBAGByqGSM49AgEGBSuBBAAKA0IABPSyWJiXGQUhIzdq + zRq7hdcyCKuSS40qpcGUNsTXJtky9Ka1hXWqbdAVawAqWsNDIrSp2I5HL9eqpvl1 + GxSvxN8EAAgAAADSJb9mAAAAAAUACAAAAFJZoGgAAAAABgCpAAAAAACjAAAAAAAD + AAAAMS4wAQABAAAAAAIABAAAAC5jb20DAAwAAABhbnRjaGFpbi5jb20EAGsAAAAA + AGUAAAAAAAEAAAAAAQBYAAAAMFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAE9LJYmJcZ + BSEjN2rNGruF1zIIq5JLjSqlwZQ2xNcm2TL0prWFdapt0BVrACpaw0MitKnYjkcv + 16qm+XUbFK/E3wUAAAAAAAcAnwAAAAAAmQAAAAAACgAAAEtFQ0NBSy0yNTYBACAA + AAD2j0+ge6shN1piGmDyb+YY7E3Fs4E7SMeQGxvOiJC5sgIAFgAAAEtlY2NhazI1 + NldpdGhTZWNwMjU2azEDAEEAAAB8QQjC0e6/Xl4PaTcx+BtX/fg3BQN9b0YdXEXR + oYklgXW6KHQ7YLt7farETz2inRjlT0eJka4LvJUSinX53WXVAA== + -----END DOMAIN NAME CERTIFICATE----- + """; + + public static final AbstractCrossChainCertificate NODE_PTC_CERT = CrossChainCertificateUtil.readCrossChainCertificateFromPem( + FileUtil.readBytes("ptc.crt") + ); + + public static final PrivateKey NODE_PTC_PRIVATE_KEY = SignAlgoEnum.KECCAK256_WITH_SECP256K1.getSigner().readPemPrivateKey( + FileUtil.readBytes("private_key.pem") + ); + + public static final byte[] RAW_NODE_PTC_PUBLIC_KEY = PemUtil.readPem(new ByteArrayInputStream(FileUtil.readBytes("public_key.pem"))); + + @MockBean + private IScheduledTaskService scheduledTaskService; + + public static class DummyBcdnsServiceImpl implements IBlockChainDomainNameService { + @Override + public QueryBCDNSTrustRootCertificateResponse queryBCDNSTrustRootCertificate() { + return null; + } + + @Override + public ApplyRelayerCertificateResponse applyRelayerCertificate(AbstractCrossChainCertificate abstractCrossChainCertificate) { + return null; + } + + @Override + public ApplicationResult queryRelayerCertificateApplicationResult(String s) { + return null; + } + + @Override + public ApplyPTCCertificateResponse applyPTCCertificate(AbstractCrossChainCertificate abstractCrossChainCertificate) { + return null; + } + + @Override + public ApplicationResult queryPTCCertificateApplicationResult(String s) { + return null; + } + + @Override + public ApplyDomainNameCertificateResponse applyDomainNameCertificate(AbstractCrossChainCertificate abstractCrossChainCertificate) { + return null; + } + + @Override + public ApplicationResult queryDomainNameCertificateApplicationResult(String s) { + return null; + } + + @Override + public QueryRelayerCertificateResponse queryRelayerCertificate(QueryRelayerCertificateRequest queryRelayerCertificateRequest) { + return null; + } + + @Override + public QueryPTCCertificateResponse queryPTCCertificate(QueryPTCCertificateRequest queryPTCCertificateRequest) { + return null; + } + + @Override + public QueryDomainNameCertificateResponse queryDomainNameCertificate(QueryDomainNameCertificateRequest queryDomainNameCertificateRequest) { + return null; + } + + @Override + public void registerDomainRouter(RegisterDomainRouterRequest registerDomainRouterRequest) throws AntChainBridgeBCDNSException { + + } + + @Override + public void registerThirdPartyBlockchainTrustAnchor(RegisterThirdPartyBlockchainTrustAnchorRequest registerThirdPartyBlockchainTrustAnchorRequest) throws AntChainBridgeBCDNSException { + + } + + @Override + public DomainRouter queryDomainRouter(QueryDomainRouterRequest queryDomainRouterRequest) { + return null; + } + + @Override + public ThirdPartyBlockchainTrustAnchor queryThirdPartyBlockchainTrustAnchor(QueryThirdPartyBlockchainTrustAnchorRequest queryThirdPartyBlockchainTrustAnchorRequest) { + return null; + } + + @Override + public PTCTrustRoot queryPTCTrustRoot(ObjectIdentity objectIdentity) { + return null; + } + + @Override + public void addPTCTrustRoot(PTCTrustRoot ptcTrustRoot) { + + } + } +} diff --git a/acb-committeeptc/monitor-node/src/test/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/dal/BCDNSRepositoryTest.java b/acb-committeeptc/monitor-node/src/test/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/dal/BCDNSRepositoryTest.java new file mode 100755 index 00000000..d35d3e90 --- /dev/null +++ b/acb-committeeptc/monitor-node/src/test/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/dal/BCDNSRepositoryTest.java @@ -0,0 +1,125 @@ +/* + * Copyright 2024 Ant Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alipay.antchain.bridge.ptc.committee.monitor.node.dal; + +import cn.hutool.core.util.ArrayUtil; +import com.alipay.antchain.bridge.bcdns.service.BCDNSTypeEnum; +import com.alipay.antchain.bridge.commons.bcdns.AbstractCrossChainCertificate; +import com.alipay.antchain.bridge.commons.bcdns.utils.CrossChainCertificateUtil; +import com.alipay.antchain.bridge.commons.core.base.CrossChainDomain; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.TestBase; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.commons.enums.BCDNSStateEnum; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.commons.models.BCDNSServiceDO; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.commons.models.DomainSpaceCertWrapper; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.dal.repository.interfaces.IBCDNSRepository; +import jakarta.annotation.Resource; +import org.junit.Assert; +import org.junit.Test; + +public class BCDNSRepositoryTest extends TestBase { + + @Resource + private IBCDNSRepository bcdnsRepository; + + private final AbstractCrossChainCertificate bcdnsRootCertObj = CrossChainCertificateUtil.readCrossChainCertificateFromPem(BCDNS_ROOT_CERT.getBytes()); + + @Test + public void testDomainSpaceCert() { + var dscObj = CrossChainCertificateUtil.readCrossChainCertificateFromPem(DOT_COM_DOMAIN_SPACE_CERT.getBytes()); + + bcdnsRepository.saveDomainSpaceCert(new DomainSpaceCertWrapper(bcdnsRootCertObj)); + bcdnsRepository.saveDomainSpaceCert(new DomainSpaceCertWrapper(dscObj)); + + Assert.assertEquals( + bcdnsRootCertObj.getCredentialSubjectInstance().getApplicant(), + bcdnsRepository.getDomainSpaceCert(CrossChainCertificateUtil.getCrossChainDomainSpace(bcdnsRootCertObj).getDomain()).getOwnerOid() + ); + Assert.assertTrue( + ArrayUtil.equals( + bcdnsRootCertObj.encode(), + bcdnsRepository.getDomainSpaceCert(bcdnsRootCertObj.getCredentialSubjectInstance().getApplicant()).getDomainSpaceCert().encode() + ) + ); + Assert.assertTrue( + bcdnsRepository.hasDomainSpaceCert(CrossChainCertificateUtil.getCrossChainDomainSpace(bcdnsRootCertObj).getDomain()) + ); + + Assert.assertEquals( + 2, + bcdnsRepository.getDomainSpaceCertChain(CrossChainCertificateUtil.getCrossChainDomainSpace(dscObj).getDomain()).size() + ); + Assert.assertEquals( + CrossChainCertificateUtil.getCrossChainDomainSpace(bcdnsRootCertObj).getDomain(), + bcdnsRepository.getDomainSpaceCertChain(CrossChainCertificateUtil.getCrossChainDomainSpace(dscObj).getDomain()) + .get(CrossChainDomain.ROOT_DOMAIN_SPACE).getDomainSpace() + ); + } + + @Test + public void testBcdns() { + + var bcdnsService = new BCDNSServiceDO(); + bcdnsService.setDomainSpaceCertWrapper(new DomainSpaceCertWrapper(bcdnsRootCertObj)); + bcdnsService.setDomainSpace(CrossChainCertificateUtil.getCrossChainDomainSpace(bcdnsRootCertObj).getDomain()); + bcdnsService.setType(BCDNSTypeEnum.EMBEDDED); + bcdnsService.setState(BCDNSStateEnum.WORKING); + bcdnsService.setOwnerOid(bcdnsRootCertObj.getCredentialSubjectInstance().getApplicant()); + bcdnsService.setProperties("{}".getBytes()); + + bcdnsRepository.saveBCDNSServiceDO(bcdnsService); + + Assert.assertEquals( + 1, + bcdnsRepository.getAllBCDNSDomainSpace().size() + ); + Assert.assertNotNull( + bcdnsRepository.getBCDNSServiceDO(bcdnsService.getDomainSpace()) + ); + Assert.assertEquals( + bcdnsService.getOwnerOid(), + bcdnsRepository.getBCDNSServiceDO(bcdnsService.getDomainSpace()).getOwnerOid() + ); + Assert.assertEquals( + bcdnsService.getState(), + bcdnsRepository.getBCDNSServiceDO(bcdnsService.getDomainSpace()).getState() + ); + Assert.assertTrue( + ArrayUtil.equals( + bcdnsService.getProperties(), + bcdnsRepository.getBCDNSServiceDO(bcdnsService.getDomainSpace()).getProperties() + ) + ); + Assert.assertTrue( + bcdnsRepository.hasBCDNSService(bcdnsService.getDomainSpace()) + ); + Assert.assertEquals(1, bcdnsRepository.countBCDNSService()); + + bcdnsRepository.updateBCDNSServiceState(bcdnsService.getDomainSpace(), BCDNSStateEnum.FROZEN); + Assert.assertEquals( + BCDNSStateEnum.FROZEN, + bcdnsRepository.getBCDNSServiceDO(bcdnsService.getDomainSpace()).getState() + ); + var newProp = """ + {"test": "test"} + """; + bcdnsRepository.updateBCDNSServiceProperties(bcdnsService.getDomainSpace(), newProp.getBytes()); + Assert.assertEquals( + newProp, + new String(bcdnsRepository.getBCDNSServiceDO(bcdnsService.getDomainSpace()).getProperties()) + ); + } +} diff --git a/acb-committeeptc/monitor-node/src/test/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/dal/EndorseServiceRepositoryTest.java b/acb-committeeptc/monitor-node/src/test/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/dal/EndorseServiceRepositoryTest.java new file mode 100755 index 00000000..219bcb3a --- /dev/null +++ b/acb-committeeptc/monitor-node/src/test/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/dal/EndorseServiceRepositoryTest.java @@ -0,0 +1,162 @@ +/* + * Copyright 2024 Ant Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alipay.antchain.bridge.ptc.committee.monitor.node.dal; + +import java.math.BigInteger; +import java.security.KeyPairGenerator; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.ListUtil; +import cn.hutool.core.util.HexUtil; +import cn.hutool.core.util.RandomUtil; +import com.alipay.antchain.bridge.commons.bcdns.PTCCredentialSubject; +import com.alipay.antchain.bridge.commons.core.base.*; +import com.alipay.antchain.bridge.commons.core.bta.BlockchainTrustAnchorV1; +import com.alipay.antchain.bridge.commons.core.ptc.PTCTypeEnum; +import com.alipay.antchain.bridge.commons.core.ptc.ThirdPartyBlockchainTrustAnchorV1; +import com.alipay.antchain.bridge.commons.core.ptc.ValidatedConsensusStateV1; +import com.alipay.antchain.bridge.commons.utils.crypto.HashAlgoEnum; +import com.alipay.antchain.bridge.commons.utils.crypto.SignAlgoEnum; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.TestBase; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.commons.models.BtaWrapper; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.commons.models.TpBtaWrapper; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.commons.models.ValidatedConsensusStateWrapper; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.dal.repository.interfaces.IEndorseServiceRepository; +import com.alipay.antchain.bridge.ptc.committee.types.basic.CommitteeEndorseProof; +import com.alipay.antchain.bridge.ptc.committee.types.basic.CommitteeNodeProof; +import com.alipay.antchain.bridge.ptc.committee.types.basic.NodePublicKeyEntry; +import com.alipay.antchain.bridge.ptc.committee.types.tpbta.CommitteeEndorseRoot; +import com.alipay.antchain.bridge.ptc.committee.types.tpbta.NodeEndorseInfo; +import com.alipay.antchain.bridge.ptc.committee.types.tpbta.OptionalEndorsePolicy; +import jakarta.annotation.Resource; +import lombok.SneakyThrows; +import org.junit.Assert; +import org.junit.Test; + +public class EndorseServiceRepositoryTest extends TestBase { + + @Resource + private IEndorseServiceRepository endorseServiceRepository; + + @SneakyThrows + @Test + public void testBta() { + var oid = new ObjectIdentity( + ObjectIdentityType.X509_PUBLIC_KEY_INFO, + KeyPairGenerator.getInstance("RSA").generateKeyPair().getPublic().getEncoded() + ); + var bta = new BlockchainTrustAnchorV1(); + bta.setBcOwnerPublicKey("test".getBytes()); + bta.setBcOwnerSig("test".getBytes()); + bta.setDomain(new CrossChainDomain("test")); + bta.setSubjectIdentity("test".getBytes()); + bta.setBcOwnerSigAlgo(SignAlgoEnum.SHA256_WITH_ECDSA); + bta.setPtcOid(oid); + bta.setSubjectProduct("test"); + bta.setExtension("test".getBytes()); + bta.setSubjectVersion(0); + + var btaWrapper = new BtaWrapper(); + btaWrapper.setBta(bta); + endorseServiceRepository.setBta(btaWrapper); + + var btaWrapperFromDB = endorseServiceRepository.getBta(btaWrapper.getDomain()); + Assert.assertEquals( + HexUtil.encodeHexStr(btaWrapper.getBta().getSubjectIdentity()), + HexUtil.encodeHexStr(btaWrapperFromDB.getBta().getSubjectIdentity()) + ); + } + + @Test + @SneakyThrows + public void testTpBta() { + var policy = new OptionalEndorsePolicy(); + policy.setThreshold(new OptionalEndorsePolicy.Threshold(OptionalEndorsePolicy.OperatorEnum.GREATER_THAN, 1)); + var nodeEndorseInfo = new NodeEndorseInfo(); + nodeEndorseInfo.setNodeId("test"); + nodeEndorseInfo.setRequired(true); + var nodePubkeyEntry = new NodePublicKeyEntry("default", SignAlgoEnum.KECCAK256_WITH_SECP256K1.getSigner().generateKeyPair().getPublic()); + nodeEndorseInfo.setPublicKey(nodePubkeyEntry); + var crossChainLane = new CrossChainLane(new CrossChainDomain("test"), new CrossChainDomain("test"), CrossChainIdentity.fromHexStr("0000000000000000000000000000000000000000000000000000000000000001"), CrossChainIdentity.fromHexStr("0000000000000000000000000000000000000000000000000000000000000001")); + var tpbta = new ThirdPartyBlockchainTrustAnchorV1( + 1, + BigInteger.ONE, + new PTCCredentialSubject( + "1", + "ptc", + PTCTypeEnum.COMMITTEE, + new X509PubkeyInfoObjectIdentity( + SignAlgoEnum.KECCAK256_WITH_SECP256K1.getSigner().generateKeyPair().getPublic().getEncoded() + ), + new byte[]{} + ), + crossChainLane, + 1, + HashAlgoEnum.KECCAK_256, + new CommitteeEndorseRoot( + "committee", + policy, + ListUtil.toList(nodeEndorseInfo) + ).encode(), + CommitteeEndorseProof.builder() + .committeeId("committee") + .sigs(ListUtil.toList(new CommitteeNodeProof( + "test", + SignAlgoEnum.KECCAK256_WITH_SECP256K1, + "".getBytes() + ))).build().encode() + ); + var tpBtaWrapper = new TpBtaWrapper(tpbta); + + endorseServiceRepository.setTpBta(tpBtaWrapper); + + Assert.assertNotNull(endorseServiceRepository.getMatchedTpBta(crossChainLane)); + Assert.assertTrue(endorseServiceRepository.hasTpBta(crossChainLane, 1)); + Assert.assertNotNull(endorseServiceRepository.getMatchedTpBta(crossChainLane)); + } + + @Test + public void testValidatedConsensusState() { + var cs = new ConsensusState( + new CrossChainDomain("test"), + BigInteger.valueOf(100L), + RandomUtil.randomBytes(32), + RandomUtil.randomBytes(32), + System.currentTimeMillis(), + new byte[]{}, + new byte[]{}, + new byte[]{} + ); + var vcs = BeanUtil.copyProperties(cs, ValidatedConsensusStateV1.class); + + endorseServiceRepository.setValidatedConsensusState(new ValidatedConsensusStateWrapper(vcs)); + + Assert.assertTrue(endorseServiceRepository.hasValidatedConsensusState("test", BigInteger.valueOf(100L))); + Assert.assertEquals( + cs.getHashHex(), + endorseServiceRepository.getValidatedConsensusState("test", BigInteger.valueOf(100L)).getValidatedConsensusState().getHashHex() + ); + Assert.assertEquals( + cs.getHeight(), + endorseServiceRepository.getValidatedConsensusState("test", BigInteger.valueOf(100L)).getHeight() + ); + Assert.assertEquals( + cs.getParentHashHex(), + endorseServiceRepository.getValidatedConsensusState("test", cs.getHashHex()).getParentHash() + ); + } +} diff --git a/acb-committeeptc/monitor-node/src/test/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/dal/SystemConfigRepositoryTest.java b/acb-committeeptc/monitor-node/src/test/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/dal/SystemConfigRepositoryTest.java new file mode 100755 index 00000000..52ca609f --- /dev/null +++ b/acb-committeeptc/monitor-node/src/test/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/dal/SystemConfigRepositoryTest.java @@ -0,0 +1,73 @@ +/* + * Copyright 2024 Ant Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alipay.antchain.bridge.ptc.committee.monitor.node.dal; + +import java.math.BigInteger; + +import cn.hutool.core.map.MapUtil; +import com.alipay.antchain.bridge.commons.core.base.CrossChainDomain; +import com.alipay.antchain.bridge.commons.core.ptc.PTCTrustRoot; +import com.alipay.antchain.bridge.commons.core.ptc.PTCVerifyAnchor; +import com.alipay.antchain.bridge.commons.utils.crypto.SignAlgoEnum; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.TestBase; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.dal.repository.interfaces.ISystemConfigRepository; +import jakarta.annotation.Resource; +import org.junit.Assert; +import org.junit.Test; + +public class SystemConfigRepositoryTest extends TestBase { + + @Resource + private ISystemConfigRepository systemConfigRepository; + + @Test + public void testPtcTrustRoot() { + var ptcCertObj = NODE_PTC_CERT; + + var va = new PTCVerifyAnchor(); + va.setAnchor(new byte[]{}); + va.setVersion(BigInteger.ONE); + + var trustRoot = new PTCTrustRoot(); + trustRoot.setSig(new byte[]{}); + trustRoot.setNetworkInfo(new byte[]{}); + trustRoot.setPtcCrossChainCert(ptcCertObj); + trustRoot.setIssuerBcdnsDomainSpace(new CrossChainDomain(CrossChainDomain.ROOT_DOMAIN_SPACE)); + trustRoot.setSigAlgo(SignAlgoEnum.KECCAK256_WITH_SECP256K1); + trustRoot.setVerifyAnchorMap(MapUtil.builder(BigInteger.ONE, va).build()); + + systemConfigRepository.setPtcTrustRoot(trustRoot); + + Assert.assertEquals( + ptcCertObj.getCredentialSubjectInstance().getApplicant(), + systemConfigRepository.getPtcTrustRoot().getPtcCredentialSubject().getApplicant() + ); + Assert.assertEquals( + BigInteger.ONE, + systemConfigRepository.queryCurrentPtcAnchorVersion() + ); + } + + @Test + public void testSystemConfig() { + systemConfigRepository.setSystemConfig("test", "test"); + Assert.assertEquals( + "test", + systemConfigRepository.getSystemConfig("test") + ); + } +} diff --git a/acb-committeeptc/monitor-node/src/test/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/server/AdminServiceImplTest.java b/acb-committeeptc/monitor-node/src/test/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/server/AdminServiceImplTest.java new file mode 100755 index 00000000..da250103 --- /dev/null +++ b/acb-committeeptc/monitor-node/src/test/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/server/AdminServiceImplTest.java @@ -0,0 +1,216 @@ +/* + * Copyright 2024 Ant Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alipay.antchain.bridge.ptc.committee.monitor.node.server; + +import java.math.BigInteger; +import java.util.concurrent.TimeUnit; + +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.json.JSONUtil; +import com.alipay.antchain.bridge.bcdns.factory.BlockChainDomainNameServiceFactory; +import com.alipay.antchain.bridge.bcdns.service.BCDNSTypeEnum; +import com.alipay.antchain.bridge.commons.bcdns.AbstractCrossChainCertificate; +import com.alipay.antchain.bridge.commons.bcdns.utils.CrossChainCertificateUtil; +import com.alipay.antchain.bridge.commons.core.base.CrossChainDomain; +import com.alipay.antchain.bridge.commons.core.ptc.PTCTrustRoot; +import com.alipay.antchain.bridge.commons.core.ptc.PTCVerifyAnchor; +import com.alipay.antchain.bridge.commons.utils.crypto.SignAlgoEnum; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.TestBase; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.commons.enums.BCDNSStateEnum; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.dal.repository.interfaces.ISystemConfigRepository; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.server.grpc.*; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.service.IBCDNSManageService; +import com.google.protobuf.ByteString; +import io.grpc.internal.testing.StreamRecorder; +import jakarta.annotation.Resource; +import lombok.SneakyThrows; +import org.junit.Assert; +import org.junit.Test; +import org.mockito.MockedStatic; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.notNull; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.when; + +public class AdminServiceImplTest extends TestBase { + + private static final AbstractCrossChainCertificate BCDNS_ROOT_CERT_OBJ = CrossChainCertificateUtil.readCrossChainCertificateFromPem(BCDNS_ROOT_CERT.getBytes()); + + @Resource + private AdminServiceImpl adminService; + + @Resource + private IBCDNSManageService bcdnsManageService; + + @Resource + private ISystemConfigRepository systemConfigRepository; + + @Test + @SneakyThrows + public void testRegisterBcdnsService() { + + MockedStatic mockStaticObj = null; + try { + mockStaticObj = mockStatic(BlockChainDomainNameServiceFactory.class); + when(BlockChainDomainNameServiceFactory.create(notNull(), any())).thenReturn(new DummyBcdnsServiceImpl()); + + StreamRecorder responseObserver = StreamRecorder.create(); + adminService.registerBcdnsService( + RegisterBcdnsServiceRequest.newBuilder() + .setBcdnsType(BCDNSTypeEnum.EMBEDDED.getCode()) + .setBcdnsRootCert(BCDNS_ROOT_CERT) + .setDomainSpace(CrossChainDomain.ROOT_DOMAIN_SPACE) + .setConfig(ByteString.empty()) + .build(), + responseObserver + ); + Assert.assertTrue(responseObserver.awaitCompletion(5, TimeUnit.SECONDS)); + Assert.assertNull(responseObserver.getError()); + + var results = responseObserver.getValues(); + Assert.assertEquals(1, results.size()); + Assert.assertEquals(0, results.getFirst().getCode()); + + Assert.assertTrue(bcdnsManageService.hasBCDNSServiceData(CrossChainDomain.ROOT_DOMAIN_SPACE)); + Assert.assertEquals(1, bcdnsManageService.countBCDNSService()); + Assert.assertNotNull(bcdnsManageService.getBCDNSClient(CrossChainDomain.ROOT_DOMAIN_SPACE)); + } finally { + if (ObjectUtil.isNotNull(mockStaticObj)) { + mockStaticObj.close(); + } + } + } + + @Test + @SneakyThrows + public void testGetBcdnsServiceInfo() { + MockedStatic mockStaticObj = null; + try { + mockStaticObj = mockStatic(BlockChainDomainNameServiceFactory.class); + when(BlockChainDomainNameServiceFactory.create(notNull(), any())).thenReturn(new DummyBcdnsServiceImpl()); + bcdnsManageService.registerBCDNSService( + CrossChainDomain.ROOT_DOMAIN_SPACE, + BCDNSTypeEnum.EMBEDDED, + "{}".getBytes(), + BCDNS_ROOT_CERT_OBJ + ); + StreamRecorder responseObserver = StreamRecorder.create(); + adminService.getBcdnsServiceInfo( + GetBcdnsServiceInfoRequest.newBuilder() + .setDomainSpace(CrossChainDomain.ROOT_DOMAIN_SPACE) + .build(), + responseObserver + ); + Assert.assertTrue(responseObserver.awaitCompletion(5, TimeUnit.SECONDS)); + Assert.assertNull(responseObserver.getError()); + + var results = responseObserver.getValues(); + Assert.assertEquals(1, results.size()); + Assert.assertEquals(0, results.getFirst().getCode()); + + Assert.assertTrue(JSONUtil.isTypeJSON(results.getFirst().getGetBcdnsServiceInfoResp().getInfoJson())); + } finally { + if (ObjectUtil.isNotNull(mockStaticObj)) { + mockStaticObj.close(); + } + } + } + + @Test + @SneakyThrows + public void testStopAndRestart() { + MockedStatic mockStaticObj = null; + try { + mockStaticObj = mockStatic(BlockChainDomainNameServiceFactory.class); + when(BlockChainDomainNameServiceFactory.create(notNull(), any())).thenReturn(new DummyBcdnsServiceImpl()); + bcdnsManageService.registerBCDNSService( + CrossChainDomain.ROOT_DOMAIN_SPACE, + BCDNSTypeEnum.EMBEDDED, + "{}".getBytes(), + BCDNS_ROOT_CERT_OBJ + ); + StreamRecorder responseObserver = StreamRecorder.create(); + adminService.stopBcdnsService( + StopBcdnsServiceRequest.newBuilder() + .setDomainSpace(CrossChainDomain.ROOT_DOMAIN_SPACE) + .build(), + responseObserver + ); + Assert.assertTrue(responseObserver.awaitCompletion(5, TimeUnit.SECONDS)); + Assert.assertNull(responseObserver.getError()); + + var results = responseObserver.getValues(); + Assert.assertEquals(1, results.size()); + Assert.assertEquals(0, results.getFirst().getCode()); + + Assert.assertEquals( + BCDNSStateEnum.FROZEN, + bcdnsManageService.getBCDNSServiceData(CrossChainDomain.ROOT_DOMAIN_SPACE).getState() + ); + + adminService.restartBcdnsService( + RestartBcdnsServiceRequest.newBuilder() + .setDomainSpace(CrossChainDomain.ROOT_DOMAIN_SPACE) + .build(), + responseObserver + ); + + Assert.assertEquals( + BCDNSStateEnum.WORKING, + bcdnsManageService.getBCDNSServiceData(CrossChainDomain.ROOT_DOMAIN_SPACE).getState() + ); + + adminService.deleteBcdnsService( + DeleteBcdnsServiceRequest.newBuilder().setDomainSpace(CrossChainDomain.ROOT_DOMAIN_SPACE).build(), + responseObserver + ); + Assert.assertFalse( + bcdnsManageService.hasBCDNSServiceData(CrossChainDomain.ROOT_DOMAIN_SPACE) + ); + + var ptcCertObj = NODE_PTC_CERT; + + var va = new PTCVerifyAnchor(); + va.setAnchor(new byte[]{}); + va.setVersion(BigInteger.ONE); + + var trustRoot = new PTCTrustRoot(); + trustRoot.setSig(new byte[]{}); + trustRoot.setNetworkInfo(new byte[]{}); + trustRoot.setPtcCrossChainCert(ptcCertObj); + trustRoot.setIssuerBcdnsDomainSpace(new CrossChainDomain(CrossChainDomain.ROOT_DOMAIN_SPACE)); + trustRoot.setSigAlgo(SignAlgoEnum.KECCAK256_WITH_SECP256K1); + trustRoot.setVerifyAnchorMap(MapUtil.builder(BigInteger.ONE, va).build()); + + adminService.addPtcTrustRoot( + AddPtcTrustRootRequest.newBuilder().setRawTrustRoot(ByteString.copyFrom(trustRoot.encode())).build(), + responseObserver + ); + + Assert.assertNotNull( + systemConfigRepository.getPtcTrustRoot() + ); + + } finally { + if (ObjectUtil.isNotNull(mockStaticObj)) { + mockStaticObj.close(); + } + } + } +} diff --git a/acb-committeeptc/monitor-node/src/test/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/server/MonitorNodeServiceTest.java b/acb-committeeptc/monitor-node/src/test/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/server/MonitorNodeServiceTest.java new file mode 100755 index 00000000..e48e5028 --- /dev/null +++ b/acb-committeeptc/monitor-node/src/test/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/server/MonitorNodeServiceTest.java @@ -0,0 +1,580 @@ +/* + * Copyright 2024 Ant Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alipay.antchain.bridge.ptc.committee.monitor.node.server; + +import java.math.BigInteger; +import java.util.concurrent.TimeUnit; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.ListUtil; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.RandomUtil; +import com.alipay.antchain.bridge.commons.bcdns.AbstractCrossChainCertificate; +import com.alipay.antchain.bridge.commons.bcdns.PTCCredentialSubject; +import com.alipay.antchain.bridge.commons.bcdns.utils.CrossChainCertificateUtil; +import com.alipay.antchain.bridge.commons.core.am.AuthMessageFactory; +import com.alipay.antchain.bridge.commons.core.base.*; +import com.alipay.antchain.bridge.commons.core.bta.BlockchainTrustAnchorV1; +import com.alipay.antchain.bridge.commons.core.ptc.*; +import com.alipay.antchain.bridge.commons.core.sdp.SDPMessageFactory; +import com.alipay.antchain.bridge.commons.core.monitor.MonitorMessageFactory; +import com.alipay.antchain.bridge.commons.utils.crypto.HashAlgoEnum; +import com.alipay.antchain.bridge.commons.utils.crypto.SignAlgoEnum; +import com.alipay.antchain.bridge.plugins.spi.ptc.IHeteroChainDataVerifierService; +import com.alipay.antchain.bridge.plugins.spi.ptc.core.VerifyResult; +import com.alipay.antchain.bridge.ptc.committee.grpc.*; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.TestBase; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.commons.models.BtaWrapper; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.commons.models.DomainSpaceCertWrapper; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.commons.models.TpBtaWrapper; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.commons.models.ValidatedConsensusStateWrapper; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.dal.repository.interfaces.IBCDNSRepository; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.dal.repository.interfaces.IEndorseServiceRepository; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.dal.repository.interfaces.ISystemConfigRepository; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.service.IEndorserService; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.service.IHcdvsPluginService; +import com.alipay.antchain.bridge.ptc.committee.types.basic.CommitteeEndorseProof; +import com.alipay.antchain.bridge.ptc.committee.types.basic.CommitteeNodeProof; +import com.alipay.antchain.bridge.ptc.committee.types.basic.EndorseBlockStateResp; +import com.alipay.antchain.bridge.ptc.committee.types.basic.NodePublicKeyEntry; +import com.alipay.antchain.bridge.ptc.committee.types.tpbta.CommitteeEndorseRoot; +import com.alipay.antchain.bridge.ptc.committee.types.tpbta.NodeEndorseInfo; +import com.alipay.antchain.bridge.ptc.committee.types.tpbta.OptionalEndorsePolicy; +import com.alipay.antchain.bridge.ptc.committee.types.tpbta.VerifyBtaExtension; +import com.google.protobuf.ByteString; +import io.grpc.internal.testing.StreamRecorder; +import jakarta.annotation.Resource; +import lombok.SneakyThrows; +import org.junit.Assert; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.mock.mockito.MockBean; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class MonitorNodeServiceTest extends TestBase { + + private static final ThirdPartyBlockchainTrustAnchorV1 tpbta; + + private static final CrossChainLane crossChainLane; + + private static final ObjectIdentity oid; + + private static final BlockchainTrustAnchorV1 bta; + + private static final AbstractCrossChainCertificate domainCert; + + private static final ConsensusState anchorState; + + private static final ConsensusState currState; + + private static final UniformCrosschainPacket ucp; + + static { + oid = new X509PubkeyInfoObjectIdentity( + RAW_NODE_PTC_PUBLIC_KEY + ); + + var policy = new OptionalEndorsePolicy(); + policy.setThreshold(new OptionalEndorsePolicy.Threshold(OptionalEndorsePolicy.OperatorEnum.GREATER_THAN, -1)); + var nodeEndorseInfo = new NodeEndorseInfo(); + nodeEndorseInfo.setNodeId("monitor-node"); + nodeEndorseInfo.setRequired(true); + var nodePubkeyEntry = new NodePublicKeyEntry("default", ((X509PubkeyInfoObjectIdentity) oid).getPublicKey()); + nodeEndorseInfo.setPublicKey(nodePubkeyEntry); + crossChainLane = new CrossChainLane(new CrossChainDomain("test"), new CrossChainDomain("test"), CrossChainIdentity.fromHexStr("0000000000000000000000000000000000000000000000000000000000000001"), CrossChainIdentity.fromHexStr("0000000000000000000000000000000000000000000000000000000000000001")); + tpbta = new ThirdPartyBlockchainTrustAnchorV1( + 1, + BigInteger.ONE, + (PTCCredentialSubject) NODE_PTC_CERT.getCredentialSubjectInstance(), + crossChainLane, + 1, + HashAlgoEnum.KECCAK_256, + new CommitteeEndorseRoot( + "1", + policy, + ListUtil.toList(nodeEndorseInfo) + ).encode(), + CommitteeEndorseProof.builder() + .committeeId("1") + .sigs(ListUtil.toList(new CommitteeNodeProof( + "test", + SignAlgoEnum.KECCAK256_WITH_SECP256K1, + "".getBytes() + ))).build().encode() + ); + + bta = new BlockchainTrustAnchorV1(); + bta.setBcOwnerPublicKey(RAW_NODE_PTC_PUBLIC_KEY); + bta.setDomain(new CrossChainDomain("ether.com")); + bta.setSubjectIdentity("test".getBytes()); + bta.setBcOwnerSigAlgo(SignAlgoEnum.KECCAK256_WITH_SECP256K1); + bta.setPtcOid(NODE_PTC_CERT.getCredentialSubjectInstance().getApplicant()); + bta.setSubjectProduct("ethereum2"); + bta.setInitHeight(BigInteger.valueOf(100L)); + bta.setInitBlockHash(RandomUtil.randomBytes(32)); + bta.setExtension( + new VerifyBtaExtension( + CommitteeEndorseRoot.decode(tpbta.getEndorseRoot()), + crossChainLane + ).encode() + ); + bta.setSubjectVersion(0); + bta.setAmId(RandomUtil.randomBytes(32)); + + bta.sign(NODE_PTC_PRIVATE_KEY); + + domainCert = CrossChainCertificateUtil.readCrossChainCertificateFromPem(ANTCHAIN_DOT_COM_CERT.getBytes()); + + anchorState = new ConsensusState( + crossChainLane.getSenderDomain(), + BigInteger.valueOf(100L), + bta.getInitBlockHash(), + RandomUtil.randomBytes(32), + System.currentTimeMillis(), + "{}".getBytes(), + "{}".getBytes(), + "{}".getBytes() + ); + + currState = new ConsensusState( + crossChainLane.getSenderDomain(), + BigInteger.valueOf(101L), + RandomUtil.randomBytes(32), + anchorState.getParentHash(), + System.currentTimeMillis(), + "{}".getBytes(), + "{}".getBytes(), + "{}".getBytes() + ); + + // need to replace "awesome antchain-bridge" with monitor message + var monitorMessage = MonitorMessageFactory.createMonitorMessage( + 1, + 2, + "this is a monitorMsg", + "awesome antchain-bridge".getBytes() + ); + + var sdpMessage = SDPMessageFactory.createSDPMessage( + 1, + new byte[32], + crossChainLane.getReceiverDomain().getDomain(), + crossChainLane.getReceiverId().getRawID(), + -1, + monitorMessage.encode() + ); + + var am = AuthMessageFactory.createAuthMessage( + 1, + crossChainLane.getSenderId().getRawID(), + 0, + sdpMessage.encode() + ); + + ucp = new UniformCrosschainPacket( + crossChainLane.getSenderDomain(), + CrossChainMessage.createCrossChainMessage( + CrossChainMessage.CrossChainMessageType.AUTH_MSG, + BigInteger.valueOf(101L), + DateUtil.current(), + currState.getHash(), + am.encode(), + "event".getBytes(), + "merkle proof".getBytes(), + RandomUtil.randomBytes(32) + ), + NODE_PTC_CERT.getCredentialSubjectInstance().getApplicant() + ); + } + + @Value("${committee.id}") + private String committeeId; + + @Value("${committee.node.id}") + private String nodeId; + + @Resource + private IEndorserService endorserService; + + @MockBean + private IEndorseServiceRepository endorseServiceRepository; + + @MockBean + private IBCDNSRepository bcdnsRepository; + + @MockBean + private ISystemConfigRepository systemConfigRepository; + + @MockBean + private IHcdvsPluginService hcdvsPluginService; + + @Resource + private AbstractCrossChainCertificate ptcCrossChainCert; + + @Resource + private MonitorNodeServiceImpl committeeNodeService; + + @Test + @SneakyThrows + public void testQueryTpBta() { + when(endorseServiceRepository.getMatchedTpBta(any())).thenReturn(new TpBtaWrapper(tpbta)); + + StreamRecorder responseObserver = StreamRecorder.create(); + committeeNodeService.queryTpBta( + QueryTpBtaRequest.newBuilder() + .setSenderDomain(crossChainLane.getSenderDomain().getDomain()) + .setSenderId(crossChainLane.getSenderIdHex()) + .setReceiverDomain(crossChainLane.getReceiverDomain().getDomain()) + .setReceiverId(crossChainLane.getReceiverIdHex()) + .build(), + responseObserver + ); + Assert.assertTrue(responseObserver.awaitCompletion(5, TimeUnit.SECONDS)); + Assert.assertNull(responseObserver.getError()); + var results = responseObserver.getValues(); + Assert.assertEquals(1, results.size()); + Assert.assertArrayEquals( + tpbta.encode(), + results.getFirst().getQueryTpBtaResp().getRawTpBta().toByteArray() + ); + } + + @Test + @SneakyThrows + public void testHeartbeat() { + when(hcdvsPluginService.getAvailableProducts()).thenReturn(ListUtil.toList("mychain")); + + StreamRecorder responseObserver = StreamRecorder.create(); + committeeNodeService.heartbeat( + HeartbeatRequest.newBuilder().build(), + responseObserver + ); + + Assert.assertTrue(responseObserver.awaitCompletion(5, TimeUnit.SECONDS)); + Assert.assertNull(responseObserver.getError()); + + var results = responseObserver.getValues(); + Assert.assertEquals(1, results.size()); + + Assert.assertEquals(committeeId, results.getFirst().getHeartbeatResp().getCommitteeId()); + Assert.assertEquals("monitor-node", results.getFirst().getHeartbeatResp().getNodeId()); + Assert.assertEquals(1, results.getFirst().getHeartbeatResp().getProductsCount()); + Assert.assertEquals("mychain", results.getFirst().getHeartbeatResp().getProductsList().getFirst()); + } + + @Test + @SneakyThrows + public void testVerifyBta() { + + when(bcdnsRepository.getDomainSpaceCert(anyString())).thenReturn( + new DomainSpaceCertWrapper( + CrossChainCertificateUtil.readCrossChainCertificateFromPem(DOT_COM_DOMAIN_SPACE_CERT.getBytes()) + ) + ); + + when(systemConfigRepository.queryCurrentPtcAnchorVersion()).thenReturn(BigInteger.ONE); + StreamRecorder responseObserver = StreamRecorder.create(); + committeeNodeService.verifyBta( + VerifyBtaRequest.newBuilder() + .setRawBta(ByteString.copyFrom(bta.encode())) + .setRawDomainCert(ByteString.copyFrom(domainCert.encode())) + .build(), + responseObserver + ); + + Assert.assertTrue(responseObserver.awaitCompletion(5, TimeUnit.SECONDS)); + Assert.assertNull(responseObserver.getError()); + + var results = responseObserver.getValues(); + Assert.assertEquals(1, results.size()); + Assert.assertEquals(0, results.getFirst().getCode()); + + var tpbtaInResp = ThirdPartyBlockchainTrustAnchor.decode(results.getFirst().getVerifyBtaResp().getRawTpBta().toByteArray()); + Assert.assertTrue( + ArrayUtil.equals( + tpbta.getEndorseRoot(), + tpbtaInResp.getEndorseRoot() + ) + ); + Assert.assertEquals( + tpbta.getCrossChainLane().getLaneKey(), + tpbtaInResp.getCrossChainLane().getLaneKey() + ); + Assert.assertTrue( + ArrayUtil.equals( + ptcCrossChainCert.getCredentialSubject(), + tpbtaInResp.getSignerPtcCredentialSubject().encode() + ) + ); + + var endorseProof = CommitteeEndorseProof.decode(tpbtaInResp.getEndorseProof()); + Assert.assertEquals( + committeeId, + endorseProof.getCommitteeId() + ); + + var endorseRoot = CommitteeEndorseRoot.decode(tpbtaInResp.getEndorseRoot()); + Assert.assertEquals( + 1, + endorseRoot.getEndorsers().size() + ); + Assert.assertEquals( + nodeId, + endorseRoot.getEndorsers().getFirst().getNodeId() + ); + Assert.assertEquals( + "default", + endorseRoot.getEndorsers().getFirst().getPublicKey().getKeyId() + ); + Assert.assertTrue( + endorseRoot.check(endorseProof, tpbtaInResp.getEncodedToSign()) + ); + } + + @Test + @SneakyThrows + public void testCommitAnchorState() { + var hcdvs = mock(IHeteroChainDataVerifierService.class); + when(hcdvs.verifyAnchorConsensusState(any(), any())).thenReturn(VerifyResult.builder().success(true).build()); + when(endorseServiceRepository.getMatchedTpBta(any())).thenReturn(new TpBtaWrapper(tpbta)); + when(endorseServiceRepository.getBta(anyString())).thenReturn(new BtaWrapper(bta)); + when(hcdvsPluginService.getHCDVSService(anyString())).thenReturn(hcdvs); + + var vcs = BeanUtil.copyProperties(anchorState, ValidatedConsensusStateV1.class); + vcs.setPtcOid(ptcCrossChainCert.getCredentialSubjectInstance().getApplicant()); + vcs.setTpbtaVersion(tpbta.getTpbtaVersion()); + vcs.setPtcType(PTCTypeEnum.COMMITTEE); + + StreamRecorder responseObserver = StreamRecorder.create(); + committeeNodeService.commitAnchorState( + CommitAnchorStateRequest.newBuilder() + .setCrossChainLane(ByteString.copyFrom(crossChainLane.encode())) + .setRawAnchorState(ByteString.copyFrom(anchorState.encode())) + .build(), + responseObserver + ); + + Assert.assertTrue(responseObserver.awaitCompletion(5, TimeUnit.SECONDS)); + Assert.assertNull(responseObserver.getError()); + + var results = responseObserver.getValues(); + Assert.assertEquals(1, results.size()); + Assert.assertEquals(0, results.getFirst().getCode()); + + var vcsSigned = ValidatedConsensusState.decode(results.getFirst().getCommitAnchorStateResp().getRawValidatedConsensusState().toByteArray()); + Assert.assertTrue( + new TpBtaWrapper(tpbta).getEndorseRoot().check( + CommitteeEndorseProof.decode(vcsSigned.getPtcProof()), + vcs.getEncodedToSign() + ) + ); + } + + @Test + @SneakyThrows + public void testCommitConsensusState() { + var vcs = BeanUtil.copyProperties(anchorState, ValidatedConsensusStateV1.class); + vcs.setPtcOid(ptcCrossChainCert.getCredentialSubjectInstance().getApplicant()); + vcs.setTpbtaVersion(tpbta.getTpbtaVersion()); + vcs.setPtcType(PTCTypeEnum.COMMITTEE); + + var currVcs = BeanUtil.copyProperties(currState, ValidatedConsensusStateV1.class); + currVcs.setPtcOid(ptcCrossChainCert.getCredentialSubjectInstance().getApplicant()); + currVcs.setTpbtaVersion(tpbta.getTpbtaVersion()); + currVcs.setPtcType(PTCTypeEnum.COMMITTEE); + + var hcdvs = mock(IHeteroChainDataVerifierService.class); + when(hcdvs.verifyConsensusState(any(), any())).thenReturn(VerifyResult.builder().success(true).build()); + when(endorseServiceRepository.getMatchedTpBta(any())).thenReturn(new TpBtaWrapper(tpbta)); + when(endorseServiceRepository.getBta(anyString())).thenReturn(new BtaWrapper(bta)); + when(endorseServiceRepository.getValidatedConsensusState(anyString(), anyString())).thenReturn(new ValidatedConsensusStateWrapper(vcs)); + when(hcdvsPluginService.getHCDVSService(anyString())).thenReturn(hcdvs); + + StreamRecorder responseObserver = StreamRecorder.create(); + committeeNodeService.commitConsensusState( + CommitConsensusStateRequest.newBuilder() + .setCrossChainLane(ByteString.copyFrom(crossChainLane.encode())) + .setRawConsensusState(ByteString.copyFrom(currState.encode())) + .build(), + responseObserver + ); + + Assert.assertTrue(responseObserver.awaitCompletion(5, TimeUnit.SECONDS)); + Assert.assertNull(responseObserver.getError()); + + var results = responseObserver.getValues(); + Assert.assertEquals(1, results.size()); + Assert.assertEquals(0, results.getFirst().getCode()); + + var vcsSigned = ValidatedConsensusState.decode(results.getFirst().getCommitConsensusStateResp().getRawValidatedConsensusState().toByteArray()); + Assert.assertTrue( + new TpBtaWrapper(tpbta).getEndorseRoot().check( + CommitteeEndorseProof.decode(vcsSigned.getPtcProof()), + currVcs.getEncodedToSign() + ) + ); + } + + @Test + @SneakyThrows + public void testVerifyCrossChainMessage() { + var currVcs = BeanUtil.copyProperties(currState, ValidatedConsensusStateV1.class); + currVcs.setPtcOid(ptcCrossChainCert.getCredentialSubjectInstance().getApplicant()); + currVcs.setTpbtaVersion(tpbta.getTpbtaVersion()); + currVcs.setPtcType(PTCTypeEnum.COMMITTEE); + + var hcdvs = mock(IHeteroChainDataVerifierService.class); + when(hcdvs.verifyCrossChainMessage(any(), any())).thenReturn(VerifyResult.builder().success(true).build()); + when(hcdvs.parseMessageFromLedgerData(any())).thenReturn(ucp.getSrcMessage().getMessage()); + when(endorseServiceRepository.getExactTpBta(any())).thenReturn(new TpBtaWrapper(tpbta)); + when(endorseServiceRepository.getBta(anyString())).thenReturn(new BtaWrapper(bta)); + when(endorseServiceRepository.getValidatedConsensusState(anyString(), anyString())).thenReturn(new ValidatedConsensusStateWrapper(currVcs)); + when(hcdvsPluginService.getHCDVSService(anyString())).thenReturn(hcdvs); + + StreamRecorder responseObserver = StreamRecorder.create(); + committeeNodeService.verifyCrossChainMessage( + VerifyCrossChainMessageRequest.newBuilder() + .setCrossChainLane(ByteString.copyFrom(crossChainLane.encode())) + .setRawUcp(ByteString.copyFrom(ucp.encode())) + .build(), + responseObserver + ); + + Assert.assertTrue(responseObserver.awaitCompletion(5, TimeUnit.SECONDS)); + Assert.assertNull(responseObserver.getError()); + + var results = responseObserver.getValues(); + Assert.assertEquals(1, results.size()); + Assert.assertEquals(0, results.getFirst().getCode()); + + var nodeProof = CommitteeNodeProof.decode(results.getFirst().getVerifyCrossChainMessageResp().getRawNodeProof().toByteArray()); + Assert.assertTrue( + new TpBtaWrapper(tpbta).getEndorseRoot().check( + CommitteeEndorseProof.builder() + .committeeId(committeeId) + .sigs(ListUtil.toList(nodeProof)) + .build(), + ThirdPartyProof.create( + tpbta.getTpbtaVersion(), + ucp.getSrcMessage().getMessage(), + crossChainLane + ).getEncodedToSign() + ) + ); + } + + @Test + @SneakyThrows + public void testQueryBlockState() { + var currVcs = BeanUtil.copyProperties(currState, ValidatedConsensusStateV1.class); + currVcs.setPtcOid(ptcCrossChainCert.getCredentialSubjectInstance().getApplicant()); + currVcs.setTpbtaVersion(tpbta.getTpbtaVersion()); + currVcs.setPtcType(PTCTypeEnum.COMMITTEE); + + when(endorseServiceRepository.getLatestValidatedConsensusState(anyString())).thenReturn(new ValidatedConsensusStateWrapper(currVcs)); + + StreamRecorder responseObserver = StreamRecorder.create(); + committeeNodeService.queryBlockState( + QueryBlockStateRequest.newBuilder() + .setDomain(currVcs.getDomain().toString()) + .build(), + responseObserver + ); + + Assert.assertTrue(responseObserver.awaitCompletion(5, TimeUnit.SECONDS)); + Assert.assertNull(responseObserver.getError()); + + var results = responseObserver.getValues(); + Assert.assertEquals(1, results.size()); + Assert.assertEquals(0, results.getFirst().getCode()); + + var blockState = BlockState.decode(results.getFirst().getQueryBlockStateResp().getRawValidatedBlockState().toByteArray()); + Assert.assertEquals(currVcs.getHeight(), blockState.getHeight()); + Assert.assertEquals(currVcs.getDomain(), blockState.getDomain()); + Assert.assertEquals(currVcs.getStateTimestamp(), blockState.getTimestamp()); + Assert.assertArrayEquals(currVcs.getHash(), blockState.getHash()); + } + + @Test + @SneakyThrows + public void testEndorseBlockState() { + var currVcs = BeanUtil.copyProperties(currState, ValidatedConsensusStateV1.class); + currVcs.setPtcOid(ptcCrossChainCert.getCredentialSubjectInstance().getApplicant()); + currVcs.setTpbtaVersion(tpbta.getTpbtaVersion()); + currVcs.setPtcType(PTCTypeEnum.COMMITTEE); + + when(endorseServiceRepository.getExactTpBta(any())).thenReturn(new TpBtaWrapper(tpbta)); + when(endorseServiceRepository.getValidatedConsensusState(anyString(), any(BigInteger.class))).thenReturn(new ValidatedConsensusStateWrapper(currVcs)); + when(endorseServiceRepository.hasValidatedConsensusState(anyString(), any())).thenReturn(true); + + StreamRecorder responseObserver = StreamRecorder.create(); + committeeNodeService.endorseBlockState( + EndorseBlockStateRequest.newBuilder() + .setCrossChainLane(ByteString.copyFrom(crossChainLane.encode())) + .setHeight(currState.getHeight().toString()) + .setReceiverDomain("receiverdomain") + .build(), + responseObserver + ); + + Assert.assertTrue(responseObserver.awaitCompletion(5, TimeUnit.SECONDS)); + Assert.assertNull(responseObserver.getError()); + + var results = responseObserver.getValues(); + Assert.assertEquals(1, results.size()); + Assert.assertEquals(0, results.getFirst().getCode()); + + var resp = new EndorseBlockStateResp( + AuthMessageFactory.createAuthMessage(results.getFirst().getEndorseBlockStateResp().getBlockStateAuthMsg().toByteArray()), + CommitteeNodeProof.decode(results.getFirst().getEndorseBlockStateResp().getCommitteeNodeProof().toByteArray()) + ); + Assert.assertEquals( + "receiverdomain", + SDPMessageFactory.createSDPMessage(resp.getBlockStateAuthMsg().getPayload()).getTargetDomain().getDomain() + ); + Assert.assertArrayEquals( + currVcs.getHash(), + resp.getBlockState().getHash() + ); + Assert.assertEquals( + currVcs.getHeight(), + resp.getBlockState().getHeight() + ); + Assert.assertEquals( + currVcs.getStateTimestamp(), + resp.getBlockState().getTimestamp() + ); + Assert.assertTrue( + new TpBtaWrapper(tpbta).getEndorseRoot().check( + CommitteeEndorseProof.builder() + .committeeId(committeeId) + .sigs(ListUtil.toList(resp.getCommitteeNodeProof())) + .build(), + ThirdPartyProof.create( + tpbta.getTpbtaVersion(), + resp.getBlockStateAuthMsg().encode(), + crossChainLane + ).getEncodedToSign() + ) + ); + } +} diff --git a/acb-committeeptc/monitor-node/src/test/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/server/MonitorOrderServiceTest.java b/acb-committeeptc/monitor-node/src/test/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/server/MonitorOrderServiceTest.java new file mode 100755 index 00000000..415a4c1a --- /dev/null +++ b/acb-committeeptc/monitor-node/src/test/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/server/MonitorOrderServiceTest.java @@ -0,0 +1,4 @@ +package com.alipay.antchain.bridge.ptc.committee.monitor.node.server; + +public class MonitorOrderServiceTest { +} diff --git a/acb-committeeptc/monitor-node/src/test/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/service/BCDNSManageServiceTest.java b/acb-committeeptc/monitor-node/src/test/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/service/BCDNSManageServiceTest.java new file mode 100755 index 00000000..af0ad4b5 --- /dev/null +++ b/acb-committeeptc/monitor-node/src/test/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/service/BCDNSManageServiceTest.java @@ -0,0 +1,133 @@ +/* + * Copyright 2024 Ant Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alipay.antchain.bridge.ptc.committee.monitor.node.service; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.util.ObjectUtil; +import com.alipay.antchain.bridge.bcdns.factory.BlockChainDomainNameServiceFactory; +import com.alipay.antchain.bridge.bcdns.service.BCDNSTypeEnum; +import com.alipay.antchain.bridge.commons.bcdns.utils.CrossChainCertificateUtil; +import com.alipay.antchain.bridge.commons.core.base.CrossChainDomain; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.TestBase; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.commons.enums.BCDNSStateEnum; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.commons.models.BCDNSServiceDO; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.commons.models.DomainSpaceCertWrapper; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.dal.repository.interfaces.IBCDNSRepository; +import jakarta.annotation.Resource; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; +import org.mockito.MockedStatic; +import org.springframework.boot.test.mock.mockito.MockBean; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.notNull; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.when; + +public class BCDNSManageServiceTest extends TestBase { + + private static MockedStatic mockStaticObj = null; + + @BeforeClass + public static void beforeClass() throws Exception { + try { + mockStaticObj = mockStatic(BlockChainDomainNameServiceFactory.class); + when(BlockChainDomainNameServiceFactory.create(notNull(), any())).thenReturn(new DummyBcdnsServiceImpl()); + } catch (Exception e) { + if (ObjectUtil.isNotNull(mockStaticObj)) { + mockStaticObj.close(); + } + } + } + + @AfterClass + public static void afterClass() throws Exception { + if (ObjectUtil.isNotNull(mockStaticObj)) { + mockStaticObj.close(); + } + } + + private static BCDNSServiceDO bcdnsServiceDO; + + static { + var dsc = new DomainSpaceCertWrapper( + CrossChainCertificateUtil.readCrossChainCertificateFromPem(BCDNS_ROOT_CERT.getBytes()) + ); + bcdnsServiceDO = new BCDNSServiceDO( + CrossChainDomain.ROOT_DOMAIN_SPACE, + dsc.getOwnerOid(), + dsc, + BCDNSTypeEnum.EMBEDDED, + BCDNSStateEnum.WORKING, + "{}".getBytes() + ); + } + + @Resource + private IBCDNSManageService bcdnsManageService; + + @MockBean + private IBCDNSRepository bcdnsRepository; + + @Test + public void countBCDNSService() { + when(bcdnsRepository.countBCDNSService()).thenReturn(1L); + assertEquals( + 1, + bcdnsManageService.countBCDNSService() + ); + } + + @Test + public void getBCDNSClient() { + when(bcdnsRepository.getBCDNSServiceDO(any())).thenReturn(bcdnsServiceDO); + assertNotNull(bcdnsManageService.getBCDNSClient(CrossChainDomain.ROOT_DOMAIN_SPACE)); + } + + @Test + public void registerBCDNSService() { + bcdnsManageService.registerBCDNSService( + bcdnsServiceDO.getDomainSpace(), + bcdnsServiceDO.getType(), + bcdnsServiceDO.getProperties(), + bcdnsServiceDO.getDomainSpaceCertWrapper().getDomainSpaceCert() + ); + Assert.assertNotNull(bcdnsManageService.getBCDNSClient(CrossChainDomain.ROOT_DOMAIN_SPACE)); + } + + @Test + public void startBCDNSService() { + assertNotNull(bcdnsManageService.startBCDNSService(bcdnsServiceDO)); + } + + @Test + public void restartBCDNSService() { + var frozenBcdnsDO = BeanUtil.copyProperties(bcdnsServiceDO, BCDNSServiceDO.class); + frozenBcdnsDO.setState(BCDNSStateEnum.FROZEN); + when(bcdnsRepository.getBCDNSServiceDO(any())).thenReturn(frozenBcdnsDO); + bcdnsManageService.restartBCDNSService(frozenBcdnsDO.getDomainSpace()); + } + + @Test + public void stopBCDNSService() { + when(bcdnsRepository.hasBCDNSService(any())).thenReturn(true); + bcdnsManageService.stopBCDNSService(bcdnsServiceDO.getDomainSpace()); + } +} \ No newline at end of file diff --git a/acb-committeeptc/monitor-node/src/test/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/service/EndorserServiceTest.java b/acb-committeeptc/monitor-node/src/test/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/service/EndorserServiceTest.java new file mode 100755 index 00000000..9801c491 --- /dev/null +++ b/acb-committeeptc/monitor-node/src/test/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/service/EndorserServiceTest.java @@ -0,0 +1,401 @@ +/* + * Copyright 2024 Ant Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alipay.antchain.bridge.ptc.committee.monitor.node.service; + +import java.math.BigInteger; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.ListUtil; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.RandomUtil; +import com.alipay.antchain.bridge.commons.bcdns.AbstractCrossChainCertificate; +import com.alipay.antchain.bridge.commons.bcdns.PTCCredentialSubject; +import com.alipay.antchain.bridge.commons.bcdns.utils.CrossChainCertificateUtil; +import com.alipay.antchain.bridge.commons.core.am.AuthMessageFactory; +import com.alipay.antchain.bridge.commons.core.base.*; +import com.alipay.antchain.bridge.commons.core.bta.BlockchainTrustAnchorV1; +import com.alipay.antchain.bridge.commons.core.ptc.PTCTypeEnum; +import com.alipay.antchain.bridge.commons.core.ptc.ThirdPartyBlockchainTrustAnchorV1; +import com.alipay.antchain.bridge.commons.core.ptc.ThirdPartyProof; +import com.alipay.antchain.bridge.commons.core.ptc.ValidatedConsensusStateV1; +import com.alipay.antchain.bridge.commons.core.sdp.SDPMessageFactory; +import com.alipay.antchain.bridge.commons.utils.crypto.HashAlgoEnum; +import com.alipay.antchain.bridge.commons.utils.crypto.SignAlgoEnum; +import com.alipay.antchain.bridge.plugins.spi.ptc.IHeteroChainDataVerifierService; +import com.alipay.antchain.bridge.plugins.spi.ptc.core.VerifyResult; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.TestBase; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.commons.models.BtaWrapper; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.commons.models.DomainSpaceCertWrapper; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.commons.models.TpBtaWrapper; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.commons.models.ValidatedConsensusStateWrapper; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.dal.repository.interfaces.IBCDNSRepository; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.dal.repository.interfaces.IEndorseServiceRepository; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.dal.repository.interfaces.ISystemConfigRepository; +import com.alipay.antchain.bridge.ptc.committee.types.basic.CommitteeEndorseProof; +import com.alipay.antchain.bridge.ptc.committee.types.basic.CommitteeNodeProof; +import com.alipay.antchain.bridge.ptc.committee.types.basic.EndorseBlockStateResp; +import com.alipay.antchain.bridge.ptc.committee.types.basic.NodePublicKeyEntry; +import com.alipay.antchain.bridge.ptc.committee.types.tpbta.CommitteeEndorseRoot; +import com.alipay.antchain.bridge.ptc.committee.types.tpbta.NodeEndorseInfo; +import com.alipay.antchain.bridge.ptc.committee.types.tpbta.OptionalEndorsePolicy; +import com.alipay.antchain.bridge.ptc.committee.types.tpbta.VerifyBtaExtension; +import jakarta.annotation.Resource; +import org.junit.Assert; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.mock.mockito.MockBean; + +import static org.mockito.Mockito.*; + +public class EndorserServiceTest extends TestBase { + + private static final ThirdPartyBlockchainTrustAnchorV1 tpbta; + + private static final CrossChainLane crossChainLane; + + private static final ObjectIdentity oid; + + private static final BlockchainTrustAnchorV1 bta; + + private static final AbstractCrossChainCertificate domainCert; + + private static final ConsensusState anchorState; + + private static final ConsensusState currState; + + private static final UniformCrosschainPacket ucp; + + static { + oid = new X509PubkeyInfoObjectIdentity( + RAW_NODE_PTC_PUBLIC_KEY + ); + + var policy = new OptionalEndorsePolicy(); + policy.setThreshold(new OptionalEndorsePolicy.Threshold(OptionalEndorsePolicy.OperatorEnum.GREATER_THAN, -1)); + var nodeEndorseInfo = new NodeEndorseInfo(); + nodeEndorseInfo.setNodeId("node1"); + nodeEndorseInfo.setRequired(true); + var nodePubkeyEntry = new NodePublicKeyEntry("default", ((X509PubkeyInfoObjectIdentity) oid).getPublicKey()); + nodeEndorseInfo.setPublicKey(nodePubkeyEntry); + crossChainLane = new CrossChainLane(new CrossChainDomain("test"), new CrossChainDomain("test"), CrossChainIdentity.fromHexStr("0000000000000000000000000000000000000000000000000000000000000001"), CrossChainIdentity.fromHexStr("0000000000000000000000000000000000000000000000000000000000000001")); + tpbta = new ThirdPartyBlockchainTrustAnchorV1( + 1, + BigInteger.ONE, + (PTCCredentialSubject) NODE_PTC_CERT.getCredentialSubjectInstance(), + crossChainLane, + 1, + HashAlgoEnum.KECCAK_256, + new CommitteeEndorseRoot( + "1", + policy, + ListUtil.toList(nodeEndorseInfo) + ).encode(), + CommitteeEndorseProof.builder() + .committeeId("1") + .sigs(ListUtil.toList(new CommitteeNodeProof( + "test", + SignAlgoEnum.KECCAK256_WITH_SECP256K1, + "".getBytes() + ))).build().encode() + ); + + bta = new BlockchainTrustAnchorV1(); + bta.setBcOwnerPublicKey(RAW_NODE_PTC_PUBLIC_KEY); + bta.setDomain(new CrossChainDomain("antchain.com")); + bta.setSubjectIdentity("test".getBytes()); + bta.setBcOwnerSigAlgo(SignAlgoEnum.KECCAK256_WITH_SECP256K1); + bta.setPtcOid(NODE_PTC_CERT.getCredentialSubjectInstance().getApplicant()); + bta.setInitHeight(BigInteger.valueOf(100L)); + bta.setInitBlockHash(RandomUtil.randomBytes(32)); + bta.setSubjectProduct("mychain"); + bta.setExtension( + new VerifyBtaExtension( + CommitteeEndorseRoot.decode(tpbta.getEndorseRoot()), + crossChainLane + ).encode() + ); + bta.setSubjectVersion(0); + bta.setAmId(RandomUtil.randomBytes(32)); + + bta.sign(NODE_PTC_PRIVATE_KEY); + + domainCert = CrossChainCertificateUtil.readCrossChainCertificateFromPem(ANTCHAIN_DOT_COM_CERT.getBytes()); + + anchorState = new ConsensusState( + crossChainLane.getSenderDomain(), + BigInteger.valueOf(100L), + bta.getInitBlockHash(), + RandomUtil.randomBytes(32), + System.currentTimeMillis(), + "{}".getBytes(), + "{}".getBytes(), + "{}".getBytes() + ); + + currState = new ConsensusState( + crossChainLane.getSenderDomain(), + BigInteger.valueOf(101L), + RandomUtil.randomBytes(32), + anchorState.getParentHash(), + System.currentTimeMillis(), + "{}".getBytes(), + "{}".getBytes(), + "{}".getBytes() + ); + + var sdpMessage = SDPMessageFactory.createSDPMessage( + 1, + new byte[32], + crossChainLane.getReceiverDomain().getDomain(), + crossChainLane.getReceiverId().getRawID(), + -1, + "awesome antchain-bridge".getBytes() + ); + + var am = AuthMessageFactory.createAuthMessage( + 1, + crossChainLane.getSenderId().getRawID(), + 0, + sdpMessage.encode() + ); + + ucp = new UniformCrosschainPacket( + crossChainLane.getSenderDomain(), + CrossChainMessage.createCrossChainMessage( + CrossChainMessage.CrossChainMessageType.AUTH_MSG, + BigInteger.valueOf(101L), + DateUtil.current(), + currState.getHash(), + am.encode(), + "event".getBytes(), + "merkle proof".getBytes(), + RandomUtil.randomBytes(32) + ), + NODE_PTC_CERT.getCredentialSubjectInstance().getApplicant() + ); + } + + @Value("${committee.id}") + private String committeeId; + + @Value("${committee.node.id}") + private String nodeId; + + @Resource + private IEndorserService endorserService; + + @MockBean + private IEndorseServiceRepository endorseServiceRepository; + + @MockBean + private IBCDNSRepository bcdnsRepository; + + @MockBean + private ISystemConfigRepository systemConfigRepository; + + @MockBean + private IHcdvsPluginService hcdvsPluginService; + + @Resource + private AbstractCrossChainCertificate ptcCrossChainCert; + + @Test + public void testQueryTpBta() { + when(endorseServiceRepository.getMatchedTpBta(any())).thenReturn(new TpBtaWrapper(tpbta)); + Assert.assertTrue( + ArrayUtil.equals( + tpbta.encode(), + endorserService.queryMatchedTpBta(crossChainLane).getTpbta().encode() + ) + ); + } + + @Test + public void testVerifyBta() { + + when(bcdnsRepository.getDomainSpaceCert(anyString())).thenReturn( + new DomainSpaceCertWrapper( + CrossChainCertificateUtil.readCrossChainCertificateFromPem(DOT_COM_DOMAIN_SPACE_CERT.getBytes()) + ) + ); + + when(systemConfigRepository.queryCurrentPtcAnchorVersion()).thenReturn(BigInteger.ONE); + + var tpbtaWrapper = endorserService.verifyBta(domainCert, bta); + + Assert.assertTrue( + ArrayUtil.equals( + tpbta.getEndorseRoot(), + tpbtaWrapper.getEndorseRoot().encode() + ) + ); + Assert.assertEquals( + tpbta.getCrossChainLane().getLaneKey(), + tpbtaWrapper.getCrossChainLane().getLaneKey() + ); + Assert.assertTrue( + ArrayUtil.equals( + ptcCrossChainCert.getCredentialSubject(), + tpbtaWrapper.getTpbta().getSignerPtcCredentialSubject().encode() + ) + ); + Assert.assertEquals( + committeeId, + tpbtaWrapper.getEndorseProof().getCommitteeId() + ); + Assert.assertEquals( + 1, + tpbtaWrapper.getEndorseRoot().getEndorsers().size() + ); + Assert.assertEquals( + nodeId, + tpbtaWrapper.getEndorseRoot().getEndorsers().getFirst().getNodeId() + ); + Assert.assertEquals( + "default", + tpbtaWrapper.getEndorseRoot().getEndorsers().getFirst().getPublicKey().getKeyId() + ); + Assert.assertTrue( + tpbtaWrapper.getEndorseRoot().check(tpbtaWrapper.getEndorseProof(), tpbtaWrapper.getTpbta().getEncodedToSign()) + ); + } + + @Test + public void testCommitAnchorState() { + var hcdvs = mock(IHeteroChainDataVerifierService.class); + when(hcdvs.verifyAnchorConsensusState(any(), any())).thenReturn(VerifyResult.builder().success(true).build()); + when(endorseServiceRepository.getMatchedTpBta(any())).thenReturn(new TpBtaWrapper(tpbta)); + when(endorseServiceRepository.getBta(anyString())).thenReturn(new BtaWrapper(bta)); + when(hcdvsPluginService.getHCDVSService(anyString())).thenReturn(hcdvs); + + var vcs = BeanUtil.copyProperties(anchorState, ValidatedConsensusStateV1.class); + vcs.setPtcOid(ptcCrossChainCert.getCredentialSubjectInstance().getApplicant()); + vcs.setTpbtaVersion(tpbta.getTpbtaVersion()); + vcs.setPtcType(PTCTypeEnum.COMMITTEE); + + var vcsSigned = endorserService.commitAnchorState(crossChainLane, anchorState); + Assert.assertTrue( + new TpBtaWrapper(tpbta).getEndorseRoot().check( + CommitteeEndorseProof.decode(vcsSigned.getPtcProof()), + vcs.getEncodedToSign() + ) + ); + } + + @Test + public void testCommitConsensusState() { + var vcs = BeanUtil.copyProperties(anchorState, ValidatedConsensusStateV1.class); + vcs.setPtcOid(ptcCrossChainCert.getCredentialSubjectInstance().getApplicant()); + vcs.setTpbtaVersion(tpbta.getTpbtaVersion()); + vcs.setPtcType(PTCTypeEnum.COMMITTEE); + + var currVcs = BeanUtil.copyProperties(currState, ValidatedConsensusStateV1.class); + currVcs.setPtcOid(ptcCrossChainCert.getCredentialSubjectInstance().getApplicant()); + currVcs.setTpbtaVersion(tpbta.getTpbtaVersion()); + currVcs.setPtcType(PTCTypeEnum.COMMITTEE); + + var hcdvs = mock(IHeteroChainDataVerifierService.class); + when(hcdvs.verifyConsensusState(any(), any())).thenReturn(VerifyResult.builder().success(true).build()); + when(endorseServiceRepository.getMatchedTpBta(any())).thenReturn(new TpBtaWrapper(tpbta)); + when(endorseServiceRepository.getBta(anyString())).thenReturn(new BtaWrapper(bta)); + when(endorseServiceRepository.getValidatedConsensusState(anyString(), anyString())).thenReturn(new ValidatedConsensusStateWrapper(vcs)); + when(hcdvsPluginService.getHCDVSService(anyString())).thenReturn(hcdvs); + + var vcsSigned = endorserService.commitConsensusState(crossChainLane, currState); + Assert.assertTrue( + new TpBtaWrapper(tpbta).getEndorseRoot().check( + CommitteeEndorseProof.decode(vcsSigned.getPtcProof()), + currVcs.getEncodedToSign() + ) + ); + } + + @Test + public void testVerifyUcp() { + var currVcs = BeanUtil.copyProperties(currState, ValidatedConsensusStateV1.class); + currVcs.setPtcOid(ptcCrossChainCert.getCredentialSubjectInstance().getApplicant()); + currVcs.setTpbtaVersion(tpbta.getTpbtaVersion()); + currVcs.setPtcType(PTCTypeEnum.COMMITTEE); + + var hcdvs = mock(IHeteroChainDataVerifierService.class); + when(hcdvs.verifyCrossChainMessage(any(), any())).thenReturn(VerifyResult.builder().success(true).build()); + when(hcdvs.parseMessageFromLedgerData(any())).thenReturn(ucp.getSrcMessage().getMessage()); + when(endorseServiceRepository.getExactTpBta(any())).thenReturn(new TpBtaWrapper(tpbta)); + when(endorseServiceRepository.getBta(anyString())).thenReturn(new BtaWrapper(bta)); + when(endorseServiceRepository.getValidatedConsensusState(anyString(), anyString())).thenReturn(new ValidatedConsensusStateWrapper(currVcs)); + when(hcdvsPluginService.getHCDVSService(anyString())).thenReturn(hcdvs); + + var nodeProof = endorserService.verifyUcp(crossChainLane, ucp); + Assert.assertTrue( + new TpBtaWrapper(tpbta).getEndorseRoot().check( + CommitteeEndorseProof.builder() + .committeeId(committeeId) + .sigs(ListUtil.toList(nodeProof)) + .build(), + ThirdPartyProof.create( + tpbta.getTpbtaVersion(), + ucp.getSrcMessage().getMessage(), + crossChainLane + ).getEncodedToSign() + ) + ); + } + + @Test + public void testEndorseBlockState() { + var currVcs = BeanUtil.copyProperties(currState, ValidatedConsensusStateV1.class); + currVcs.setPtcOid(ptcCrossChainCert.getCredentialSubjectInstance().getApplicant()); + currVcs.setTpbtaVersion(tpbta.getTpbtaVersion()); + currVcs.setPtcType(PTCTypeEnum.COMMITTEE); + + when(endorseServiceRepository.getExactTpBta(any())).thenReturn(new TpBtaWrapper(tpbta)); + when(endorseServiceRepository.getValidatedConsensusState(anyString(), any(BigInteger.class))).thenReturn(new ValidatedConsensusStateWrapper(currVcs)); + when(endorseServiceRepository.hasValidatedConsensusState(anyString(), any())).thenReturn(true); + + EndorseBlockStateResp resp = endorserService.endorseBlockState(tpbta.getCrossChainLane(), "receiverdomain", currVcs.getHeight()); + Assert.assertEquals( + "receiverdomain", + SDPMessageFactory.createSDPMessage(resp.getBlockStateAuthMsg().getPayload()).getTargetDomain().getDomain() + ); + Assert.assertArrayEquals( + currVcs.getHash(), + resp.getBlockState().getHash() + ); + Assert.assertEquals( + currVcs.getHeight(), + resp.getBlockState().getHeight() + ); + Assert.assertEquals( + currVcs.getStateTimestamp(), + resp.getBlockState().getTimestamp() + ); + Assert.assertTrue( + new TpBtaWrapper(tpbta).getEndorseRoot().check( + CommitteeEndorseProof.builder() + .committeeId(committeeId) + .sigs(ListUtil.toList(resp.getCommitteeNodeProof())) + .build(), + ThirdPartyProof.create( + tpbta.getTpbtaVersion(), + resp.getBlockStateAuthMsg().encode(), + crossChainLane + ).getEncodedToSign() + ) + ); + } +} diff --git a/acb-committeeptc/monitor-node/src/test/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/service/HCDVSServiceTest.java b/acb-committeeptc/monitor-node/src/test/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/service/HCDVSServiceTest.java new file mode 100755 index 00000000..2e783cb8 --- /dev/null +++ b/acb-committeeptc/monitor-node/src/test/java/com/alipay/antchain/bridge/ptc/committee/monitor/node/service/HCDVSServiceTest.java @@ -0,0 +1,88 @@ +/* + * Copyright 2024 Ant Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alipay.antchain.bridge.ptc.committee.monitor.node.service; + +import java.util.List; + +import com.alipay.antchain.bridge.plugins.manager.core.AntChainBridgePluginState; +import com.alipay.antchain.bridge.ptc.committee.monitor.node.TestBase; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.junit.Assert; +import org.junit.Test; + +@Slf4j +public class HCDVSServiceTest extends TestBase { + + private static final String PLUGIN_PRODUCT = "testchain"; + + private static final String PLUGIN_PATH = "./src/test/resources/plugins/plugin-testchain2-0.1-SNAPSHOT-plugin.jar"; + + @Resource + private IHcdvsPluginService hcdvsPluginService; + + @Test + public void testHCDVSService() { + List supportProducts = hcdvsPluginService.getAvailableProducts(); + Assert.assertFalse(supportProducts.isEmpty()); + + Assert.assertTrue(hcdvsPluginService.hasPlugin(PLUGIN_PRODUCT)); + hcdvsPluginService.stopPlugin(PLUGIN_PRODUCT); + Assert.assertEquals(hcdvsPluginService.getPlugin(PLUGIN_PRODUCT).getCurrState(), AntChainBridgePluginState.STOP); + hcdvsPluginService.startPluginFromStop(PLUGIN_PRODUCT); + Assert.assertEquals(hcdvsPluginService.getPlugin(PLUGIN_PRODUCT).getCurrState(), AntChainBridgePluginState.START); + } + + @Test + public void testGetAvailableProduct() { + List supportProducts = hcdvsPluginService.getAvailableProducts(); + + for (String product:supportProducts) { + log.info("Committee-node's HcdvsService Contains products: {}", product); + } + } + + /* + @Test + public void testGetHCDVSService() { + IHeteroChainDataVerifierService hcdvsService = hcdvsPluginService.getHCDVSService(PLUGIN_PRODUCT); + hcdvsPluginService.stopPlugin(PLUGIN_PRODUCT); // stopPlugin逻辑里只是把state=STOP,要load指定PLUGIN需要一个unloadPlugin的接口,暂缺 + hcdvsPluginService.loadPlugin(PLUGIN_PATH); + hcdvsPluginService.startPlugin(PLUGIN_PATH); + } + */ + + @Test + public void testReloadPluginWithoutPath() { + if(hcdvsPluginService.hasPlugin(PLUGIN_PRODUCT)) { + hcdvsPluginService.stopPlugin(PLUGIN_PRODUCT); + Assert.assertEquals(hcdvsPluginService.getPlugin(PLUGIN_PRODUCT).getCurrState(), AntChainBridgePluginState.STOP); + } + hcdvsPluginService.reloadPlugin(PLUGIN_PRODUCT); + Assert.assertEquals(hcdvsPluginService.getPlugin(PLUGIN_PRODUCT).getCurrState(), AntChainBridgePluginState.START); + } + + @Test + public void testReloadPluginWithPath() { + if(hcdvsPluginService.hasPlugin(PLUGIN_PRODUCT)) { + hcdvsPluginService.stopPlugin(PLUGIN_PRODUCT); + Assert.assertEquals(hcdvsPluginService.getPlugin(PLUGIN_PRODUCT).getCurrState(), AntChainBridgePluginState.STOP); + } + hcdvsPluginService.reloadPlugin(PLUGIN_PRODUCT, PLUGIN_PATH); + Assert.assertEquals(hcdvsPluginService.getPlugin(PLUGIN_PRODUCT).getCurrState(), AntChainBridgePluginState.START); + } +} diff --git a/acb-committeeptc/monitor-node/src/test/resources/application-test.yml b/acb-committeeptc/monitor-node/src/test/resources/application-test.yml new file mode 100755 index 00000000..dc4cd06a --- /dev/null +++ b/acb-committeeptc/monitor-node/src/test/resources/application-test.yml @@ -0,0 +1,43 @@ +spring: + application: + name: committee-ptc + datasource: + driver-class-name: org.h2.Driver + url: jdbc:h2:mem:committee-node;DB_CLOSE_DELAY=-1;MODE=MySQL;IGNORECASE=TRUE + password: 123 + username: root + sql: + init: + data-locations: classpath:data/ddl.sql +logging: + file: + path: ./logs + level: + app: INFO + logback: + rollingpolicy: + clean-history-on-start: true +committee: + id: 1 + node: + credential: + sign-algo: "Keccak256WithSecp256k1" + private-key-file: ./private_key.pem + cert-file: ./ptc.crt + admin: + port: ${random.int(10088,20088)} + plugin: + # where to load the hetero-chain plugins + repo: ./src/test/resources/plugins + policy: + # limit actions of the plugin classloader + classloader: + resource: + ban-with-prefix: + # the plugin classloader will not read the resource file starting with the prefix below + APPLICATION: "META-INF/services/io.grpc." +grpc: + server: + port: 0 + security: + enabled: false \ No newline at end of file diff --git a/acb-committeeptc/monitor-node/src/test/resources/data/ddl.sql b/acb-committeeptc/monitor-node/src/test/resources/data/ddl.sql new file mode 100755 index 00000000..3c42b2a4 --- /dev/null +++ b/acb-committeeptc/monitor-node/src/test/resources/data/ddl.sql @@ -0,0 +1,93 @@ +/* + * Copyright 2024 Ant Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +CREATE TABLE IF NOT EXISTS `system_config` +( + `id` INT(11) NOT NULL AUTO_INCREMENT, + `conf_key` VARCHAR(128) DEFAULT NULL, + `conf_value` VARCHAR(15000) DEFAULT NULL, + `gmt_create` DATETIME DEFAULT CURRENT_TIMESTAMP, + `gmt_modified` DATETIME DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `conf_key` (`conf_key`) +); + +CREATE TABLE IF NOT EXISTS `bcdns_service` +( + `id` INT(11) PRIMARY KEY NOT NULL AUTO_INCREMENT, + `domain_space` VARCHAR(128) NOT NULL, + `parent_space` VARCHAR(128), + `owner_oid` VARCHAR(255) NOT NULL, + `type` VARCHAR(32) NOT NULL, + `state` INT NOT NULL, + `properties` BLOB, + `gmt_create` DATETIME DEFAULT CURRENT_TIMESTAMP, + `gmt_modified` DATETIME DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS `domain_space_cert` +( + `id` INT(11) NOT NULL AUTO_INCREMENT, + `domain_space` VARCHAR(128) DEFAULT NULL, + `parent_space` VARCHAR(128) DEFAULT NULL, + `owner_oid_hex` VARCHAR(255) NOT NULL, + `description` VARCHAR(128) DEFAULT NULL, + `domain_space_cert` BLOB, + `gmt_create` DATETIME DEFAULT CURRENT_TIMESTAMP, + `gmt_modified` DATETIME DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) +); + +CREATE TABLE IF NOT EXISTS `bta` +( + `id` INT(11) PRIMARY KEY NOT NULL AUTO_INCREMENT, + `product` VARCHAR(64) DEFAULT NULL, + `domain` VARCHAR(128) NOT NULL, + `bta_version` INT(11) DEFAULT NULL, + `subject_version` INT(11) DEFAULT NULL, + `raw_bta` BLOB, + `gmt_create` DATETIME DEFAULT CURRENT_TIMESTAMP, + `gmt_modified` DATETIME DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS `tpbta` +( + `id` INT(11) PRIMARY KEY NOT NULL AUTO_INCREMENT, + `version` INT(11) DEFAULT NULL, + `sender_domain` VARCHAR(128) NOT NULL, + `bta_subject_version` INT(11) DEFAULT NULL, + `sender_id` VARCHAR(64) DEFAULT NULL, + `receiver_domain` VARCHAR(128) DEFAULT NULL, + `receiver_id` VARCHAR(64) DEFAULT NULL, + `tpbta_version` INT(11) DEFAULT NULL, + `ptc_verify_anchor_version` INT(11) DEFAULT NULL, + `raw_tpbta` BLOB, + `gmt_create` DATETIME DEFAULT CURRENT_TIMESTAMP, + `gmt_modified` DATETIME DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS `validated_consensus_states` +( + `id` INT(11) PRIMARY KEY NOT NULL AUTO_INCREMENT, + `cs_version` INT(11) NOT NULL, + `domain` VARCHAR(128) NOT NULL, + `height` BIGINT UNSIGNED NOT NULL, + `hash` VARCHAR(64) DEFAULT NULL, + `parent_hash` VARCHAR(64) DEFAULT NULL, + `raw_vcs` BLOB, + `gmt_create` DATETIME DEFAULT CURRENT_TIMESTAMP, + `gmt_modified` DATETIME DEFAULT CURRENT_TIMESTAMP +); diff --git a/acb-committeeptc/monitor-node/src/test/resources/data/drop_all.sql b/acb-committeeptc/monitor-node/src/test/resources/data/drop_all.sql new file mode 100755 index 00000000..77183565 --- /dev/null +++ b/acb-committeeptc/monitor-node/src/test/resources/data/drop_all.sql @@ -0,0 +1 @@ +DROP ALL OBJECTS \ No newline at end of file diff --git a/acb-committeeptc/monitor-node/src/test/resources/private_key.pem b/acb-committeeptc/monitor-node/src/test/resources/private_key.pem new file mode 100755 index 00000000..49c22afb --- /dev/null +++ b/acb-committeeptc/monitor-node/src/test/resources/private_key.pem @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHQCAQEEIF2vqNkpBC7VIdSWCsfFw7oMJFxpvBAlXxRIcpaME1LXoAcGBSuBBAAK +oUQDQgAE9LJYmJcZBSEjN2rNGruF1zIIq5JLjSqlwZQ2xNcm2TL0prWFdapt0BVr +ACpaw0MitKnYjkcv16qm+XUbFK/E3w== +-----END EC PRIVATE KEY----- diff --git a/acb-committeeptc/monitor-node/src/test/resources/ptc.crt b/acb-committeeptc/monitor-node/src/test/resources/ptc.crt new file mode 100755 index 00000000..09ed47de --- /dev/null +++ b/acb-committeeptc/monitor-node/src/test/resources/ptc.crt @@ -0,0 +1,13 @@ +-----BEGIN PROOF TRANSFORMATION COMPONENT CERTIFICATE----- +AAD4AQAAAAABAAAAMQEADAAAAGFudGNoYWluLXB0YwIAAQAAAAIDAGsAAAAAAGUA +AAAAAAEAAAAAAQBYAAAAMFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAE9LJYmJcZBSEj +N2rNGruF1zIIq5JLjSqlwZQ2xNcm2TL0prWFdapt0BVrACpaw0MitKnYjkcv16qm ++XUbFK/E3wQACAAAANIlv2YAAAAABQAIAAAAUlmgaAAAAAAGAKAAAAAAAJoAAAAA +AAMAAAAxLjABAA0AAABjb21taXR0ZWUtcHRjAgABAAAAAQMAawAAAAAAZQAAAAAA +AQAAAAABAFgAAAAwVjAQBgcqhkjOPQIBBgUrgQQACgNCAAT0sliYlxkFISM3as0a +u4XXMgirkkuNKqXBlDbE1ybZMvSmtYV1qm3QFWsAKlrDQyK0qdiORy/Xqqb5dRsU +r8TfBAAAAAAABwCfAAAAAACZAAAAAAAKAAAAS0VDQ0FLLTI1NgEAIAAAAMMQP7r7 +zn4cC2mvoPTZOFzcfakEvZMwQykDEQv9al5fAgAWAAAAS2VjY2FrMjU2V2l0aFNl +Y3AyNTZrMQMAQQAAAOKPO60UaQlG2KkvfR9QQmM94G2vojyH2RAtWBbewOIqGtgk +OZuajWhHiipbzTT8Ssb/LS65C/5HeVMAYNkc/gAA +-----END PROOF TRANSFORMATION COMPONENT CERTIFICATE----- diff --git a/acb-committeeptc/monitor-node/src/test/resources/public_key.pem b/acb-committeeptc/monitor-node/src/test/resources/public_key.pem new file mode 100755 index 00000000..c28fd629 --- /dev/null +++ b/acb-committeeptc/monitor-node/src/test/resources/public_key.pem @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAE9LJYmJcZBSEjN2rNGruF1zIIq5JLjSql +wZQ2xNcm2TL0prWFdapt0BVrACpaw0MitKnYjkcv16qm+XUbFK/E3w== +-----END PUBLIC KEY----- diff --git a/acb-committeeptc/pom.xml b/acb-committeeptc/pom.xml index 0c27c452..e7b9b833 100644 --- a/acb-committeeptc/pom.xml +++ b/acb-committeeptc/pom.xml @@ -17,6 +17,8 @@ supervisor node node-cli + monitor-node + monitor-node-cli pom diff --git a/acb-pluginserver/ps-server/src/main/java/com/alipay/antchain/bridge/pluginserver/server/CrossChainServiceImpl.java b/acb-pluginserver/ps-server/src/main/java/com/alipay/antchain/bridge/pluginserver/server/CrossChainServiceImpl.java index 371698a6..d31b9227 100644 --- a/acb-pluginserver/ps-server/src/main/java/com/alipay/antchain/bridge/pluginserver/server/CrossChainServiceImpl.java +++ b/acb-pluginserver/ps-server/src/main/java/com/alipay/antchain/bridge/pluginserver/server/CrossChainServiceImpl.java @@ -20,6 +20,7 @@ import cn.hutool.core.util.StrUtil; import com.alipay.antchain.bridge.commons.bbc.AbstractBBCContext; import com.alipay.antchain.bridge.commons.bbc.DefaultBBCContext; +import com.alipay.antchain.bridge.commons.bbc.syscontract.MonitorContract; import com.alipay.antchain.bridge.commons.bbc.syscontract.PTCContract; import com.alipay.antchain.bridge.commons.bbc.syscontract.SDPContract; import com.alipay.antchain.bridge.commons.core.base.CrossChainDomain; @@ -42,6 +43,7 @@ import net.devh.boot.grpc.server.service.GrpcService; import java.math.BigInteger; +import java.util.Set; import java.util.stream.Collectors; import javax.annotation.Resource; @@ -140,9 +142,24 @@ public void bbcCall(CallBBCRequest request, StreamObserver responseObs case SETUPAUTHMESSAGECONTRACTREQ: resp = handleSetupAuthMessageContract(bbcService, product, domain); break; + case SETUPMONITORMESSAGECONTRACTREQ: + resp = handleSetupMonitorMessageContract(bbcService, product, domain); + break; case SETPROTOCOLREQ: resp = handleSetProtocol(bbcService, request.getSetProtocolReq(), product, domain); break; + case SETPROTOCOLINMONITORREQ: + resp = handleSetProtocolInMonitor(bbcService, request.getSetProtocolInMonitorReq(), product, domain); + break; + case SETMONITORCONTROLREQ: + resp = handleSetMonitorControl(bbcService, request.getSetMonitorControlReq(), product, domain); + break; + case SETPTCHUBINMONITORVERIFIERREQ: + resp = handleSetPtcHubInMonitorVerifier(bbcService, request.getSetPtcHubInMonitorVerifierReq(), product, domain); + break; + case SETMONITORCONTRACTREQ: + resp = handleSetMonitorContract(bbcService, request.getSetMonitorContractReq(), product, domain); + break; case SETAMCONTRACTREQ: resp = handleSetAmContract(bbcService, request.getSetAmContractReq(), product, domain); break; @@ -152,6 +169,9 @@ public void bbcCall(CallBBCRequest request, StreamObserver responseObs case RELAYAUTHMESSAGEREQ: resp = handleRelayAuthMessage(bbcService, request.getRelayAuthMessageReq(), product, domain); break; + case RELAYMONITORORDERREQ: + resp = handleRelayMonitorOrder(bbcService, request.getRelayMonitorOrderReq(), product, domain); + break; case READCROSSCHAINMESSAGERECEIPTREQ: resp = handleReadCrossChainMessageReceiptRequest(bbcService, request.getReadCrossChainMessageReceiptReq(), product, domain); break; @@ -289,6 +309,26 @@ private Response handleGetContext(IBBCService bbcService, String product, String } } + private Response handleSetupMonitorMessageContract(IBBCService bbcService, String product, String domain) { + try { + bbcService.setupMonitorContract(); + MonitorContract monitor = bbcService.getContext().getMonitorContract(); + log.info("BBCCall(handleSetupMonitorMessageContract) success [product: {}, domain: {}]: monitorAddr: {}, monitorStatus: {}", product, domain, monitor.getContractAddress(), monitor.getStatus()); + return ResponseBuilder.buildBBCSuccessResp(CallBBCResponse.newBuilder() + .setSetupMonitorResp(SetupMonitorMessageContractResponse.newBuilder() + .setMonitorContract( + MonitorMessageContract.newBuilder() + .setContractAddress(monitor.getContractAddress()) + .setStatusValue(monitor.getStatus().ordinal()) + ) + ) + ); + } catch (Exception e) { + log.error("BBCCall(handleSetupMonitorMessageContract) fail [product: {}, domain: {}, errorCode: {}, errorMsg: {}]", product, domain, ServerErrorCodeEnum.BBC_SETUPMONITORMESSAGECONTRACT_ERROR.getErrorCode(), ServerErrorCodeEnum.BBC_SETUPMONITORMESSAGECONTRACT_ERROR.getShortMsg(), e); + return ResponseBuilder.buildFailResp(ServerErrorCodeEnum.BBC_SETUPMONITORMESSAGECONTRACT_ERROR, e.toString()); + } + } + private Response handleSetupSDPMessageContract(IBBCService bbcService, String product, String domain) { try { bbcService.setupSDPMessageContract(); @@ -336,6 +376,46 @@ private Response handleSetProtocol(IBBCService bbcService, SetProtocolRequest re } } + private Response handleSetProtocolInMonitor(IBBCService bbcService, SetProtocolInMonitorRequest request, String product, String domain) { + try { + bbcService.setProtocolInMonitor(request.getProtocolAddress()); + return ResponseBuilder.buildBBCSuccessResp(CallBBCResponse.newBuilder()); + } catch (Exception e) { + log.error("BBCCall(handleSetProtocolInMonitor) fail [product: {}, domain: {}, errorCode: {}, errorMsg: {}]", product, domain, ServerErrorCodeEnum.BBC_SETPROTOCOLINMONITOR_ERROR.getErrorCode(), ServerErrorCodeEnum.BBC_SETPROTOCOLINMONITOR_ERROR.getShortMsg(), e); + return ResponseBuilder.buildFailResp(ServerErrorCodeEnum.BBC_SETPROTOCOLINMONITOR_ERROR, e.toString()); + } + } + + private Response handleSetMonitorControl(IBBCService bbcService, SetMonitorControlRequest request, String product, String domain) { + try { + bbcService.setMonitorControl(request.getMonitorType()); + return ResponseBuilder.buildBBCSuccessResp(CallBBCResponse.newBuilder()); + } catch (Exception e) { + log.error("BBCCall(handleSetMonitorControl) fail [product: {}, domain: {}, errorCode: {}, errorMsg: {}]", product, domain, ServerErrorCodeEnum.BBC_SETMONITORCONTROL_ERROR.getErrorCode(), ServerErrorCodeEnum.BBC_SETMONITORCONTROL_ERROR.getShortMsg(), e); + return ResponseBuilder.buildFailResp(ServerErrorCodeEnum.BBC_SETMONITORCONTROL_ERROR, e.toString()); + } + } + + private Response handleSetPtcHubInMonitorVerifier(IBBCService bbcService, SetPtcHubInMonitorVerifierRequest request, String product, String domain) { + try { + bbcService.setPtcHubInMonitorVerifier(request.getContractAddress()); + return ResponseBuilder.buildBBCSuccessResp(CallBBCResponse.newBuilder()); + } catch (Exception e) { + log.error("BBCCall(handleSetPtcHubInMonitorVerifier) fail [product: {}, domain: {}, errorCode: {}, errorMsg: {}]", product, domain, ServerErrorCodeEnum.BBC_SETPTCHUBINMONITORVERIFIER_ERROR.getErrorCode(), ServerErrorCodeEnum.BBC_SETPTCHUBINMONITORVERIFIER_ERROR.getShortMsg(), e); + return ResponseBuilder.buildFailResp(ServerErrorCodeEnum.BBC_SETPTCHUBINMONITORVERIFIER_ERROR, e.toString()); + } + } + + private Response handleSetMonitorContract(IBBCService bbcService, SetMonitorContractRequest request, String product, String domain) { + try { + bbcService.setMonitorContract(request.getContractAddress()); + return ResponseBuilder.buildBBCSuccessResp(CallBBCResponse.newBuilder()); + } catch (Exception e) { + log.error("BBCCall(handleSetMonitorContract) fail [product: {}, domain: {}, errorCode: {}, errorMsg: {}]", product, domain, ServerErrorCodeEnum.BBC_SETMONITORCONTRACT_ERROR.getErrorCode(), ServerErrorCodeEnum.BBC_SETMONITORCONTRACT_ERROR.getShortMsg(), e); + return ResponseBuilder.buildFailResp(ServerErrorCodeEnum.BBC_SETMONITORCONTRACT_ERROR, e.toString()); + } + } + private Response handleSetAmContract(IBBCService bbcService, SetAmContractRequest request, String product, String domain) { try { bbcService.setAmContract(request.getContractAddress()); @@ -382,6 +462,28 @@ private Response handleRelayAuthMessage(IBBCService bbcService, RelayAuthMessage } } + private Response handleRelayMonitorOrder(IBBCService bbcService, RelayMonitorOrderRequest request, String product, String domain) { + try { + CrossChainMessageReceipt ret = bbcService.relayMonitorOrder(request.getCommitteeId(), request.getSignAlgo(), request.getRawProof().toByteArray(), request.getRawMonitorOrder().toByteArray()); + return ResponseBuilder.buildBBCSuccessResp(CallBBCResponse.newBuilder() + .setRelayMonitorOrderResp(RelayMonitorOrderResponse.newBuilder() + .setReceipt( + com.alipay.antchain.bridge.pluginserver.service.CrossChainMessageReceipt.newBuilder() + .setTxhash(ObjectUtil.defaultIfNull(ret.getTxhash(), "")) + .setConfirmed(ret.isConfirmed()) + .setSuccessful(ret.isSuccessful()) + .setErrorMsg(ObjectUtil.defaultIfNull(ret.getErrorMsg(), "")) + .setTxTimestamp(ret.getTxTimestamp()) + .setRawTx(ByteString.copyFrom(ObjectUtil.defaultIfNull(ret.getRawTx(), new byte[]{}))) + ) + ) + ); + } catch (Exception e) { + log.error("BBCCall(handleRelayMonitorOrder) fail [product: {}, domain: {}, errorCode: {}, errorMsg: {}]", product, domain, ServerErrorCodeEnum.BBC_RELAYMONITORORDER_ERROR.getErrorCode(), ServerErrorCodeEnum.BBC_RELAYMONITORORDER_ERROR.getShortMsg(), e); + return ResponseBuilder.buildFailResp(ServerErrorCodeEnum.BBC_RELAYMONITORORDER_ERROR, e.toString()); + } + } + private Response handleReadCrossChainMessageReceiptRequest(IBBCService bbcService, ReadCrossChainMessageReceiptRequest request, String product, String domain) { try { CrossChainMessageReceipt receipt = bbcService.readCrossChainMessageReceipt(request.getTxhash()); diff --git a/acb-pluginserver/ps-server/src/main/java/com/alipay/antchain/bridge/pluginserver/server/exception/ServerErrorCodeEnum.java b/acb-pluginserver/ps-server/src/main/java/com/alipay/antchain/bridge/pluginserver/server/exception/ServerErrorCodeEnum.java index 6e0eddf5..e481f84a 100644 --- a/acb-pluginserver/ps-server/src/main/java/com/alipay/antchain/bridge/pluginserver/server/exception/ServerErrorCodeEnum.java +++ b/acb-pluginserver/ps-server/src/main/java/com/alipay/antchain/bridge/pluginserver/server/exception/ServerErrorCodeEnum.java @@ -24,6 +24,18 @@ public enum ServerErrorCodeEnum { SUCCESS(0, "success"), + BBC_SETUPMONITORMESSAGECONTRACT_ERROR(250, "[bbc] set up monitor contract failed"), + + BBC_SETPROTOCOLINMONITOR_ERROR(251, "[bbc] set protocol in monitor contract failed"), + + BBC_SETMONITORCONTROL_ERROR(252, "[bbc] set monitor control failed"), + + BBC_SETPTCHUBINMONITORVERIFIER_ERROR(253, "[bbc] set ptc hub in monitor verifier failed"), + + BBC_SETMONITORCONTRACT_ERROR(254, "[bbc] set monitor contract failed"), + + BBC_RELAYMONITORORDER_ERROR(255, "[bbc] relay monitor order failed"), + UNSUPPORT_BBC_REQUEST_ERROR(200, "unsupport bbc request type"), BBC_GET_SERVICE_ERROR(201, "[bbc] get service failed"), diff --git a/acb-pluginserver/ps-service/src/main/proto/pluginserver.proto b/acb-pluginserver/ps-service/src/main/proto/pluginserver.proto index 706f88c5..a7f48eda 100644 --- a/acb-pluginserver/ps-service/src/main/proto/pluginserver.proto +++ b/acb-pluginserver/ps-service/src/main/proto/pluginserver.proto @@ -100,6 +100,12 @@ message CallBBCRequest { QueryValidatedBlockStateRequest queryValidatedBlockStateRequest = 28; RecvOffChainExceptionRequest recvOffChainExceptionRequest = 29; ReliableRetryRequest reliableRetryRequest = 30; + SetupMonitorMessageContractRequest setupMonitorMessageContractReq=31; + SetMonitorContractRequest setMonitorContractReq = 32; + SetProtocolInMonitorRequest setProtocolInMonitorReq = 33; + SetMonitorControlRequest setMonitorControlReq = 34; + SetPtcHubInMonitorVerifierRequest setPtcHubInMonitorVerifierReq = 35; + RelayMonitorOrderRequest relayMonitorOrderReq =36; } } @@ -123,6 +129,26 @@ message SetupSDPMessageContractRequest { // stay empty body for now, maybe fill some stuff in future } +message SetupMonitorMessageContractRequest { + // stay empty body for now, maybe fill some stuff in future +} + +message SetMonitorContractRequest { + string contractAddress = 1; +} + +message SetProtocolInMonitorRequest { + string protocolAddress = 1; +} + +message SetMonitorControlRequest { + uint32 monitorType = 1; +} + +message SetPtcHubInMonitorVerifierRequest { + string contractAddress = 1; +} + message SetProtocolRequest { string protocolAddress = 1; string protocolType = 2; @@ -223,6 +249,13 @@ message ReliableRetryRequest { bytes reliableCrossChainMessageData = 1; } +message RelayMonitorOrderRequest { + string committeeId = 1; + string signAlgo = 2; + bytes rawProof = 3; + bytes rawMonitorOrder = 4; +} + // basic messages. // same as project `antchain-bridge-commons` message CrossChainMessageReceipt { @@ -277,6 +310,8 @@ message CallBBCResponse { QueryValidatedBlockStateResponse queryValidatedBlockStateResponse = 18; RecvOffChainExceptionResponse recvOffChainExceptionResponse = 19; ReliableRetryResponse reliableRetryResponse = 20; + SetupMonitorMessageContractResponse setupMonitorResp = 21; + RelayMonitorOrderResponse relayMonitorOrderResp = 22; } } @@ -309,6 +344,16 @@ message SetupSDPMessageContractResponse { SDPMessageContract sdpContract = 1; } +message MonitorMessageContract { + string contractAddress = 1; + ContractStatusEnum status = 2; +} + + +message SetupMonitorMessageContractResponse { + MonitorMessageContract monitorContract = 1; +} + message PtcContract { string contractAddress = 1; ContractStatusEnum status = 2; @@ -380,4 +425,8 @@ message RecvOffChainExceptionResponse { message ReliableRetryResponse { CrossChainMessageReceipt receipt = 1; +} + +message RelayMonitorOrderResponse { + CrossChainMessageReceipt receipt = 1; } \ No newline at end of file diff --git a/acb-relayer/r-commons/src/main/java/com/alipay/antchain/bridge/relayer/commons/model/BlockchainMeta.java b/acb-relayer/r-commons/src/main/java/com/alipay/antchain/bridge/relayer/commons/model/BlockchainMeta.java index d8017b6b..b5067fd6 100644 --- a/acb-relayer/r-commons/src/main/java/com/alipay/antchain/bridge/relayer/commons/model/BlockchainMeta.java +++ b/acb-relayer/r-commons/src/main/java/com/alipay/antchain/bridge/relayer/commons/model/BlockchainMeta.java @@ -71,6 +71,7 @@ public static BlockchainProperties decode(byte[] rawData) { "am_client_contract_address", "sdp_msg_contract_address", "ptc_contract_address", + "monitor_contract_address", "anchor_runtime_status", "init_block_height", "is_domain_registered", @@ -89,6 +90,9 @@ public static BlockchainProperties decode(byte[] rawData) { @JSONField(name = "ptc_contract_address") private String ptcContractAddress; + @JSONField(name = "monitor_contract_address") + private String monitorContractAddress; + @JSONField(name = "anchor_runtime_status") private BlockchainStateEnum anchorRuntimeStatus; diff --git a/acb-relayer/r-core/src/main/java/com/alipay/antchain/bridge/relayer/core/manager/bbc/IMonitorClientContract.java b/acb-relayer/r-core/src/main/java/com/alipay/antchain/bridge/relayer/core/manager/bbc/IMonitorClientContract.java new file mode 100755 index 00000000..92b48418 --- /dev/null +++ b/acb-relayer/r-core/src/main/java/com/alipay/antchain/bridge/relayer/core/manager/bbc/IMonitorClientContract.java @@ -0,0 +1,13 @@ +package com.alipay.antchain.bridge.relayer.core.manager.bbc; + +public interface IMonitorClientContract { + + void setProtocolInMonitor(String protocolContract); + + void setMonitorControl(int monitorType); + + void setPtcHubInMonitorVerifier(String contractAddress); + + void deployContract(); + +} diff --git a/acb-relayer/r-core/src/main/java/com/alipay/antchain/bridge/relayer/core/manager/bbc/ISDPMsgClientContract.java b/acb-relayer/r-core/src/main/java/com/alipay/antchain/bridge/relayer/core/manager/bbc/ISDPMsgClientContract.java index 670a234a..823f2628 100644 --- a/acb-relayer/r-core/src/main/java/com/alipay/antchain/bridge/relayer/core/manager/bbc/ISDPMsgClientContract.java +++ b/acb-relayer/r-core/src/main/java/com/alipay/antchain/bridge/relayer/core/manager/bbc/ISDPMsgClientContract.java @@ -18,6 +18,8 @@ public interface ISDPMsgClientContract { */ void setAmContract(String amContract); + void setMonitorContract(String monitorContract); + /** * Query the sequence number of the cross-chain direction * diff --git a/acb-relayer/r-core/src/main/java/com/alipay/antchain/bridge/relayer/core/manager/bbc/MonitorClientContractHeteroBlockchainImpl.java b/acb-relayer/r-core/src/main/java/com/alipay/antchain/bridge/relayer/core/manager/bbc/MonitorClientContractHeteroBlockchainImpl.java new file mode 100755 index 00000000..ffd7be2f --- /dev/null +++ b/acb-relayer/r-core/src/main/java/com/alipay/antchain/bridge/relayer/core/manager/bbc/MonitorClientContractHeteroBlockchainImpl.java @@ -0,0 +1,34 @@ +package com.alipay.antchain.bridge.relayer.core.manager.bbc; + +import com.alipay.antchain.bridge.relayer.core.types.pluginserver.IBBCServiceClient; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class MonitorClientContractHeteroBlockchainImpl implements IMonitorClientContract { + private IBBCServiceClient bbcServiceClient; + + public MonitorClientContractHeteroBlockchainImpl(IBBCServiceClient bbcServiceClient) { + this.bbcServiceClient = bbcServiceClient; + } + + @Override + public void setProtocolInMonitor(String protocolContract) { + this.bbcServiceClient.setProtocolInMonitor(protocolContract); + } + + @Override + public void setMonitorControl(int monitorType) { + this.bbcServiceClient.setMonitorControl(monitorType); + } + + @Override + public void setPtcHubInMonitorVerifier(String contractAddress) { + this.bbcServiceClient.setPtcHubInMonitorVerifier(contractAddress); + } + + @Override + public void deployContract() { + this.bbcServiceClient.setupMonitorContract(); + } +} + diff --git a/acb-relayer/r-core/src/main/java/com/alipay/antchain/bridge/relayer/core/manager/bbc/SDPMsgClientHeteroBlockchainImpl.java b/acb-relayer/r-core/src/main/java/com/alipay/antchain/bridge/relayer/core/manager/bbc/SDPMsgClientHeteroBlockchainImpl.java index 246a111d..b249c7ce 100644 --- a/acb-relayer/r-core/src/main/java/com/alipay/antchain/bridge/relayer/core/manager/bbc/SDPMsgClientHeteroBlockchainImpl.java +++ b/acb-relayer/r-core/src/main/java/com/alipay/antchain/bridge/relayer/core/manager/bbc/SDPMsgClientHeteroBlockchainImpl.java @@ -23,6 +23,11 @@ public void setAmContract(String amContract) { this.bbcServiceClient.setLocalDomain(bbcServiceClient.getDomain()); } + @Override + public void setMonitorContract(String monitorContract) { + this.bbcServiceClient.setMonitorContract(monitorContract); + } + @Override public long querySDPMsgSeqOnChain(String senderDomain, String from, String receiverDomain, String to) { try { diff --git a/acb-relayer/r-core/src/main/java/com/alipay/antchain/bridge/relayer/core/manager/blockchain/BlockchainManager.java b/acb-relayer/r-core/src/main/java/com/alipay/antchain/bridge/relayer/core/manager/blockchain/BlockchainManager.java index bee47c21..2d9844d2 100644 --- a/acb-relayer/r-core/src/main/java/com/alipay/antchain/bridge/relayer/core/manager/blockchain/BlockchainManager.java +++ b/acb-relayer/r-core/src/main/java/com/alipay/antchain/bridge/relayer/core/manager/blockchain/BlockchainManager.java @@ -89,6 +89,10 @@ public class BlockchainManager implements IBlockchainManager { private final Map latestVerifyAnchorVersionOnchain = new ConcurrentHashMap<>(); + private static final int MONITOR_CLOSE = 1; + + private static final int MONITOR_OPEN = 2; + @Override public void addBlockchain( String product, @@ -926,6 +930,13 @@ private void deployHeteroBlockchainAMContract(BlockchainMeta blockchainMeta) { blockchainClient.getSDPMsgClientContract().deployContract(); } + if ( + ObjectUtil.isNull(bbcContext.getMonitorContract()) + || ContractStatusEnum.INIT == bbcContext.getMonitorContract().getStatus() + ) { + blockchainClient.getMonitorClientContract().deployContract(); + } + boolean isPtcSupport = true; if ( ObjectUtil.isNull(bbcContext.getPtcContract()) @@ -954,6 +965,13 @@ private void deployHeteroBlockchainAMContract(BlockchainMeta blockchainMeta) { blockchainMeta.getProduct(), blockchainMeta.getBlockchainId(), blockchainMeta.getPluginServerId() ); } + if (ObjectUtil.isNull(bbcContext.getMonitorContract()) || StrUtil.isEmpty(bbcContext.getMonitorContract().getContractAddress())) { + throw new AntChainBridgeRelayerException( + RelayerErrorCodeEnum.CORE_BLOCKCHAIN_ERROR, + "monitor contract is empty for blockchain ( product: {}, bcId: {}, pluginServer: {})", + blockchainMeta.getProduct(), blockchainMeta.getBlockchainId(), blockchainMeta.getPluginServerId() + ); + } if (isPtcSupport && (ObjectUtil.isNull(bbcContext.getPtcContract()) || StrUtil.isEmpty(bbcContext.getPtcContract().getContractAddress()))) { throw new AntChainBridgeRelayerException( RelayerErrorCodeEnum.CORE_BLOCKCHAIN_ERROR, @@ -974,10 +992,25 @@ private void deployHeteroBlockchainAMContract(BlockchainMeta blockchainMeta) { blockchainClient.getSDPMsgClientContract() .setAmContract(bbcContext.getAuthMessageContract().getContractAddress()); + blockchainClient.getSDPMsgClientContract() + .setMonitorContract(bbcContext.getMonitorContract().getContractAddress()); + + blockchainClient.getMonitorClientContract() + .setMonitorControl(MONITOR_OPEN); + + blockchainClient.getMonitorClientContract() + .setProtocolInMonitor(bbcContext.getSdpContract().getContractAddress()); + + if (isPtcSupport) { + blockchainClient.getMonitorClientContract() + .setPtcHubInMonitorVerifier(bbcContext.getPtcContract().getContractAddress()); + } + bbcContext = blockchainClient.queryBBCContext(); blockchainMeta.getProperties().setAmClientContractAddress(bbcContext.getAuthMessageContract().getContractAddress()); blockchainMeta.getProperties().setSdpMsgContractAddress(bbcContext.getSdpContract().getContractAddress()); + blockchainMeta.getProperties().setMonitorContractAddress(bbcContext.getMonitorContract().getContractAddress()); if (isPtcSupport) { blockchainMeta.getProperties().setPtcContractAddress(bbcContext.getPtcContract().getContractAddress()); } diff --git a/acb-relayer/r-core/src/main/java/com/alipay/antchain/bridge/relayer/core/types/blockchain/AbstractBlockchainClient.java b/acb-relayer/r-core/src/main/java/com/alipay/antchain/bridge/relayer/core/types/blockchain/AbstractBlockchainClient.java index c8e5119f..6363fd05 100644 --- a/acb-relayer/r-core/src/main/java/com/alipay/antchain/bridge/relayer/core/types/blockchain/AbstractBlockchainClient.java +++ b/acb-relayer/r-core/src/main/java/com/alipay/antchain/bridge/relayer/core/types/blockchain/AbstractBlockchainClient.java @@ -8,6 +8,7 @@ import com.alipay.antchain.bridge.commons.core.rcc.ReliableCrossChainMessage; import com.alipay.antchain.bridge.relayer.commons.model.BlockchainMeta; import com.alipay.antchain.bridge.relayer.core.manager.bbc.IAMClientContract; +import com.alipay.antchain.bridge.relayer.core.manager.bbc.IMonitorClientContract; import com.alipay.antchain.bridge.relayer.core.manager.bbc.IPtcContract; import com.alipay.antchain.bridge.relayer.core.manager.bbc.ISDPMsgClientContract; import lombok.AllArgsConstructor; @@ -41,6 +42,8 @@ public abstract class AbstractBlockchainClient { public abstract ISDPMsgClientContract getSDPMsgClientContract(); + public abstract IMonitorClientContract getMonitorClientContract(); + public abstract IPtcContract getPtcContract(); public abstract CrossChainMessageReceipt queryCommittedTxReceipt(String txhash); diff --git a/acb-relayer/r-core/src/main/java/com/alipay/antchain/bridge/relayer/core/types/blockchain/HeteroBlockchainClient.java b/acb-relayer/r-core/src/main/java/com/alipay/antchain/bridge/relayer/core/types/blockchain/HeteroBlockchainClient.java index b1b12fa0..ce678b8e 100644 --- a/acb-relayer/r-core/src/main/java/com/alipay/antchain/bridge/relayer/core/types/blockchain/HeteroBlockchainClient.java +++ b/acb-relayer/r-core/src/main/java/com/alipay/antchain/bridge/relayer/core/types/blockchain/HeteroBlockchainClient.java @@ -26,6 +26,8 @@ public class HeteroBlockchainClient extends AbstractBlockchainClient { private final ISDPMsgClientContract sdpMsgClient; + private final IMonitorClientContract monitorClient; + private final IPtcContract ptcContract; public HeteroBlockchainClient(IBBCServiceClient bbcClient, BlockchainMeta blockchainMeta) { @@ -33,6 +35,7 @@ public HeteroBlockchainClient(IBBCServiceClient bbcClient, BlockchainMeta blockc this.bbcClient = bbcClient; this.amClientContract = new AMClientContractHeteroBlockchainImpl(bbcClient); this.sdpMsgClient = new SDPMsgClientHeteroBlockchainImpl(bbcClient); + this.monitorClient = new MonitorClientContractHeteroBlockchainImpl(bbcClient); this.ptcContract = new PtcContractHeteroBlockchainImpl(bbcClient); } @@ -182,6 +185,11 @@ public ISDPMsgClientContract getSDPMsgClientContract() { return this.sdpMsgClient; } + @Override + public IMonitorClientContract getMonitorClientContract() { + return this.monitorClient; + } + @Override public IPtcContract getPtcContract() { return this.ptcContract; diff --git a/acb-relayer/r-core/src/main/java/com/alipay/antchain/bridge/relayer/core/types/network/ws/client/generated/Request.java b/acb-relayer/r-core/src/main/java/com/alipay/antchain/bridge/relayer/core/types/network/ws/client/generated/Request.java index e017caec..69156e99 100644 --- a/acb-relayer/r-core/src/main/java/com/alipay/antchain/bridge/relayer/core/types/network/ws/client/generated/Request.java +++ b/acb-relayer/r-core/src/main/java/com/alipay/antchain/bridge/relayer/core/types/network/ws/client/generated/Request.java @@ -7,9 +7,9 @@ /** - *

request complex type的 Java 类。 + *

Java class for request complex type. * - *

以下模式片段指定包含在此类中的预期内容。 + *

The following schema fragment specifies the expected content contained within this class. * *

  * <complexType name="request">
@@ -34,7 +34,7 @@ public class Request {
     protected String relayerRequest;
 
     /**
-     * 获取relayerRequest属性的值。
+     * Gets the value of the relayerRequest property.
      * 
      * @return
      *     possible object is
@@ -46,7 +46,7 @@ public String getRelayerRequest() {
     }
 
     /**
-     * 设置relayerRequest属性的值。
+     * Sets the value of the relayerRequest property.
      * 
      * @param value
      *     allowed object is
diff --git a/acb-relayer/r-core/src/main/java/com/alipay/antchain/bridge/relayer/core/types/network/ws/client/generated/RequestResponse.java b/acb-relayer/r-core/src/main/java/com/alipay/antchain/bridge/relayer/core/types/network/ws/client/generated/RequestResponse.java
index f1e53e0a..7d271b0b 100644
--- a/acb-relayer/r-core/src/main/java/com/alipay/antchain/bridge/relayer/core/types/network/ws/client/generated/RequestResponse.java
+++ b/acb-relayer/r-core/src/main/java/com/alipay/antchain/bridge/relayer/core/types/network/ws/client/generated/RequestResponse.java
@@ -8,9 +8,9 @@
 
 
 /**
- * 

requestResponse complex type的 Java 类。 + *

Java class for requestResponse complex type. * - *

以下模式片段指定包含在此类中的预期内容。 + *

The following schema fragment specifies the expected content contained within this class. * *

  * <complexType name="requestResponse">
@@ -36,7 +36,7 @@ public class RequestResponse {
     protected String _return;
 
     /**
-     * 获取return属性的值。
+     * Gets the value of the return property.
      * 
      * @return
      *     possible object is
@@ -48,7 +48,7 @@ public String getReturn() {
     }
 
     /**
-     * 设置return属性的值。
+     * Sets the value of the return property.
      * 
      * @param value
      *     allowed object is
diff --git a/acb-relayer/r-core/src/main/java/com/alipay/antchain/bridge/relayer/core/types/pluginserver/GRpcBBCServiceClient.java b/acb-relayer/r-core/src/main/java/com/alipay/antchain/bridge/relayer/core/types/pluginserver/GRpcBBCServiceClient.java
index 83f0d71f..c5da0443 100644
--- a/acb-relayer/r-core/src/main/java/com/alipay/antchain/bridge/relayer/core/types/pluginserver/GRpcBBCServiceClient.java
+++ b/acb-relayer/r-core/src/main/java/com/alipay/antchain/bridge/relayer/core/types/pluginserver/GRpcBBCServiceClient.java
@@ -210,6 +210,91 @@ public void setupSDPMessageContract() {
         }
     }
 
+    @Override
+    public void setupMonitorContract() {
+        Response response = this.blockingStub.bbcCall(
+                CallBBCRequest.newBuilder()
+                        .setProduct(this.getProduct())
+                        .setDomain(this.getDomain())
+                        .setSetupMonitorMessageContractReq(SetupMonitorMessageContractRequest.getDefaultInstance())
+                        .build()
+        );
+        if (response.getCode() != 0) {
+            throw new RuntimeException(
+                    String.format("[GRpcBBCServiceClient (domain: %s, product: %s)] setupMonitorMessageContract request failed for plugin server %s: %s",
+                            this.domain, this.product, this.psId, response.getErrorMsg())
+            );
+        }
+    }
+
+    @Override
+    public void setMonitorContract(String monitorContract) {
+        Response response = this.blockingStub.bbcCall(
+                CallBBCRequest.newBuilder()
+                        .setProduct(this.getProduct())
+                        .setDomain(this.getDomain())
+                        .setSetMonitorContractReq(SetMonitorContractRequest.newBuilder().setContractAddress(monitorContract))
+                        .build()
+        );
+        if (response.getCode() != 0) {
+            throw new RuntimeException(
+                    String.format("[GRpcBBCServiceClient (domain: %s, product: %s)] setMonitorContract request failed for plugin server %s: %s",
+                            this.domain, this.product, this.psId, response.getErrorMsg())
+            );
+        }
+    }
+
+    @Override
+    public void setProtocolInMonitor(String protocolContract) {
+        Response response = this.blockingStub.bbcCall(
+                CallBBCRequest.newBuilder()
+                        .setProduct(this.getProduct())
+                        .setDomain(this.getDomain())
+                        .setSetProtocolInMonitorReq(SetProtocolInMonitorRequest.newBuilder().setProtocolAddress(protocolContract))
+                        .build()
+        );
+        if (response.getCode() != 0) {
+            throw new RuntimeException(
+                    String.format("[GRpcBBCServiceClient (domain: %s, product: %s)] setProtocolInMonitor request failed for plugin server %s: %s",
+                            this.domain, this.product, this.psId, response.getErrorMsg())
+            );
+        }
+    }
+
+    @Override
+    public void setMonitorControl(int monitorType) {
+        Response response = this.blockingStub.bbcCall(
+                CallBBCRequest.newBuilder()
+                        .setProduct(this.getProduct())
+                        .setDomain(this.getDomain())
+                        .setSetMonitorControlReq(SetMonitorControlRequest.newBuilder().setMonitorType(monitorType))
+                        .build()
+        );
+        if (response.getCode() != 0) {
+            throw new RuntimeException(
+                    String.format("[GRpcBBCServiceClient (domain: %s, product: %s)] setMonitorControl request failed for plugin server %s: %s",
+                            this.domain, this.product, this.psId, response.getErrorMsg())
+            );
+        }
+    }
+
+    @Override
+    public void setPtcHubInMonitorVerifier(String contractAddress) {
+        Response response = this.blockingStub.bbcCall(
+                CallBBCRequest.newBuilder()
+                        .setProduct(this.getProduct())
+                        .setDomain(this.getDomain())
+                        .setSetPtcHubInMonitorVerifierReq(SetPtcHubInMonitorVerifierRequest.newBuilder().setContractAddress(contractAddress))
+                        .build()
+        );
+        if (response.getCode() != 0) {
+            throw new RuntimeException(
+                    String.format("[GRpcBBCServiceClient (domain: %s, product: %s)] setPtcHubInMonitorVerifier request failed for plugin server %s: %s",
+                            this.domain, this.product, this.psId, response.getErrorMsg())
+            );
+        }
+    }
+
     @Override
     public void setProtocol(String protocolAddress, String protocolType) {
         Response response = this.blockingStub.bbcCall(
@@ -270,6 +355,11 @@ public CrossChainMessageReceipt relayAuthMessage(byte[] rawMessage) {
         return PluginServerUtils.convertFromGRpcCrossChainMessageReceipt(response.getBbcResp().getRelayAuthMessageResponse().getReceipt());
     }
 
+    @Override
+    public CrossChainMessageReceipt relayMonitorOrder(String committeeId, String signAlgo, byte[] rawProof, byte[] rawMonitorOrder) {
+        return new CrossChainMessageReceipt();
+    }
+
     @Override
     public void setAmContract(String contractAddress) {
         Response response = this.blockingStub.bbcCall(
diff --git a/acb-relayer/r-core/src/main/proto/pluginserver.proto b/acb-relayer/r-core/src/main/proto/pluginserver.proto
index 706f88c5..a7f48eda 100644
--- a/acb-relayer/r-core/src/main/proto/pluginserver.proto
+++ b/acb-relayer/r-core/src/main/proto/pluginserver.proto
@@ -100,6 +100,12 @@ message CallBBCRequest {
     QueryValidatedBlockStateRequest queryValidatedBlockStateRequest = 28;
     RecvOffChainExceptionRequest recvOffChainExceptionRequest = 29;
     ReliableRetryRequest reliableRetryRequest = 30;
+    SetupMonitorMessageContractRequest setupMonitorMessageContractReq=31;
+    SetMonitorContractRequest setMonitorContractReq = 32;
+    SetProtocolInMonitorRequest setProtocolInMonitorReq = 33;
+    SetMonitorControlRequest setMonitorControlReq = 34;
+    SetPtcHubInMonitorVerifierRequest setPtcHubInMonitorVerifierReq = 35;
+    RelayMonitorOrderRequest relayMonitorOrderReq =36;
   }
 }
 
@@ -123,6 +129,26 @@ message SetupSDPMessageContractRequest {
   // stay empty body for now, maybe fill some stuff in future
 }
 
+message SetupMonitorMessageContractRequest {
+  // stay empty body for now, maybe fill some stuff in future
+}
+
+message SetMonitorContractRequest {
+  string contractAddress = 1;
+}
+
+message SetProtocolInMonitorRequest {
+  string protocolAddress = 1;
+}
+
+message SetMonitorControlRequest {
+  uint32 monitorType = 1;
+}
+
+message SetPtcHubInMonitorVerifierRequest {
+  string contractAddress = 1;
+}
+
 message SetProtocolRequest {
   string protocolAddress = 1;
   string protocolType = 2;
@@ -223,6 +249,13 @@ message ReliableRetryRequest {
   bytes reliableCrossChainMessageData = 1;
 }
 
+message RelayMonitorOrderRequest {
+  string committeeId = 1;
+  string signAlgo = 2;
+  bytes rawProof = 3;
+  bytes rawMonitorOrder = 4;
+}
+
 // basic messages.
 // same as project `antchain-bridge-commons`
 message CrossChainMessageReceipt {
@@ -277,6 +310,8 @@ message CallBBCResponse {
     QueryValidatedBlockStateResponse queryValidatedBlockStateResponse = 18;
     RecvOffChainExceptionResponse recvOffChainExceptionResponse = 19;
     ReliableRetryResponse reliableRetryResponse = 20;
+    SetupMonitorMessageContractResponse setupMonitorResp = 21;
+    RelayMonitorOrderResponse relayMonitorOrderResp = 22;
   }
 }
 
@@ -309,6 +344,16 @@ message SetupSDPMessageContractResponse {
   SDPMessageContract sdpContract = 1;
 }
 
+message MonitorMessageContract {
+  string contractAddress = 1;
+  ContractStatusEnum status = 2;
+}
+
+
+message SetupMonitorMessageContractResponse {
+  MonitorMessageContract monitorContract = 1;
+}
+
 message PtcContract {
   string contractAddress = 1;
   ContractStatusEnum status = 2;
@@ -380,4 +425,8 @@ message RecvOffChainExceptionResponse {
 
 message ReliableRetryResponse {
   CrossChainMessageReceipt receipt = 1;
+}
+
+message RelayMonitorOrderResponse {
+  CrossChainMessageReceipt receipt = 1;
 }
\ No newline at end of file
diff --git a/acb-relayer/r-facade/src/main/java/com/alipay/antchain/bridge/relayer/facade/admin/types/SysContractsInfo.java b/acb-relayer/r-facade/src/main/java/com/alipay/antchain/bridge/relayer/facade/admin/types/SysContractsInfo.java
index 4e0b030a..c7d33a80 100644
--- a/acb-relayer/r-facade/src/main/java/com/alipay/antchain/bridge/relayer/facade/admin/types/SysContractsInfo.java
+++ b/acb-relayer/r-facade/src/main/java/com/alipay/antchain/bridge/relayer/facade/admin/types/SysContractsInfo.java
@@ -17,6 +17,9 @@ public class SysContractsInfo {
     @JSONField(name = "ptc_contract")
     private String ptcContract;
 
+    @JSONField(name = "monitor_contract")
+    private String monitorContract;
+
     @JSONField(name = "state")
     private String state;
 }
diff --git a/acb-relayer/r-server/src/main/java/com/alipay/antchain/bridge/relayer/server/admin/impl/BlockchainNamespace.java b/acb-relayer/r-server/src/main/java/com/alipay/antchain/bridge/relayer/server/admin/impl/BlockchainNamespace.java
index c16320b8..ade3f462 100644
--- a/acb-relayer/r-server/src/main/java/com/alipay/antchain/bridge/relayer/server/admin/impl/BlockchainNamespace.java
+++ b/acb-relayer/r-server/src/main/java/com/alipay/antchain/bridge/relayer/server/admin/impl/BlockchainNamespace.java
@@ -189,6 +189,12 @@ Object getBlockchainContracts(String... args) {
                 )
         );
 
+        sysContractsInfo.setMonitorContract(
+                ObjectUtil.defaultIfNull(
+                        blockchainMeta.getProperties().getMonitorContractAddress(), "empty"
+                )
+        );
+
         OnChainServiceStatusEnum amStatus = blockchainMeta.getProperties().getAmServiceStatus();
         sysContractsInfo.setState(
                 ObjectUtil.isNull(amStatus)? "" : amStatus.name()
diff --git a/acb-sdk/antchain-bridge-commons/src/main/java/com/alipay/antchain/bridge/commons/bbc/AbstractBBCContext.java b/acb-sdk/antchain-bridge-commons/src/main/java/com/alipay/antchain/bridge/commons/bbc/AbstractBBCContext.java
index c2a9d88e..53ebcd4c 100644
--- a/acb-sdk/antchain-bridge-commons/src/main/java/com/alipay/antchain/bridge/commons/bbc/AbstractBBCContext.java
+++ b/acb-sdk/antchain-bridge-commons/src/main/java/com/alipay/antchain/bridge/commons/bbc/AbstractBBCContext.java
@@ -19,6 +19,7 @@
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.annotation.JSONField;
 import com.alipay.antchain.bridge.commons.bbc.syscontract.AuthMessageContract;
+import com.alipay.antchain.bridge.commons.bbc.syscontract.MonitorContract;
 import com.alipay.antchain.bridge.commons.bbc.syscontract.SDPContract;
 import com.alipay.antchain.bridge.commons.bbc.syscontract.PTCContract;
 import lombok.Getter;
@@ -37,6 +38,9 @@ public abstract class AbstractBBCContext implements IBBCContext {
     @JSONField(name = "sdp_contract")
     private SDPContract sdpContract;
 
+    @JSONField(name = "monitor_contract")
+    private MonitorContract monitorContract;
+
     @JSONField(name = "is_reliable")
     private boolean isReliable;
 
@@ -47,6 +51,7 @@ public abstract class AbstractBBCContext implements IBBCContext {
     public void decodeFromBytes(byte[] raw) {
         AbstractBBCContext state = JSON.parseObject(raw, this.getClass());
         this.setSdpContract(state.getSdpContract());
+        this.setMonitorContract(state.getMonitorContract());
         this.setPtcContract(state.getPtcContract());
         this.setAuthMessageContract(state.getAuthMessageContract());
         this.setReliable(state.isReliable());
diff --git a/acb-sdk/antchain-bridge-commons/src/main/java/com/alipay/antchain/bridge/commons/bbc/syscontract/MonitorContract.java b/acb-sdk/antchain-bridge-commons/src/main/java/com/alipay/antchain/bridge/commons/bbc/syscontract/MonitorContract.java
new file mode 100755
index 00000000..6934c5f5
--- /dev/null
+++ b/acb-sdk/antchain-bridge-commons/src/main/java/com/alipay/antchain/bridge/commons/bbc/syscontract/MonitorContract.java
@@ -0,0 +1,13 @@
+package com.alipay.antchain.bridge.commons.bbc.syscontract;
+
+import lombok.Getter;
+import lombok.Setter;
+
+@Getter
+@Setter
+public class MonitorContract {
+
+    private String contractAddress;
+
+    private ContractStatusEnum status;
+}
diff --git a/acb-sdk/antchain-bridge-commons/src/main/java/com/alipay/antchain/bridge/commons/core/monitor/AbstractMonitorMessage.java b/acb-sdk/antchain-bridge-commons/src/main/java/com/alipay/antchain/bridge/commons/core/monitor/AbstractMonitorMessage.java
new file mode 100755
index 00000000..4c00c90b
--- /dev/null
+++ b/acb-sdk/antchain-bridge-commons/src/main/java/com/alipay/antchain/bridge/commons/core/monitor/AbstractMonitorMessage.java
@@ -0,0 +1,25 @@
+package com.alipay.antchain.bridge.commons.core.monitor;
+
+import lombok.Getter;
+import lombok.Setter;
+
+
+@Getter
+@Setter
+public abstract class AbstractMonitorMessage implements IMonitorMessage {
+
+    public static int MONITOR_CLOSE = 1;
+
+    public static int MONITOR_OPEN = 2;
+
+    public static int MONITOR_ROLLBACK = 3;
+
+    public static int MONITOR_ORDER = 4;
+
+    private int monitorType;
+
+    private String monitorMsg;
+
+    private byte[] payload;
+
+}
diff --git a/acb-sdk/antchain-bridge-commons/src/main/java/com/alipay/antchain/bridge/commons/core/monitor/AbstractMonitorOrder.java b/acb-sdk/antchain-bridge-commons/src/main/java/com/alipay/antchain/bridge/commons/core/monitor/AbstractMonitorOrder.java
new file mode 100644
index 00000000..b31d85cf
--- /dev/null
+++ b/acb-sdk/antchain-bridge-commons/src/main/java/com/alipay/antchain/bridge/commons/core/monitor/AbstractMonitorOrder.java
@@ -0,0 +1,74 @@
+package com.alipay.antchain.bridge.commons.core.monitor;
+
+import cn.hutool.core.util.HexUtil;
+import com.alipay.antchain.bridge.commons.exception.AntChainBridgeCommonsException;
+import com.alipay.antchain.bridge.commons.exception.CommonsErrorCodeEnum;
+import lombok.Getter;
+import lombok.Setter;
+
+@Getter
+@Setter
+public abstract class AbstractMonitorOrder implements IMonitorOrder {
+
+    public static final int IDENTITY_LENGTH = 32;
+
+    private String product;
+
+    private String domain;
+
+    private long monitorOrderType;
+
+    private String senderDomain;
+
+    private String fromAddress;
+
+    private String receiverDomain;
+
+    private String toAddress;
+
+    private String transactionContent;
+
+    private String extra;
+
+    public byte[] getRawFromAddress() {
+        byte[] rawID = HexUtil.decodeHex(this.fromAddress);
+        if (rawID.length != IDENTITY_LENGTH) {
+            throw new AntChainBridgeCommonsException(
+                    CommonsErrorCodeEnum.CROSS_CHAIN_IDENTITY_DECODE_ERROR,
+                    String.format("expected id with length %d but got %d", IDENTITY_LENGTH, rawID.length)
+            );
+        }
+        return rawID;
+    }
+
+    public byte[] getRawToAddress() {
+        byte[] rawID = HexUtil.decodeHex(this.toAddress);
+        if (rawID.length != IDENTITY_LENGTH) {
+            throw new AntChainBridgeCommonsException(
+                    CommonsErrorCodeEnum.CROSS_CHAIN_IDENTITY_DECODE_ERROR,
+                    String.format("expected id with length %d but got %d", IDENTITY_LENGTH, rawID.length)
+            );
+        }
+        return rawID;
+    }
+
+    public void setFromAddressFromBytes(byte[] fromAddress) {
+        if (fromAddress.length != IDENTITY_LENGTH) {
+            throw new AntChainBridgeCommonsException(
+                    CommonsErrorCodeEnum.CROSS_CHAIN_IDENTITY_DECODE_ERROR,
+                    String.format("expected id with length %d but got %d", IDENTITY_LENGTH, fromAddress.length)
+            );
+        }
+        this.fromAddress = HexUtil.encodeHexStr(fromAddress);
+    }
+
+    public void setToAddressFromBytes(byte[] toAddress) {
+        if (toAddress.length != IDENTITY_LENGTH) {
+            throw new AntChainBridgeCommonsException(
+                    CommonsErrorCodeEnum.CROSS_CHAIN_IDENTITY_DECODE_ERROR,
+                    String.format("expected id with length %d but got %d", IDENTITY_LENGTH, toAddress.length)
+            );
+        }
+        this.toAddress = HexUtil.encodeHexStr(toAddress);
+    }
+}
diff --git a/acb-sdk/antchain-bridge-commons/src/main/java/com/alipay/antchain/bridge/commons/core/monitor/IMonitorMessage.java b/acb-sdk/antchain-bridge-commons/src/main/java/com/alipay/antchain/bridge/commons/core/monitor/IMonitorMessage.java
new file mode 100755
index 00000000..abff5991
--- /dev/null
+++ b/acb-sdk/antchain-bridge-commons/src/main/java/com/alipay/antchain/bridge/commons/core/monitor/IMonitorMessage.java
@@ -0,0 +1,12 @@
+package com.alipay.antchain.bridge.commons.core.monitor;
+
+import com.alipay.antchain.bridge.commons.core.base.IMessage;
+
+public interface IMonitorMessage extends IMessage {
+
+    int getMonitorType();
+
+    String getMonitorMsg();
+
+    byte[] getPayload();
+}
diff --git a/acb-sdk/antchain-bridge-commons/src/main/java/com/alipay/antchain/bridge/commons/core/monitor/IMonitorOrder.java b/acb-sdk/antchain-bridge-commons/src/main/java/com/alipay/antchain/bridge/commons/core/monitor/IMonitorOrder.java
new file mode 100644
index 00000000..2483ef0d
--- /dev/null
+++ b/acb-sdk/antchain-bridge-commons/src/main/java/com/alipay/antchain/bridge/commons/core/monitor/IMonitorOrder.java
@@ -0,0 +1,25 @@
+package com.alipay.antchain.bridge.commons.core.monitor;
+
+import com.alipay.antchain.bridge.commons.core.base.IMessage;
+
+public interface IMonitorOrder extends IMessage {
+
+    String getProduct();
+
+    String getDomain();
+
+    long getMonitorOrderType();
+
+    String getSenderDomain();
+
+    String getFromAddress();
+
+    String getReceiverDomain();
+
+    String getToAddress();
+
+    String getTransactionContent();
+
+    String getExtra();
+
+}
diff --git a/acb-sdk/antchain-bridge-commons/src/main/java/com/alipay/antchain/bridge/commons/core/monitor/MonitorMessageFactory.java b/acb-sdk/antchain-bridge-commons/src/main/java/com/alipay/antchain/bridge/commons/core/monitor/MonitorMessageFactory.java
new file mode 100755
index 00000000..3451e7cc
--- /dev/null
+++ b/acb-sdk/antchain-bridge-commons/src/main/java/com/alipay/antchain/bridge/commons/core/monitor/MonitorMessageFactory.java
@@ -0,0 +1,39 @@
+package com.alipay.antchain.bridge.commons.core.monitor;
+
+import com.alipay.antchain.bridge.commons.exception.AntChainBridgeCommonsException;
+import com.alipay.antchain.bridge.commons.exception.CommonsErrorCodeEnum;
+
+public class MonitorMessageFactory {
+
+    public static IMonitorMessage createMonitorMessage(byte[] rawMessage) {
+        IMonitorMessage monitorMessage = createAbstractMonitorMessage(MonitorMessageV1.MY_VERSION);
+        monitorMessage.decode(rawMessage);
+        return monitorMessage;
+    }
+
+    public static IMonitorMessage createMonitorMessage(int version, int monitorType, String monitorMsg, byte[] payload) {
+        AbstractMonitorMessage monitorMessage = createAbstractMonitorMessage(version);
+
+        monitorMessage.setMonitorType(monitorType);
+        monitorMessage.setMonitorMsg(monitorMsg);
+        monitorMessage.setPayload(payload);
+
+        return monitorMessage;
+    }
+
+    public static AbstractMonitorMessage createAbstractMonitorMessage(int version) {
+        AbstractMonitorMessage monitorMessage;
+        switch (version) {
+            case MonitorMessageV1.MY_VERSION:
+                monitorMessage = new MonitorMessageV1();
+                break;
+            default:
+                throw new AntChainBridgeCommonsException(
+                        CommonsErrorCodeEnum.INCORRECT_MONITOR_MESSAGE_ERROR,
+                        String.format("wrong version: %d", version)
+                );
+        }
+        return monitorMessage;
+    }
+
+}
diff --git a/acb-sdk/antchain-bridge-commons/src/main/java/com/alipay/antchain/bridge/commons/core/monitor/MonitorMessageV1.java b/acb-sdk/antchain-bridge-commons/src/main/java/com/alipay/antchain/bridge/commons/core/monitor/MonitorMessageV1.java
new file mode 100755
index 00000000..dfdfde20
--- /dev/null
+++ b/acb-sdk/antchain-bridge-commons/src/main/java/com/alipay/antchain/bridge/commons/core/monitor/MonitorMessageV1.java
@@ -0,0 +1,109 @@
+package com.alipay.antchain.bridge.commons.core.monitor;
+
+import cn.hutool.core.util.ByteUtil;
+import com.alipay.antchain.bridge.commons.exception.AntChainBridgeCommonsException;
+import com.alipay.antchain.bridge.commons.exception.CommonsErrorCodeEnum;
+
+import java.nio.ByteOrder;
+import java.nio.charset.StandardCharsets;
+
+public class MonitorMessageV1 extends AbstractMonitorMessage {
+
+    public static final int MY_VERSION = 1;
+
+    @Override
+    public void decode(byte[] rawMessage) {
+        int offset = rawMessage.length;
+
+        offset = extractMonitorType(rawMessage, offset);
+        offset = extractMonitorMsg(rawMessage, offset);
+        extractPayload(rawMessage, offset);
+    }
+
+    public int extractMonitorType(byte[] rawMessage, int offset) {
+        offset -= 4;
+        byte[] rawSeq = new byte[4];
+        System.arraycopy(rawMessage, offset, rawSeq, 0, 4);
+        this.setMonitorType(ByteUtil.bytesToInt(rawSeq, ByteOrder.BIG_ENDIAN));
+
+        return offset;
+    }
+
+    public int extractMonitorMsg(byte[] rawMessage, int offset) {
+        offset -= 4;
+        byte[] rawMonitorMsgLen = new byte[4];
+        System.arraycopy(rawMessage, offset, rawMonitorMsgLen, 0, 4);
+
+        byte[] monitorMsg = new byte[ByteUtil.bytesToInt(rawMonitorMsgLen, ByteOrder.BIG_ENDIAN)];
+        offset -= monitorMsg.length;
+        if (offset < 0) {
+            throw new AntChainBridgeCommonsException(
+                    CommonsErrorCodeEnum.MONITOR_MESSAGE_DECODE_ERROR,
+                    "length of error message in MonitorV1 is incorrect"
+            );
+        }
+        System.arraycopy(rawMessage, offset, monitorMsg, 0, monitorMsg.length);
+        this.setMonitorMsg(new String(monitorMsg));
+
+        return offset;
+    }
+
+    public int extractPayload(byte[] rawMessage, int offset) {
+        offset -= 4;
+        byte[] rawPayloadLen = new byte[4];
+        System.arraycopy(rawMessage, offset, rawPayloadLen, 0, 4);
+
+        byte[] payload = new byte[ByteUtil.bytesToInt(rawPayloadLen, ByteOrder.BIG_ENDIAN)];
+        offset -= payload.length;
+        if (offset < 0) {
+            throw new AntChainBridgeCommonsException(
+                    CommonsErrorCodeEnum.MONITOR_MESSAGE_DECODE_ERROR,
+                    "wrong payload or length of payload in MonitorV1"
+            );
+        }
+        System.arraycopy(rawMessage, offset, payload, 0, payload.length);
+        this.setPayload(payload);
+
+        return offset;
+    }
+
+    @Override
+    public byte[] encode() {
+        int rawMessageLen = 12 + this.getMonitorMsg().getBytes(StandardCharsets.UTF_8).length + this.getPayload().length;
+        byte[] rawMessage = new byte[rawMessageLen];
+
+        int offset = putMonitorType(rawMessage, rawMessageLen);
+        offset = putMonitorMsg(rawMessage, offset);
+        putPayload(rawMessage, offset);
+
+        return rawMessage;
+    }
+
+    private int putMonitorType(byte[] rawMessage, int offset) {
+        offset -= 4;
+        System.arraycopy(ByteUtil.intToBytes(this.getMonitorType(), ByteOrder.BIG_ENDIAN), 0, rawMessage, offset, 4);
+
+        return offset;
+    }
+
+    private int putMonitorMsg(byte[] rawMessage, int offset) {
+        offset -= 4;
+        System.arraycopy(ByteUtil.intToBytes(this.getMonitorMsg().getBytes(StandardCharsets.UTF_8).length, ByteOrder.BIG_ENDIAN), 0, rawMessage, offset, 4);
+
+        offset -= this.getMonitorMsg().length();
+        System.arraycopy(this.getMonitorMsg().getBytes(), 0, rawMessage, offset, this.getMonitorMsg().length());
+
+        return offset;
+    }
+
+    private int putPayload(byte[] rawMessage, int offset) {
+        offset -= 4;
+        System.arraycopy(ByteUtil.intToBytes(this.getPayload().length, ByteOrder.BIG_ENDIAN), 0, rawMessage, offset, 4);
+
+        offset -= this.getPayload().length;
+        System.arraycopy(this.getPayload(), 0, rawMessage, offset, this.getPayload().length);
+
+        return offset;
+    }
+
+}
diff --git a/acb-sdk/antchain-bridge-commons/src/main/java/com/alipay/antchain/bridge/commons/core/monitor/MonitorOrderFactory.java b/acb-sdk/antchain-bridge-commons/src/main/java/com/alipay/antchain/bridge/commons/core/monitor/MonitorOrderFactory.java
new file mode 100644
index 00000000..913874d8
--- /dev/null
+++ b/acb-sdk/antchain-bridge-commons/src/main/java/com/alipay/antchain/bridge/commons/core/monitor/MonitorOrderFactory.java
@@ -0,0 +1,41 @@
+package com.alipay.antchain.bridge.commons.core.monitor;
+
+import com.alipay.antchain.bridge.commons.exception.AntChainBridgeCommonsException;
+import com.alipay.antchain.bridge.commons.exception.CommonsErrorCodeEnum;
+
+public class MonitorOrderFactory {
+
+    public static IMonitorOrder createMonitorOrder(int version, String product, String domain, long monitorOrderType,
+                                                   String senderDomain, String fromAddress,
+                                                   String receiverDomain, String toAddress,
+                                                   String transactionContent, String extra) {
+        AbstractMonitorOrder monitorOrder = createAbstractMonitorOrder(version);
+
+        monitorOrder.setProduct(product);
+        monitorOrder.setDomain(domain);
+        monitorOrder.setMonitorOrderType(monitorOrderType);
+        monitorOrder.setSenderDomain(senderDomain);
+        monitorOrder.setFromAddress(fromAddress);
+        monitorOrder.setReceiverDomain(receiverDomain);
+        monitorOrder.setToAddress(toAddress);
+        monitorOrder.setTransactionContent(transactionContent);
+        monitorOrder.setExtra(extra);
+
+        return monitorOrder;
+    }
+
+    private static AbstractMonitorOrder createAbstractMonitorOrder(int version) {
+        AbstractMonitorOrder monitorOrder;
+        switch (version) {
+            case MonitorOrderV1.MY_VERSION:
+                monitorOrder = new MonitorOrderV1();
+                break;
+            default:
+                throw new AntChainBridgeCommonsException(
+                        CommonsErrorCodeEnum.INCORRECT_MONITOR_ORDER,
+                        String.format("wrong version: %d", version)
+                );
+        }
+        return monitorOrder;
+    }
+}
diff --git a/acb-sdk/antchain-bridge-commons/src/main/java/com/alipay/antchain/bridge/commons/core/monitor/MonitorOrderV1.java b/acb-sdk/antchain-bridge-commons/src/main/java/com/alipay/antchain/bridge/commons/core/monitor/MonitorOrderV1.java
new file mode 100644
index 00000000..f07fc509
--- /dev/null
+++ b/acb-sdk/antchain-bridge-commons/src/main/java/com/alipay/antchain/bridge/commons/core/monitor/MonitorOrderV1.java
@@ -0,0 +1,123 @@
+package com.alipay.antchain.bridge.commons.core.monitor;
+
+import cn.hutool.core.util.ByteUtil;
+
+import java.nio.ByteOrder;
+import java.nio.charset.StandardCharsets;
+
+public class MonitorOrderV1 extends AbstractMonitorOrder {
+
+    public static final int MY_VERSION = 1;
+
+    // no need to do this
+    @Override
+    public void decode(byte[] rawMessage) { }
+
+    @Override
+    public byte[] encode() {
+        // monitorOrderType only needs 4 bytes
+        int rawMessageLen = 28 + this.getProduct().getBytes(StandardCharsets.UTF_8).length +
+                this.getDomain().getBytes(StandardCharsets.UTF_8).length +
+                this.getSenderDomain().getBytes(StandardCharsets.UTF_8).length +
+                this.getRawFromAddress().length +
+                this.getReceiverDomain().getBytes(StandardCharsets.UTF_8).length +
+                this.getRawToAddress().length +
+                this.getTransactionContent().getBytes(StandardCharsets.UTF_8).length +
+                this.getExtra().getBytes(StandardCharsets.UTF_8).length;
+        byte[] rawMessage = new byte[rawMessageLen];
+
+        int offset = putProduct(rawMessage, rawMessageLen);
+        offset = putDomain(rawMessage, offset);
+        offset = putMonitorOrderType(rawMessage, offset);
+        offset = putSenderDomain(rawMessage, offset);
+        offset = putRawFromAddress(rawMessage, offset);
+        offset = putReceiverDomain(rawMessage, offset);
+        offset = putRawToAddress(rawMessage, offset);
+        offset = putTransactionContent(rawMessage, offset);
+        putExtra(rawMessage, offset);
+
+        return rawMessage;
+    }
+
+    private int putProduct(byte[] rawMessage, int offset) {
+        offset -= 4;
+        System.arraycopy(ByteUtil.intToBytes(this.getProduct().getBytes(StandardCharsets.UTF_8).length, ByteOrder.BIG_ENDIAN), 0, rawMessage, offset, 4);
+
+        offset -= this.getProduct().length();
+        System.arraycopy(this.getProduct().getBytes(), 0, rawMessage, offset, this.getProduct().length());
+
+        return offset;
+    }
+
+    private int putDomain(byte[] rawMessage, int offset) {
+        offset -= 4;
+        System.arraycopy(ByteUtil.intToBytes(this.getDomain().getBytes(StandardCharsets.UTF_8).length, ByteOrder.BIG_ENDIAN), 0, rawMessage, offset, 4);
+
+        offset -= this.getDomain().length();
+        System.arraycopy(this.getDomain().getBytes(), 0, rawMessage, offset, this.getDomain().length());
+
+        return offset;
+    }
+
+    private int putMonitorOrderType(byte[] rawMessage, int offset) {
+        offset -= 4;
+        System.arraycopy(ByteUtil.intToBytes((int)this.getMonitorOrderType(), ByteOrder.BIG_ENDIAN), 0, rawMessage, offset, 4);
+
+        return offset;
+    }
+
+    private int putSenderDomain(byte[] rawMessage, int offset) {
+        offset -= 4;
+        System.arraycopy(ByteUtil.intToBytes(this.getSenderDomain().getBytes(StandardCharsets.UTF_8).length, ByteOrder.BIG_ENDIAN), 0, rawMessage, offset, 4);
+
+        offset -= this.getSenderDomain().length();
+        System.arraycopy(this.getSenderDomain().getBytes(), 0, rawMessage, offset, this.getSenderDomain().length());
+
+        return offset;
+    }
+
+    private int putRawFromAddress(byte[] rawMessage, int offset) {
+        offset -= 32;
+        System.arraycopy(this.getRawFromAddress(), 0, rawMessage, offset, 32);
+
+        return offset;
+    }
+
+    private int putReceiverDomain(byte[] rawMessage, int offset) {
+        offset -= 4;
+        System.arraycopy(ByteUtil.intToBytes(this.getReceiverDomain().getBytes(StandardCharsets.UTF_8).length, ByteOrder.BIG_ENDIAN), 0, rawMessage, offset, 4);
+
+        offset -= this.getReceiverDomain().length();
+        System.arraycopy(this.getReceiverDomain().getBytes(), 0, rawMessage, offset, this.getReceiverDomain().length());
+
+        return offset;
+    }
+
+    private int putRawToAddress(byte[] rawMessage, int offset) {
+        offset -= 32;
+        System.arraycopy(this.getRawToAddress(), 0, rawMessage, offset, 32);
+
+        return offset;
+    }
+
+    private int putTransactionContent(byte[] rawMessage, int offset) {
+        offset -= 4;
+        System.arraycopy(ByteUtil.intToBytes(this.getTransactionContent().getBytes(StandardCharsets.UTF_8).length, ByteOrder.BIG_ENDIAN), 0, rawMessage, offset, 4);
+
+        offset -= this.getTransactionContent().length();
+        System.arraycopy(this.getTransactionContent().getBytes(), 0, rawMessage, offset, this.getTransactionContent().length());
+
+        return offset;
+    }
+
+    private int putExtra(byte[] rawMessage, int offset) {
+        offset -= 4;
+        System.arraycopy(ByteUtil.intToBytes(this.getExtra().getBytes(StandardCharsets.UTF_8).length, ByteOrder.BIG_ENDIAN), 0, rawMessage, offset, 4);
+
+        offset -= this.getExtra().length();
+        System.arraycopy(this.getExtra().getBytes(), 0, rawMessage, offset, this.getExtra().length());
+
+        return offset;
+    }
+
+}
diff --git a/acb-sdk/antchain-bridge-commons/src/main/java/com/alipay/antchain/bridge/commons/exception/CommonsErrorCodeEnum.java b/acb-sdk/antchain-bridge-commons/src/main/java/com/alipay/antchain/bridge/commons/exception/CommonsErrorCodeEnum.java
index 19690cd3..1a0fdc4c 100644
--- a/acb-sdk/antchain-bridge-commons/src/main/java/com/alipay/antchain/bridge/commons/exception/CommonsErrorCodeEnum.java
+++ b/acb-sdk/antchain-bridge-commons/src/main/java/com/alipay/antchain/bridge/commons/exception/CommonsErrorCodeEnum.java
@@ -140,7 +140,22 @@ public enum CommonsErrorCodeEnum {
     /**
      *
      */
-    BCDNS_BID_PUBLIC_KEY_ALGO_NOT_SUPPORT("0706", "BID pubkey algo not support");
+    BCDNS_BID_PUBLIC_KEY_ALGO_NOT_SUPPORT("0706", "BID pubkey algo not support"),
+
+    /**
+     * Something wrong about {@code MonitorMessage}, like version, etc.
+     */
+    INCORRECT_MONITOR_MESSAGE_ERROR("0801", "wrong monitor"),
+
+    /**
+     * Code shows where decode {@code MonitorMessage} failed
+     */
+    MONITOR_MESSAGE_DECODE_ERROR("0802", "monitor decode failed"),
+
+    /**
+     * Something wrong about {@code MonitorOrder}, like version, etc.
+     */
+    INCORRECT_MONITOR_ORDER("0803", "wrong monitor order");
 
     /**
      * Error code for errors happened in project {@code antchain-bridge-commons}
diff --git a/acb-sdk/antchain-bridge-spi/src/main/java/com/alipay/antchain/bridge/plugins/spi/bbc/core/write/IAntChainBridgeDataWriter.java b/acb-sdk/antchain-bridge-spi/src/main/java/com/alipay/antchain/bridge/plugins/spi/bbc/core/write/IAntChainBridgeDataWriter.java
index 77a4196b..c4af44ce 100644
--- a/acb-sdk/antchain-bridge-spi/src/main/java/com/alipay/antchain/bridge/plugins/spi/bbc/core/write/IAntChainBridgeDataWriter.java
+++ b/acb-sdk/antchain-bridge-spi/src/main/java/com/alipay/antchain/bridge/plugins/spi/bbc/core/write/IAntChainBridgeDataWriter.java
@@ -40,7 +40,7 @@
  *     interfaces like {@link IAMWriter}, {@link ISDPWriter}.
  * 

*/ -public interface IAntChainBridgeDataWriter extends IAMWriter, ISDPWriter { +public interface IAntChainBridgeDataWriter extends IAMWriter, ISDPWriter, IMonitorWriter { /** * Set up the AuthMessage contract. @@ -65,6 +65,10 @@ public interface IAntChainBridgeDataWriter extends IAMWriter, ISDPWriter { */ void setupSDPMessageContract(); + /** + * Set up the monitor contracts, including monitor and monitorVerifier contract + */ + void setupMonitorContract(); /** * Set up the PTC contracts. For example PTCHub and its verify contracts */ diff --git a/acb-sdk/antchain-bridge-spi/src/main/java/com/alipay/antchain/bridge/plugins/spi/bbc/core/write/IMonitorWriter.java b/acb-sdk/antchain-bridge-spi/src/main/java/com/alipay/antchain/bridge/plugins/spi/bbc/core/write/IMonitorWriter.java new file mode 100755 index 00000000..7d011b87 --- /dev/null +++ b/acb-sdk/antchain-bridge-spi/src/main/java/com/alipay/antchain/bridge/plugins/spi/bbc/core/write/IMonitorWriter.java @@ -0,0 +1,18 @@ +package com.alipay.antchain.bridge.plugins.spi.bbc.core.write; + +import com.alipay.antchain.bridge.commons.core.base.CrossChainMessageReceipt; + +/** + * Through {@code IMonitorWriter}, you can write data + * to the storage of the MonitorContract. + */ +public interface IMonitorWriter { + + void setProtocolInMonitor(String contractAddress); + + void setMonitorControl(int monitorType); + + void setPtcHubInMonitorVerifier(String contractAddress); + + CrossChainMessageReceipt relayMonitorOrder(String committeeId, String signAlgo, byte[] rawProof, byte[] rawMonitorOrder); +} \ No newline at end of file diff --git a/acb-sdk/antchain-bridge-spi/src/main/java/com/alipay/antchain/bridge/plugins/spi/bbc/core/write/ISDPWriter.java b/acb-sdk/antchain-bridge-spi/src/main/java/com/alipay/antchain/bridge/plugins/spi/bbc/core/write/ISDPWriter.java index 798c54a7..534bba44 100644 --- a/acb-sdk/antchain-bridge-spi/src/main/java/com/alipay/antchain/bridge/plugins/spi/bbc/core/write/ISDPWriter.java +++ b/acb-sdk/antchain-bridge-spi/src/main/java/com/alipay/antchain/bridge/plugins/spi/bbc/core/write/ISDPWriter.java @@ -41,4 +41,6 @@ public interface ISDPWriter { * @param domain the domain value */ void setLocalDomain(String domain); + + void setMonitorContract(String contractAddress); } diff --git a/acb-sdk/pluginset/ethereum2/offchain-plugin/pom.xml b/acb-sdk/pluginset/ethereum2/offchain-plugin/pom.xml index 13847e02..1a7c0aa8 100644 --- a/acb-sdk/pluginset/ethereum2/offchain-plugin/pom.xml +++ b/acb-sdk/pluginset/ethereum2/offchain-plugin/pom.xml @@ -236,10 +236,12 @@ + Monitor AuthMsg SDPMsg PtcHub CommitteePtcVerifier + MonitorVerifier AppContract ProxyAdmin TransparentUpgradeableProxy diff --git a/acb-sdk/pluginset/ethereum2/offchain-plugin/src/main/java/com/alipay/antchain/bridge/plugins/ethereum2/EthereumBBCService.java b/acb-sdk/pluginset/ethereum2/offchain-plugin/src/main/java/com/alipay/antchain/bridge/plugins/ethereum2/EthereumBBCService.java index 12df3c6a..92991dfa 100644 --- a/acb-sdk/pluginset/ethereum2/offchain-plugin/src/main/java/com/alipay/antchain/bridge/plugins/ethereum2/EthereumBBCService.java +++ b/acb-sdk/pluginset/ethereum2/offchain-plugin/src/main/java/com/alipay/antchain/bridge/plugins/ethereum2/EthereumBBCService.java @@ -26,10 +26,7 @@ import cn.hutool.core.util.StrUtil; import com.alibaba.fastjson.JSON; import com.alipay.antchain.bridge.commons.bbc.AbstractBBCContext; -import com.alipay.antchain.bridge.commons.bbc.syscontract.AuthMessageContract; -import com.alipay.antchain.bridge.commons.bbc.syscontract.ContractStatusEnum; -import com.alipay.antchain.bridge.commons.bbc.syscontract.PTCContract; -import com.alipay.antchain.bridge.commons.bbc.syscontract.SDPContract; +import com.alipay.antchain.bridge.commons.bbc.syscontract.*; import com.alipay.antchain.bridge.commons.bcdns.AbstractCrossChainCertificate; import com.alipay.antchain.bridge.commons.bcdns.CrossChainCertificateTypeEnum; import com.alipay.antchain.bridge.commons.bcdns.utils.CrossChainCertificateUtil; @@ -102,6 +99,7 @@ public void startup(AbstractBBCContext abstractBBCContext) { authMessageContract.setContractAddress(this.config.getAmContractAddressDeployed()); authMessageContract.setStatus(ContractStatusEnum.CONTRACT_DEPLOYED); this.bbcContext.setAuthMessageContract(authMessageContract); + getBBCLogger().info("set the pre-deployed am contracts into context: {}: ", this.config.getAmContractAddressDeployed()); } if (ObjectUtil.isNull(abstractBBCContext.getSdpContract()) @@ -110,6 +108,16 @@ public void startup(AbstractBBCContext abstractBBCContext) { sdpContract.setContractAddress(this.config.getSdpContractAddressDeployed()); sdpContract.setStatus(ContractStatusEnum.CONTRACT_DEPLOYED); this.bbcContext.setSdpContract(sdpContract); + getBBCLogger().info("set the pre-deployed sdp contracts into context: {}: ", this.config.getSdpContractAddressDeployed()); + } + + if (ObjectUtil.isNull(abstractBBCContext.getMonitorContract()) + && StrUtil.isNotEmpty(this.config.getMonitorContractAddressDeployed())) { + MonitorContract monitorContract = new MonitorContract(); + monitorContract.setContractAddress(this.config.getMonitorContractAddressDeployed()); + monitorContract.setStatus(ContractStatusEnum.CONTRACT_DEPLOYED); + this.bbcContext.setMonitorContract(monitorContract); + getBBCLogger().info("set the pre-deployed monitor contracts into context: {}: ", this.config.getMonitorContractAddressDeployed()); } if (ObjectUtil.isNull(abstractBBCContext.getPtcContract()) @@ -118,6 +126,7 @@ public void startup(AbstractBBCContext abstractBBCContext) { ptcContract.setContractAddress(this.config.getPtcHubContractAddressDeployed()); ptcContract.setStatus(ContractStatusEnum.CONTRACT_READY); this.bbcContext.setPtcContract(ptcContract); + getBBCLogger().info("set the pre-deployed ptc contracts into context: {}: ", this.config.getPtcHubContractAddressDeployed()); } if (ObjectUtil.isEmpty(this.config.getProxyAdmin()) && this.config.isUpgradableContracts()) { @@ -140,11 +149,13 @@ public AbstractBBCContext getContext() { throw new RuntimeException("empty bbc context"); } - getBBCLogger().debug("ETH BBCService context (amAddr: {}, amStatus: {}, sdpAddr: {}, sdpStatus: {})", + getBBCLogger().debug("ETH BBCService context (amAddr: {}, amStatus: {}, sdpAddr: {}, sdpStatus: {}, monitorAddr: {}, monitorStatus: {})", this.bbcContext.getAuthMessageContract() != null ? this.bbcContext.getAuthMessageContract().getContractAddress() : "", this.bbcContext.getAuthMessageContract() != null ? this.bbcContext.getAuthMessageContract().getStatus() : "", this.bbcContext.getSdpContract() != null ? this.bbcContext.getSdpContract().getContractAddress() : "", - this.bbcContext.getSdpContract() != null ? this.bbcContext.getSdpContract().getStatus() : "" + this.bbcContext.getSdpContract() != null ? this.bbcContext.getSdpContract().getStatus() : "", + this.bbcContext.getMonitorContract() != null ? this.bbcContext.getMonitorContract().getContractAddress() : "", + this.bbcContext.getMonitorContract() != null ? this.bbcContext.getMonitorContract().getStatus() : "" ); this.bbcContext.setConfForBlockchainClient(this.config.toJsonString().getBytes()); @@ -293,6 +304,32 @@ public void setupSDPMessageContract() { getBBCLogger().info("setup sdp contract successful: {}", sdpContractAddr); } + @Override + public void setupMonitorContract() { + // 1. check context + if (ObjectUtil.isNull(this.bbcContext)) { + throw new RuntimeException("empty bbc context"); + } + if (this.config.isUpgradableContracts() && StrUtil.isEmpty(this.config.getProxyAdmin())) { + throw new RuntimeException("empty proxy admin"); + } + if (ObjectUtil.isNotNull(this.bbcContext.getMonitorContract()) + && StrUtil.isNotEmpty(this.bbcContext.getMonitorContract().getContractAddress())) { + // If the contract has been pre-deployed and the contract address is configured in the configuration file, + // there is no need to redeploy. + return; + } + + // 2. deploy contract + var monitorContractAddr = acbEthClient.deployMonitorContract(acbEthClient.deployMonitorVerifierContract()); + + MonitorContract monitorContract = new MonitorContract(); + monitorContract.setContractAddress(monitorContractAddr); + monitorContract.setStatus(ContractStatusEnum.CONTRACT_DEPLOYED); + bbcContext.setMonitorContract(monitorContract); + getBBCLogger().info("setup monitor contract successful: {}", monitorContractAddr); + } + @Override public long querySDPMessageSeq(String senderDomain, String senderID, String receiverDomain, String receiverID) { // 1. check context @@ -347,7 +384,38 @@ public void setAmContract(String contractAddress) { // 4. update sdp contract status try { if (!StrUtil.isEmpty(acbEthClient.getAmContractFromSdp(this.bbcContext.getSdpContract().getContractAddress())) - && !isByteArrayZero(acbEthClient.getLocalDomainFromSdp(this.bbcContext.getSdpContract().getContractAddress()))) { + && !isByteArrayZero(acbEthClient.getLocalDomainFromSdp(this.bbcContext.getSdpContract().getContractAddress())) + && !"0x0000000000000000000000000000000000000000".equalsIgnoreCase(acbEthClient.getMonitorContractFromSdp(this.bbcContext.getSdpContract().getContractAddress())) + ) { + this.bbcContext.getSdpContract().setStatus(ContractStatusEnum.CONTRACT_READY); + } + } catch (Exception e) { + throw new RuntimeException( + String.format( + "failed to update sdp contract status (address: %s)", + this.bbcContext.getSdpContract().getContractAddress() + ), e); + } + } + + @Override + public void setMonitorContract(String contractAddress) { + // 1. check context + if (ObjectUtil.isNull(this.bbcContext)) { + throw new RuntimeException("empty bbc context"); + } + if (ObjectUtil.isNull(this.bbcContext.getSdpContract())) { + throw new RuntimeException("empty sdp contract in bbc context"); + } + + acbEthClient.setMonitorContractToSdp(this.bbcContext.getSdpContract().getContractAddress(), contractAddress); + + // 4. update sdp contract status + try { + if (!StrUtil.isEmpty(acbEthClient.getAmContractFromSdp(this.bbcContext.getSdpContract().getContractAddress())) + && !isByteArrayZero(acbEthClient.getLocalDomainFromSdp(this.bbcContext.getSdpContract().getContractAddress())) + && !"0x0000000000000000000000000000000000000000".equalsIgnoreCase(acbEthClient.getMonitorContractFromSdp(this.bbcContext.getSdpContract().getContractAddress())) + ) { this.bbcContext.getSdpContract().setStatus(ContractStatusEnum.CONTRACT_READY); } } catch (Exception e) { @@ -374,7 +442,9 @@ public void setLocalDomain(String domain) { // 4. update sdp contract status try { if (!StrUtil.isEmpty(acbEthClient.getAmContractFromSdp(this.bbcContext.getSdpContract().getContractAddress())) - && !isByteArrayZero(acbEthClient.getLocalDomainFromSdp(this.bbcContext.getSdpContract().getContractAddress()))) { + && !isByteArrayZero(acbEthClient.getLocalDomainFromSdp(this.bbcContext.getSdpContract().getContractAddress())) + && !"0x0000000000000000000000000000000000000000".equalsIgnoreCase(acbEthClient.getMonitorContractFromSdp(this.bbcContext.getSdpContract().getContractAddress())) + ) { this.bbcContext.getSdpContract().setStatus(ContractStatusEnum.CONTRACT_READY); } } catch (Exception e) { @@ -386,6 +456,120 @@ public void setLocalDomain(String domain) { } } + @Override + public void setProtocolInMonitor(String protocolAddress) { + // 1. check context + if (ObjectUtil.isNull(this.bbcContext)) { + throw new RuntimeException("empty bbc context"); + } + if (ObjectUtil.isNull(this.bbcContext.getMonitorContract())) { + throw new RuntimeException("empty monitor contract in bbc context"); + } + + acbEthClient.setProtocolToMonitor(this.bbcContext.getMonitorContract().getContractAddress(), protocolAddress); + + // 4. update monitor contract status + try { + String monitorVerifier = acbEthClient.getMonitorVerifierFromMonitor(this.bbcContext.getMonitorContract().getContractAddress()); + if (!"0x0000000000000000000000000000000000000000".equalsIgnoreCase(monitorVerifier) + && !"0x0000000000000000000000000000000000000000".equalsIgnoreCase(acbEthClient.getPtcHubFromMonitorVerifier(monitorVerifier))) { + if (!"0x0000000000000000000000000000000000000000".equalsIgnoreCase(acbEthClient.getProtocolFromMonitor(this.bbcContext.getMonitorContract().getContractAddress())) + && acbEthClient.getMonitorControlFromMonitor(this.bbcContext.getMonitorContract().getContractAddress()) != 0) { + this.bbcContext.getMonitorContract().setStatus(ContractStatusEnum.CONTRACT_READY); + } + } + } catch (Exception e) { + throw new RuntimeException( + String.format( + "failed to update monitor contract status (address: %s)", + this.bbcContext.getMonitorContract().getContractAddress() + ), e); + } + } + + @Override + public void setMonitorControl(int monitorType) { + // 1. check context + if (ObjectUtil.isNull(this.bbcContext)) { + throw new RuntimeException("empty bbc context"); + } + if (ObjectUtil.isNull(this.bbcContext.getMonitorContract())) { + throw new RuntimeException("empty monitor contract in bbc context"); + } + + acbEthClient.setMonitorControl(this.bbcContext.getMonitorContract().getContractAddress(), monitorType); + + // 4. update monitor contract status + try { + String monitorVerifier = acbEthClient.getMonitorVerifierFromMonitor(this.bbcContext.getMonitorContract().getContractAddress()); + if (!"0x0000000000000000000000000000000000000000".equalsIgnoreCase(monitorVerifier) + && !"0x0000000000000000000000000000000000000000".equalsIgnoreCase(acbEthClient.getPtcHubFromMonitorVerifier(monitorVerifier))) { + if (!"0x0000000000000000000000000000000000000000".equalsIgnoreCase(acbEthClient.getProtocolFromMonitor(this.bbcContext.getMonitorContract().getContractAddress())) + && acbEthClient.getMonitorControlFromMonitor(this.bbcContext.getMonitorContract().getContractAddress()) != 0) { + this.bbcContext.getMonitorContract().setStatus(ContractStatusEnum.CONTRACT_READY); + } + } + } catch (Exception e) { + throw new RuntimeException( + String.format( + "failed to update monitor contract status (address: %s)", + this.bbcContext.getMonitorContract().getContractAddress() + ), e); + } + } + + @Override + public void setPtcHubInMonitorVerifier(String contractAddress) { + // 1. check context + if (ObjectUtil.isNull(this.bbcContext)) { + throw new RuntimeException("empty bbc context"); + } + if (ObjectUtil.isNull(this.bbcContext.getMonitorContract())) { + throw new RuntimeException("empty monitor contract in bbc context"); + } + if (ObjectUtil.isNull(this.bbcContext.getPtcContract())) { + throw new RuntimeException("empty PtcHub contract in bbc context"); + } + String monitorVerifier = acbEthClient.getMonitorVerifierFromMonitor(this.bbcContext.getMonitorContract().getContractAddress()); + if ("0x0000000000000000000000000000000000000000".equalsIgnoreCase(monitorVerifier)) { + throw new RuntimeException("not set monitor verifier contract in monitor contract yet"); + } + + acbEthClient.setPtcHubToMonitorVerifier(monitorVerifier, contractAddress); + + // 4. update monitor contract status + try { + if (!"0x0000000000000000000000000000000000000000".equalsIgnoreCase(acbEthClient.getPtcHubFromMonitorVerifier(monitorVerifier)) && + !"0x0000000000000000000000000000000000000000".equalsIgnoreCase(acbEthClient.getProtocolFromMonitor(this.bbcContext.getMonitorContract().getContractAddress())) + && acbEthClient.getMonitorControlFromMonitor(this.bbcContext.getMonitorContract().getContractAddress()) != 0 ) { + this.bbcContext.getMonitorContract().setStatus(ContractStatusEnum.CONTRACT_READY); + } + } catch (Exception e) { + throw new RuntimeException( + String.format( + "failed to update monitor contract status (address: %s)", + this.bbcContext.getMonitorContract().getContractAddress() + ), e); + } + + } + + @Override + public CrossChainMessageReceipt relayMonitorOrder(String committeeId, String signAlgo, byte[] rawProof, byte[] rawMonitorOrder) { + // 1. check context + if (ObjectUtil.isNull(this.bbcContext)) { + throw new RuntimeException("empty bbc context"); + } + if (ObjectUtil.isNull(this.bbcContext.getMonitorContract())) { + throw new RuntimeException("empty monitor contract in bbc context"); + } + + getBBCLogger().debug("relay MonitorOrder to {} ", this.bbcContext.getMonitorContract().getContractAddress()); + + return acbEthClient.relayMonitorOrderToMonitor(this.bbcContext.getMonitorContract().getContractAddress(), committeeId, signAlgo, rawProof, rawMonitorOrder); + } + + @Override public CrossChainMessageReceipt relayAuthMessage(byte[] rawMessage) { // 1. check context @@ -499,6 +683,7 @@ public boolean hasPTCVerifyAnchor(ObjectIdentity ptcOwnerOid, BigInteger version return acbEthClient.hasPTCVerifyAnchor(this.bbcContext.getPtcContract().getContractAddress(), ptcOwnerOid, version); } + // in this version, ptc contract needs to be deployed after monitor contract, since monitor verifier is necessary for the init @Override public void setupPTCContract() { // 1. check context @@ -525,7 +710,8 @@ public void setupPTCContract() { } // 2. deploy contract - String ptcHubContractAddr = acbEthClient.deployPtcHubContract(bcdnsRootCert, acbEthClient.deployCommitteeVerifierContract()); + String ptcHubContractAddr = acbEthClient.deployPtcHubContract(bcdnsRootCert, acbEthClient.deployCommitteeVerifierContract(), + acbEthClient.getMonitorVerifierFromMonitor(this.bbcContext.getMonitorContract().getContractAddress())); PTCContract ptcContract = new PTCContract(); ptcContract.setContractAddress(ptcHubContractAddr); diff --git a/acb-sdk/pluginset/ethereum2/offchain-plugin/src/main/java/com/alipay/antchain/bridge/plugins/ethereum2/conf/EthereumConfig.java b/acb-sdk/pluginset/ethereum2/offchain-plugin/src/main/java/com/alipay/antchain/bridge/plugins/ethereum2/conf/EthereumConfig.java index 41a482d1..5814323d 100644 --- a/acb-sdk/pluginset/ethereum2/offchain-plugin/src/main/java/com/alipay/antchain/bridge/plugins/ethereum2/conf/EthereumConfig.java +++ b/acb-sdk/pluginset/ethereum2/offchain-plugin/src/main/java/com/alipay/antchain/bridge/plugins/ethereum2/conf/EthereumConfig.java @@ -87,6 +87,9 @@ public enum CrossChainMessageScanPolicyEnum { @JSONField private String sdpContractAddressDeployed; + @JSONField + private String monitorContractAddressDeployed; + @JSONField private BlockHeightPolicyEnum blockHeightPolicy = BlockHeightPolicyEnum.FINALIZED; diff --git a/acb-sdk/pluginset/ethereum2/offchain-plugin/src/main/java/com/alipay/antchain/bridge/plugins/ethereum2/core/AcbEthClient.java b/acb-sdk/pluginset/ethereum2/offchain-plugin/src/main/java/com/alipay/antchain/bridge/plugins/ethereum2/core/AcbEthClient.java index 532adb5a..1de0c219 100644 --- a/acb-sdk/pluginset/ethereum2/offchain-plugin/src/main/java/com/alipay/antchain/bridge/plugins/ethereum2/core/AcbEthClient.java +++ b/acb-sdk/pluginset/ethereum2/offchain-plugin/src/main/java/com/alipay/antchain/bridge/plugins/ethereum2/core/AcbEthClient.java @@ -256,6 +256,190 @@ public String deploySdpContract() { return sdpMsg.getContractAddress(); } + public String deployMonitorContract(String monitorVerifierContractAddress) { + Monitor monitor; + try { + monitor = Monitor.deploy( + web3j, + rawTransactionManager, + new AcbGasProvider( + this.contractGasPriceProvider, + createDeployGasLimitProvider(Monitor.BINARY) + ) + ).send(); + } catch (Exception e) { + throw new RuntimeException("failed to deploy monitor", e); + } + + String monitorContractAddress = monitor.getContractAddress(); + + if (this.config.isUpgradableContracts()) { + TransparentUpgradeableProxy proxy; + try { + proxy = TransparentUpgradeableProxy.deploy( + web3j, + rawTransactionManager, + new AcbGasProvider( + this.contractGasPriceProvider, + createDeployGasLimitProvider( + TransparentUpgradeableProxy.BINARY + + FunctionEncoder.encodeConstructor(ListUtil.toList( + new org.web3j.abi.datatypes.Address(monitor.getContractAddress()), + new org.web3j.abi.datatypes.Address(this.config.getProxyAdmin()), + new DynamicBytes(HexUtil.decodeHex("e1c7392a")) + )) + ) + ), + BigInteger.ZERO, + monitor.getContractAddress(), + this.config.getProxyAdmin(), + HexUtil.decodeHex("e1c7392a") + ).send(); + } catch (Exception e) { + throw new RuntimeException("failed to deploy monitor contract", e); + } + getBbcLogger().info("deploy proxy contract for monitor: {}", proxy.getContractAddress()); + monitorContractAddress = proxy.getContractAddress(); + } + + // set monitor verifier to monitor + monitor = Monitor.load( + monitorContractAddress, + web3j, + rawTransactionManager, + new AcbGasProvider( + this.contractGasPriceProvider, + createEthCallGasLimitProvider( + monitorContractAddress, + new Function( + Monitor.FUNC_SETMONITORVERIFIER, + ListUtil.toList(new org.web3j.abi.datatypes.Address(monitorVerifierContractAddress)), + ListUtil.empty() + ) + ) + ) + ); + + try { + TransactionReceipt receipt = monitor.setMonitorVerifier(monitorVerifierContractAddress).send(); + if (!receipt.isStatusOK()) { + throw new RuntimeException( + StrUtil.format( + "transaction {} shows failed when set monitor verifier {} to monitor {}: {}", + receipt.getTransactionHash(), monitorVerifierContractAddress, monitorContractAddress, receipt.getRevertReason() + ) + ); + } + getBbcLogger().info( + "set monitor verifier {} to monitor {} by tx {} ", monitorVerifierContractAddress, monitorContractAddress, receipt.getTransactionHash() + ); + } catch (Exception e) { + throw new RuntimeException( + String.format( + "unexpected failure when setting monitor verifier %s to monitor %s", + monitorVerifierContractAddress, monitorContractAddress + ), e + ); + } + + return monitorContractAddress; + } + + public String deployMonitorVerifierContract() { + MonitorVerifier monitorVerifier; + try { + monitorVerifier = MonitorVerifier.deploy( + web3j, + rawTransactionManager, + new AcbGasProvider( + this.contractGasPriceProvider, + createDeployGasLimitProvider(MonitorVerifier.BINARY) + ) + ).send(); + } catch (Exception e) { + throw new RuntimeException("failed to deploy MonitorVerifier", e); + } + + if (this.config.isUpgradableContracts()) { + TransparentUpgradeableProxy proxy; + try { + proxy = TransparentUpgradeableProxy.deploy( + web3j, + rawTransactionManager, + new AcbGasProvider( + this.contractGasPriceProvider, + createDeployGasLimitProvider( + TransparentUpgradeableProxy.BINARY + + FunctionEncoder.encodeConstructor(ListUtil.toList( + new org.web3j.abi.datatypes.Address(monitorVerifier.getContractAddress()), + new org.web3j.abi.datatypes.Address(this.config.getProxyAdmin()), + new DynamicBytes(HexUtil.decodeHex("e1c7392a")) + )) + ) + ), + BigInteger.ZERO, + monitorVerifier.getContractAddress(), + this.config.getProxyAdmin(), + HexUtil.decodeHex("e1c7392a") + ).send(); + } catch (Exception e) { + throw new RuntimeException("failed to deploy monitorVerifier contract", e); + } + getBbcLogger().info("deploy proxy contract for monitorVerifier: {}", proxy.getContractAddress()); + return proxy.getContractAddress(); + } + + return monitorVerifier.getContractAddress(); + } + + @SneakyThrows + public String getMonitorVerifierFromMonitor(String monitorContractAddress) { + Monitor monitor = Monitor.load(monitorContractAddress, web3j, rawTransactionManager, new DefaultGasProvider()); + return monitor.getMonitorVerifier().send(); + } + + public void setPtcHubToMonitorVerifier(String monitorVerifierContractAddress, String ptcHubContractAddress) { + // 2. load monitor verifier contract + MonitorVerifier monitorVerifier = MonitorVerifier.load( + monitorVerifierContractAddress, + this.web3j, + this.rawTransactionManager, + new AcbGasProvider( + this.contractGasPriceProvider, + createEthCallGasLimitProvider( + monitorVerifierContractAddress, + new Function( + MonitorVerifier.FUNC_SETPTCHUBADDRESS, + ListUtil.toList(new org.web3j.abi.datatypes.Address(ptcHubContractAddress)), // inputs + Collections.emptyList() + ) + ) + ) + ); + + // 3. set ptc hub to monitor verifier + try { + var receipt = monitorVerifier.setPtcHubAddress(ptcHubContractAddress).send(); + getBbcLogger().info( + "set ptc hub {} to monitor verifier (address: {}) by tx {} ", + ptcHubContractAddress, monitorVerifierContractAddress, receipt.getTransactionHash() + ); + } catch (Exception e) { + throw new RuntimeException( + String.format( + "failed to set ptc hub (address: %s) to monitor verifier %s", + ptcHubContractAddress, monitorVerifierContractAddress + ), e + ); + } + } + + @SneakyThrows + public String getPtcHubFromMonitorVerifier(String monitorVerifierContractAddress) { + MonitorVerifier monitorVerifier = MonitorVerifier.load(monitorVerifierContractAddress, web3j, rawTransactionManager, new DefaultGasProvider()); + return monitorVerifier.getPtcHubAddress().send(); + } + public long querySdpSeq(String sdpContractAddress, String senderDomain, String senderID, String receiverDomain, String receiverID) { // 2. load sdpMsg SDPMsg sdpMsg = SDPMsg.load(sdpContractAddress, web3j, rawTransactionManager, new DefaultGasProvider()); @@ -401,6 +585,198 @@ public void setLocalDomainToSdp(String sdpContractAddress, String localDomain) { } } + public void setMonitorContractToSdp(String sdpContractAddress, String monitorContractAddress) { + // 2. load sdp contract + SDPMsg sdp = SDPMsg.load( + sdpContractAddress, + this.web3j, + this.rawTransactionManager, + new AcbGasProvider( + this.contractGasPriceProvider, + createEthCallGasLimitProvider( + sdpContractAddress, + new Function( + SDPMsg.FUNC_SETMONITORCONTRACT, + ListUtil.toList(new org.web3j.abi.datatypes.Address(monitorContractAddress)), // inputs + Collections.emptyList() + ) + ) + ) + ); + + // 3. set monitor to sdp + try { + var receipt = sdp.setMonitorContract(monitorContractAddress).send(); + getBbcLogger().info( + "set monitor contract (address: {}) to SDP {} by tx {}", + monitorContractAddress, sdpContractAddress, receipt.getTransactionHash() + ); + } catch (Exception e) { + throw new RuntimeException( + String.format("failed to set monitor contract (address: %s) to SDP %s", monitorContractAddress, sdpContractAddress), e + ); + } + } + + @SneakyThrows + public String getMonitorContractFromSdp(String sdpContractAddress) { + var sdp = SDPMsg.load(sdpContractAddress, web3j, rawTransactionManager, new DefaultGasProvider()); + return sdp.getMonitorAddress().send(); + } + + public void setProtocolToMonitor(String monitorContractAddress, String protocolAddress) { + // 2. load am contract + Monitor monitor = Monitor.load( + monitorContractAddress, + this.web3j, + this.rawTransactionManager, + new AcbGasProvider( + this.contractGasPriceProvider, + createEthCallGasLimitProvider( + monitorContractAddress, + new Function( + Monitor.FUNC_SETPROTOCOL, + ListUtil.toList(new org.web3j.abi.datatypes.Address(protocolAddress)), // inputs + Collections.emptyList() + ) + ) + ) + ); + + // 3. set protocol to monitor + try { + var receipt = monitor.setProtocol(protocolAddress).send(); + getBbcLogger().info( + "set protocol (address: {}) to monitor {} by tx {} ", + protocolAddress, monitorContractAddress, receipt.getTransactionHash() + ); + } catch (Exception e) { + throw new RuntimeException( + String.format( + "failed to set protocol (address: %s) to monitor %s", + protocolAddress, monitorContractAddress + ), e + ); + } + } + + @SneakyThrows + public String getProtocolFromMonitor(String monitorContractAddress) { + var monitor = Monitor.load(monitorContractAddress, web3j, rawTransactionManager, new DefaultGasProvider()); + return monitor.getProtocol().send(); + } + + public void setMonitorControl(String monitorContractAddress, int monitorType) { + // load monitor contract + Monitor monitor = Monitor.load( + monitorContractAddress, + this.web3j, + this.rawTransactionManager, + new AcbGasProvider( + this.contractGasPriceProvider, + createEthCallGasLimitProvider( + monitorContractAddress, + new Function( + Monitor.FUNC_SETMONITORCONTROL, + ListUtil.toList(new org.web3j.abi.datatypes.generated.Uint32(monitorType)), // inputs + Collections.emptyList() + ) + ) + ) + ); + + // set monitorType to monitor + try { + var receipt = monitor.setMonitorControl(BigInteger.valueOf(monitorType)).send(); + getBbcLogger().info( + "set monitorType (value: {}) to monitor {} by tx {} ", + monitorType, monitorContractAddress, receipt.getTransactionHash() + ); + } catch (Exception e) { + throw new RuntimeException( + String.format( + "failed to set monitorType (value: %s) to monitor %s", + monitorType, monitorContractAddress + ), e + ); + } + } + + @SneakyThrows + public int getMonitorControlFromMonitor(String monitorContractAddress) { + var monitor = Monitor.load(monitorContractAddress, web3j, rawTransactionManager, new DefaultGasProvider()); + return monitor.getMonitorControl().send().intValue(); + } + + public CrossChainMessageReceipt relayMonitorOrderToMonitor(String monitorContractAddress, String committeeId, String signAlgo, byte[] rawProof, byte[] rawMonitorOrder) { + // 2. create Transaction + try { + // 2.1 create function + Function function = new Function( + Monitor.FUNC_RECVMONITORORDER, // function name + ListUtil.toList(new Utf8String(committeeId), + new Utf8String(signAlgo), + new DynamicBytes(rawProof), + new DynamicBytes(rawMonitorOrder)), // inputs + Collections.emptyList() // outputs + ); + String encodedFunc = FunctionEncoder.encode(function); + + // 2.2 pre-execute before commit tx + EthCall call = this.web3j.ethCall( + org.web3j.protocol.core.methods.request.Transaction.createEthCallTransaction( + config.isKmsService() ? this.txKMSSignService.getAddress() : this.credentials.getAddress(), + monitorContractAddress, + encodedFunc + ), + DefaultBlockParameterName.LATEST + ).send(); + + // 2.3 set `confirmed` and `successful` to false if reverted + CrossChainMessageReceipt crossChainMessageReceipt = new CrossChainMessageReceipt(); + if (call.isReverted()) { + getBbcLogger().error("call monitor contract [recvMonitorOrder] {} reverted, reason: {}", monitorContractAddress, call.getRevertReason()); + crossChainMessageReceipt.setSuccessful(false); + crossChainMessageReceipt.setConfirmed(false); + crossChainMessageReceipt.setErrorMsg(call.getRevertReason()); + return crossChainMessageReceipt; + } + + // 2.4 async send tx + EthSendTransaction ethSendTransaction = rawTransactionManager.sendTransaction( + this.contractGasPriceProvider.getGasPrice(encodedFunc), + createEthCallGasLimitProvider(monitorContractAddress, function).getGasLimit(encodedFunc), + monitorContractAddress, + encodedFunc, + BigInteger.ZERO + ); + if (ObjectUtil.isNull(ethSendTransaction)) { + throw new RuntimeException("send tx with null result"); + } + if (ethSendTransaction.hasError()) { + throw new RuntimeException(StrUtil.format("tx error: {} - {}", + ethSendTransaction.getError().getCode(), ethSendTransaction.getError().getMessage())); + } + if (StrUtil.isEmpty(ethSendTransaction.getTransactionHash())) { + throw new RuntimeException("tx hash is empty"); + } + + // 2.5 return crossChainMessageReceipt + crossChainMessageReceipt.setConfirmed(false); + crossChainMessageReceipt.setSuccessful(true); + crossChainMessageReceipt.setTxhash(ethSendTransaction.getTransactionHash()); + crossChainMessageReceipt.setErrorMsg(""); + + getBbcLogger().info("relay monitor order by tx {}", ethSendTransaction.getTransactionHash()); + + return crossChainMessageReceipt; + } catch (Exception e) { + throw new RuntimeException( + String.format("failed to relay monitorOrder to %s", monitorContractAddress), e + ); + } + } + public CrossChainMessageReceipt relayMsgToAuthMsg(String amContractAddress, byte[] rawMessage) { // 2. create Transaction try { @@ -674,7 +1050,7 @@ public boolean hasPTCVerifyAnchor(String ptcHubAddress, ObjectIdentity ptcOwnerO } } - public String deployPtcHubContract(AbstractCrossChainCertificate bcdnsRootCert, String committeePtcVerifier) { + public String deployPtcHubContract(AbstractCrossChainCertificate bcdnsRootCert, String committeePtcVerifier, String monitorVerifier) { PtcHub ptcHub; try { var rawBcdnsRootCert = bcdnsRootCert.encode(); @@ -731,6 +1107,7 @@ public String deployPtcHubContract(AbstractCrossChainCertificate bcdnsRootCert, getBbcLogger().info("deploy proxy contract for ptc hub: {}", proxy.getContractAddress()); } + // set committeePtcVerifier to ptc hub ptcHub = PtcHub.load( ptcHubContractAddr, web3j, @@ -770,6 +1147,46 @@ public String deployPtcHubContract(AbstractCrossChainCertificate bcdnsRootCert, ); } + // set monitorVerifier to ptc hub + ptcHub = PtcHub.load( + ptcHubContractAddr, + web3j, + rawTransactionManager, + new AcbGasProvider( + this.contractGasPriceProvider, + createEthCallGasLimitProvider( + ptcHubContractAddr, + new Function( + PtcHub.FUNC_SETMONITORVERIFIER, + ListUtil.toList(new org.web3j.abi.datatypes.Address(monitorVerifier)), + ListUtil.empty() + ) + ) + ) + ); + + try { + TransactionReceipt receipt = ptcHub.setMonitorVerifier(monitorVerifier).send(); + if (!receipt.isStatusOK()) { + throw new RuntimeException( + StrUtil.format( + "transaction {} shows failed when set monitor verifier {} to ptc hub {}: {}", + receipt.getTransactionHash(), monitorVerifier, ptcHubContractAddr, receipt.getRevertReason() + ) + ); + } + getBbcLogger().info( + "set monitor verifier {} to ptc hub {} by tx {} ", monitorVerifier, ptcHubContractAddr, receipt.getTransactionHash() + ); + } catch (Exception e) { + throw new RuntimeException( + String.format( + "unexpected failure when setting monitor verifier %s to ptc hub %s", + monitorVerifier, ptcHubContractAddr + ), e + ); + } + return ptcHubContractAddr; } diff --git a/acb-sdk/pluginset/ethereum2/offchain-plugin/src/test/java/com/alipay/antchain/bridge/plugins/ethereum2/EthereumBBCServiceTest.java b/acb-sdk/pluginset/ethereum2/offchain-plugin/src/test/java/com/alipay/antchain/bridge/plugins/ethereum2/EthereumBBCServiceTest.java index 10dc7cef..282b432f 100644 --- a/acb-sdk/pluginset/ethereum2/offchain-plugin/src/test/java/com/alipay/antchain/bridge/plugins/ethereum2/EthereumBBCServiceTest.java +++ b/acb-sdk/pluginset/ethereum2/offchain-plugin/src/test/java/com/alipay/antchain/bridge/plugins/ethereum2/EthereumBBCServiceTest.java @@ -43,6 +43,7 @@ import com.alipay.antchain.bridge.commons.bbc.DefaultBBCContext; import com.alipay.antchain.bridge.commons.bbc.syscontract.AuthMessageContract; import com.alipay.antchain.bridge.commons.bbc.syscontract.ContractStatusEnum; +import com.alipay.antchain.bridge.commons.bbc.syscontract.MonitorContract; import com.alipay.antchain.bridge.commons.bbc.syscontract.SDPContract; import com.alipay.antchain.bridge.commons.bcdns.AbstractCrossChainCertificate; import com.alipay.antchain.bridge.commons.bcdns.PTCCredentialSubject; @@ -50,6 +51,7 @@ import com.alipay.antchain.bridge.commons.core.am.AuthMessageFactory; import com.alipay.antchain.bridge.commons.core.am.IAuthMessage; import com.alipay.antchain.bridge.commons.core.base.*; +import com.alipay.antchain.bridge.commons.core.monitor.*; import com.alipay.antchain.bridge.commons.core.ptc.*; import com.alipay.antchain.bridge.commons.core.sdp.ISDPMessage; import com.alipay.antchain.bridge.commons.core.sdp.SDPMessageFactory; @@ -60,6 +62,7 @@ import com.alipay.antchain.bridge.commons.utils.crypto.SignAlgoEnum; import com.alipay.antchain.bridge.plugins.ethereum2.abi.AppContract; import com.alipay.antchain.bridge.plugins.ethereum2.abi.AuthMsg; +import com.alipay.antchain.bridge.plugins.ethereum2.abi.Monitor; import com.alipay.antchain.bridge.plugins.ethereum2.abi.SDPMsg; import com.alipay.antchain.bridge.plugins.ethereum2.conf.Eth2NetworkEnum; import com.alipay.antchain.bridge.plugins.ethereum2.conf.EthereumConfig; @@ -220,7 +223,7 @@ public class EthereumBBCServiceTest { nodeEndorseInfo.setPublicKey(nodePubkeyEntry); NodeEndorseInfo nodeEndorseInfo2 = new NodeEndorseInfo(); - nodeEndorseInfo2.setNodeId("node2"); + nodeEndorseInfo2.setNodeId("monitor-node"); nodeEndorseInfo2.setRequired(false); nodeEndorseInfo2.setPublicKey(nodePubkeyEntry); @@ -260,7 +263,7 @@ public class EthereumBBCServiceTest { .sign(NODE_PTC_PRIVATE_KEY, tpbta.getEncodedToSign()) ), new CommitteeNodeProof( - "node2", + "monitor-node", SignAlgoEnum.KECCAK256_WITH_SECP256K1, SignAlgoEnum.KECCAK256_WITH_SECP256K1.getSigner() .sign(NODE_PTC_PRIVATE_KEY, tpbta.getEncodedToSign()) @@ -277,14 +280,14 @@ public class EthereumBBCServiceTest { CommitteeVerifyAnchor verifyAnchor = new CommitteeVerifyAnchor("committee"); verifyAnchor.addNode("node1", "default", ((X509PubkeyInfoObjectIdentity) oid).getPublicKey()); - verifyAnchor.addNode("node2", "default", ((X509PubkeyInfoObjectIdentity) oid).getPublicKey()); + verifyAnchor.addNode("monitor-node", "default", ((X509PubkeyInfoObjectIdentity) oid).getPublicKey()); verifyAnchor.addNode("node3", "default", ((X509PubkeyInfoObjectIdentity) oid).getPublicKey()); verifyAnchor.addNode("node4", "default", ((X509PubkeyInfoObjectIdentity) oid).getPublicKey()); // prepare the network stuff CommitteeNetworkInfo committeeNetworkInfo = new CommitteeNetworkInfo("committee"); committeeNetworkInfo.addEndpoint("node1", "grpcs://0.0.0.0:8080", ""); - committeeNetworkInfo.addEndpoint("node2", "grpcs://0.0.0.0:8080", ""); + committeeNetworkInfo.addEndpoint("monitor-node", "grpcs://0.0.0.0:8080", ""); committeeNetworkInfo.addEndpoint("node3", "grpcs://0.0.0.0:8080", ""); committeeNetworkInfo.addEndpoint("node4", "grpcs://0.0.0.0:8080", ""); @@ -371,16 +374,18 @@ public void testStartupWithDeployedContract() { EthereumBBCService bbcServiceTmp = new EthereumBBCService(); bbcServiceTmp.startup(mockValidCtx); - // set up am and sdp + // set up am and sdp and monitor bbcServiceTmp.setupAuthMessageContract(); bbcServiceTmp.setupSDPMessageContract(); + bbcServiceTmp.setupMonitorContract(); bbcServiceTmp.setupPTCContract(); String amAddr = bbcServiceTmp.getContext().getAuthMessageContract().getContractAddress(); String sdpAddr = bbcServiceTmp.getContext().getSdpContract().getContractAddress(); + String monitorAddr = bbcServiceTmp.getContext().getMonitorContract().getContractAddress(); String ptcAddr = bbcServiceTmp.getContext().getPtcContract().getContractAddress(); // start up success - AbstractBBCContext ctx = mockValidCtxWithPreDeployedContracts(amAddr, sdpAddr, ptcAddr); + AbstractBBCContext ctx = mockValidCtxWithPreDeployedContracts(amAddr, sdpAddr, monitorAddr, ptcAddr); EthereumBBCService ethereumBBCService = new EthereumBBCService(); ethereumBBCService.startup(ctx); @@ -388,6 +393,8 @@ public void testStartupWithDeployedContract() { Assert.assertEquals(ContractStatusEnum.CONTRACT_DEPLOYED, ethereumBBCService.getBbcContext().getAuthMessageContract().getStatus()); Assert.assertEquals(sdpAddr, ethereumBBCService.getBbcContext().getSdpContract().getContractAddress()); Assert.assertEquals(ContractStatusEnum.CONTRACT_DEPLOYED, ethereumBBCService.getBbcContext().getSdpContract().getStatus()); + Assert.assertEquals(monitorAddr, ethereumBBCService.getBbcContext().getMonitorContract().getContractAddress()); + Assert.assertEquals(ContractStatusEnum.CONTRACT_DEPLOYED, ethereumBBCService.getBbcContext().getMonitorContract().getStatus()); Assert.assertEquals(ptcAddr, ethereumBBCService.getBbcContext().getPtcContract().getContractAddress()); Assert.assertEquals(ContractStatusEnum.CONTRACT_READY, ethereumBBCService.getBbcContext().getPtcContract().getStatus()); } @@ -399,22 +406,26 @@ public void testStartupWithReadyContract() { EthereumBBCService bbcServiceTmp = new EthereumBBCService(); bbcServiceTmp.startup(mockValidCtx); - // set up am and sdp + // set up am and sdp and monitor bbcServiceTmp.setupAuthMessageContract(); bbcServiceTmp.setupSDPMessageContract(); + bbcServiceTmp.setupMonitorContract(); bbcServiceTmp.setupPTCContract(); String amAddr = bbcServiceTmp.getContext().getAuthMessageContract().getContractAddress(); String sdpAddr = bbcServiceTmp.getContext().getSdpContract().getContractAddress(); + String monitorAddr = bbcServiceTmp.getContext().getMonitorContract().getContractAddress(); String ptcAddr = bbcServiceTmp.getContext().getPtcContract().getContractAddress(); // start up success EthereumBBCService ethereumBBCService = new EthereumBBCService(); - AbstractBBCContext ctx = mockValidCtxWithPreReadyContracts(amAddr, sdpAddr, ptcAddr); + AbstractBBCContext ctx = mockValidCtxWithPreReadyContracts(amAddr, sdpAddr, monitorAddr, ptcAddr); ethereumBBCService.startup(ctx); Assert.assertEquals(amAddr, ethereumBBCService.getBbcContext().getAuthMessageContract().getContractAddress()); Assert.assertEquals(ContractStatusEnum.CONTRACT_READY, ethereumBBCService.getBbcContext().getAuthMessageContract().getStatus()); Assert.assertEquals(sdpAddr, ethereumBBCService.getBbcContext().getSdpContract().getContractAddress()); Assert.assertEquals(ContractStatusEnum.CONTRACT_READY, ethereumBBCService.getBbcContext().getSdpContract().getStatus()); + Assert.assertEquals(monitorAddr, ethereumBBCService.getBbcContext().getMonitorContract().getContractAddress()); + Assert.assertEquals(ContractStatusEnum.CONTRACT_READY, ethereumBBCService.getBbcContext().getMonitorContract().getStatus()); Assert.assertEquals(ptcAddr, ethereumBBCService.getBbcContext().getPtcContract().getContractAddress()); Assert.assertEquals(ContractStatusEnum.CONTRACT_READY, ethereumBBCService.getBbcContext().getPtcContract().getStatus()); } @@ -468,18 +479,65 @@ public void testSetupSDPMessageContract() { } @Test - public void testPtcContractAll() { + public void testSetupMonitorContract() { EthereumBBCService ethereumBBCService = new EthereumBBCService(); // start up AbstractBBCContext mockValidCtx = mockValidCtx(); ethereumBBCService.startup(mockValidCtx); - // set up sdp + // set up monitor + ethereumBBCService.setupMonitorContract(); + + // get context + AbstractBBCContext ctx = ethereumBBCService.getContext(); + Assert.assertEquals(ContractStatusEnum.CONTRACT_DEPLOYED, ctx.getMonitorContract().getStatus()); + } + + @Test + public void testMonitorAndPtcContractAll() throws Exception { + EthereumBBCService ethereumBBCService = new EthereumBBCService(); + // start up + AbstractBBCContext mockValidCtx = mockValidCtx(); + ethereumBBCService.startup(mockValidCtx); + + // set up sdp monitor and ptc + ethereumBBCService.setupSDPMessageContract(); + ethereumBBCService.setupMonitorContract(); ethereumBBCService.setupPTCContract(); var ctx = ethereumBBCService.getContext(); Assert.assertEquals(ContractStatusEnum.CONTRACT_READY, ctx.getPtcContract().getStatus()); + Assert.assertEquals(ContractStatusEnum.CONTRACT_DEPLOYED, ctx.getSdpContract().getStatus()); + Assert.assertEquals(ContractStatusEnum.CONTRACT_DEPLOYED, ctx.getMonitorContract().getStatus()); + + // init monitor + ethereumBBCService.setPtcHubInMonitorVerifier(ethereumBBCService.getBbcContext().getPtcContract().getContractAddress()); + + ethereumBBCService.setProtocolInMonitor(ctx.getSdpContract().getContractAddress()); + String addr = Monitor.load( + ethereumBBCService.getBbcContext().getMonitorContract().getContractAddress(), + ethereumBBCService.getAcbEthClient().getWeb3j(), + ethereumBBCService.getAcbEthClient().getCredentials(), + new DefaultGasProvider() + ).getProtocol().send(); + log.info("protocol in monitor: {}", addr); + + Assert.assertEquals(ContractStatusEnum.CONTRACT_DEPLOYED, ctx.getMonitorContract().getStatus()); + + int monitorType = 2; // 2 denotes MONITOR_OPEN + ethereumBBCService.setMonitorControl(monitorType); + int monitorControl = Monitor.load( + ethereumBBCService.getBbcContext().getMonitorContract().getContractAddress(), + ethereumBBCService.getAcbEthClient().getWeb3j(), + ethereumBBCService.getAcbEthClient().getCredentials(), + new DefaultGasProvider() + ).getMonitorControl().send().intValue(); + log.info("monitor control in monitor: {}", monitorControl); + + Assert.assertEquals(ContractStatusEnum.CONTRACT_READY, ctx.getMonitorContract().getStatus()); + + // init ptc ethereumBBCService.updatePTCTrustRoot(ptcTrustRoot); var root = ethereumBBCService.getPTCTrustRoot(oid); @@ -556,7 +614,8 @@ public void testSetProtocol() throws Exception { } @Test - public void testSetAmContractAndLocalDomain() throws Exception { +// public void testSetAmContractAndLocalDomain() throws Exception { + public void testSetSDPContractAll() throws Exception { EthereumBBCService ethereumBBCService = new EthereumBBCService(); // start up AbstractBBCContext mockValidCtx = mockValidCtx(); @@ -568,20 +627,19 @@ public void testSetAmContractAndLocalDomain() throws Exception { // set up sdp ethereumBBCService.setupSDPMessageContract(); + // set up monitor + ethereumBBCService.setupMonitorContract(); + // get context AbstractBBCContext ctx = ethereumBBCService.getContext(); Assert.assertEquals(ContractStatusEnum.CONTRACT_DEPLOYED, ctx.getAuthMessageContract().getStatus()); Assert.assertEquals(ContractStatusEnum.CONTRACT_DEPLOYED, ctx.getSdpContract().getStatus()); + Assert.assertEquals(ContractStatusEnum.CONTRACT_DEPLOYED, ctx.getMonitorContract().getStatus()); // set am to sdp ethereumBBCService.setAmContract(ctx.getAuthMessageContract().getContractAddress()); - String amAddr = SDPMsg.load( - ethereumBBCService.getBbcContext().getSdpContract().getContractAddress(), - ethereumBBCService.getAcbEthClient().getWeb3j(), - ethereumBBCService.getAcbEthClient().getCredentials(), - new DefaultGasProvider() - ).getAmAddress().send(); + String amAddr = ethereumBBCService.getAcbEthClient().getAmContractFromSdp(ctx.getSdpContract().getContractAddress()); log.info("amAddr: {}", amAddr); // check contract status @@ -591,13 +649,31 @@ public void testSetAmContractAndLocalDomain() throws Exception { // set the domain ethereumBBCService.setLocalDomain(CHAIN_DOMAIN); - byte[] rawDomain = SDPMsg.load( +// byte[] rawDomain = SDPMsg.load( +// ethereumBBCService.getBbcContext().getSdpContract().getContractAddress(), +// ethereumBBCService.getAcbEthClient().getWeb3j(), +// ethereumBBCService.getAcbEthClient().getCredentials(), +// new DefaultGasProvider() +// ).getLocalDomain().send(); + byte[] rawDomain = ethereumBBCService.getAcbEthClient().getLocalDomainFromSdp(ctx.getSdpContract().getContractAddress()); + log.info("domain: {}", HexUtil.encodeHexStr(rawDomain)); + +// String monitorAddr0 = ethereumBBCService.getAcbEthClient().getMonitorContractFromSdp(ctx.getSdpContract().getContractAddress()); +// log.info("monitorAddr: {}", monitorAddr0); + + // check contract status + ctx = ethereumBBCService.getContext(); + Assert.assertEquals(ContractStatusEnum.CONTRACT_DEPLOYED, ctx.getSdpContract().getStatus()); + + // set monitor to sdp + ethereumBBCService.setMonitorContract(ctx.getMonitorContract().getContractAddress()); + String monitorAddr = SDPMsg.load( ethereumBBCService.getBbcContext().getSdpContract().getContractAddress(), ethereumBBCService.getAcbEthClient().getWeb3j(), ethereumBBCService.getAcbEthClient().getCredentials(), new DefaultGasProvider() - ).getLocalDomain().send(); - log.info("domain: {}", HexUtil.encodeHexStr(rawDomain)); + ).getMonitorAddress().send(); + log.info("monitorAddr: {}", monitorAddr); // check contract status ctx = ethereumBBCService.getContext(); @@ -635,6 +711,124 @@ public void testReadCrossChainMessageReceipt() throws IOException, InterruptedEx Assert.assertEquals(crossChainMessageReceipt.isSuccessful(), crossChainMessageReceipt1.isSuccessful()); } + @Test + public void testRelayMonitorOrder() throws Exception { + setupBbc(); + + // relay monitor order: add app contract to blacklist + log.info("[TEST-1]: relay monitor order to ADD app contract to BLACKLIST"); + + IMonitorOrder monitorOrder = MonitorOrderFactory.createMonitorOrder( + 1, + "ethereum", + "testDomain1", + Long.parseLong("1000"+"0000000000000000000000000000", 2), + "testDomain1", + StrUtil.replace(appContract.getContractAddress(), "0x", "000000000000000000000000"), + "testDomain2", + StrUtil.replace(REMOTE_APP_CONTRACT, "0x", "000000000000000000000000"), + "this is a test monitor order", + "nothing in extra" + ); + + byte[] rawMonitorOrder = monitorOrder.encode(); + byte[] rawProof = SignAlgoEnum.KECCAK256_WITH_SECP256K1.getSigner().sign( + NODE_PTC_PRIVATE_KEY, + rawMonitorOrder + ); + + CrossChainMessageReceipt crossChainMessageReceipt = ethereumBBCService.relayMonitorOrder( + COMMITTEE_ID, + SignAlgoEnum.KECCAK256_WITH_SECP256K1.getName(), + rawProof, + rawMonitorOrder + ); + +// CrossChainMessageReceipt crossChainMessageReceipt = ethereumBBCService.relayMonitorOrder( +// Long.parseLong("1000"+"0000000000000000000000000000", 2), +// "testDomain1", +// StrUtil.replace(appContract.getContractAddress(), "0x", "000000000000000000000000"), +// "testDomain2", +// StrUtil.replace(REMOTE_APP_CONTRACT, "0x", "000000000000000000000000"), +// "this is a test monitor order", +// "nothing in extra" +// ); + Assert.assertTrue(crossChainMessageReceipt.isSuccessful()); + waitForTxConfirmed(crossChainMessageReceipt.getTxhash(), ethereumBBCService.getAcbEthClient().getWeb3j()); + CrossChainMessageReceipt crossChainMessageReceipt1 = ethereumBBCService.readCrossChainMessageReceipt(crossChainMessageReceipt.getTxhash()); + Assert.assertEquals(crossChainMessageReceipt.isSuccessful(), crossChainMessageReceipt1.isSuccessful()); + + // test app contract: it should be unable to send message + try { + appContract.sendUnorderedMessage("remoteDomain", DigestUtil.sha256(REMOTE_APP_CONTRACT), "UnorderedCrossChainMessage".getBytes()).send(); + Assert.fail("[appContract.sendUnorderedMessage]: expected transaction to revert, but it succeeded."); + } catch (Exception e) { + log.info("[appContract.sendUnorderedMessage]: reverted as expected: {}", e.getMessage()); + } + + // relay monitor order: remove app contract from blacklist + log.info("[TEST-2]: relay monitor order to REMOVE app contract to BLACKLIST"); + IMonitorOrder monitorOrder2 = MonitorOrderFactory.createMonitorOrder( + 1, + "ethereum", + "testDomain1", + Long.parseLong("1001"+"0000000000000000000000000000", 2), + "testDomain1", + StrUtil.replace(appContract.getContractAddress(), "0x", "000000000000000000000000"), + "testDomain2", + StrUtil.replace(REMOTE_APP_CONTRACT, "0x", "000000000000000000000000"), + "this is a test monitor order", + "nothing in extra" + ); + + byte[] rawMonitorOrder2 = monitorOrder2.encode(); + byte[] rawProof2 = SignAlgoEnum.KECCAK256_WITH_SECP256K1.getSigner().sign( + NODE_PTC_PRIVATE_KEY, + rawMonitorOrder2 + ); + + CrossChainMessageReceipt crossChainMessageReceipt2 = ethereumBBCService.relayMonitorOrder( + COMMITTEE_ID, + SignAlgoEnum.KECCAK256_WITH_SECP256K1.getName(), + rawProof2, + rawMonitorOrder2 + ); +// CrossChainMessageReceipt crossChainMessageReceipt2 = ethereumBBCService.relayMonitorOrder( +// Long.parseLong("1001"+"0000000000000000000000000000", 2), +// "testDomain1", +// StrUtil.replace(appContract.getContractAddress(), "0x", "000000000000000000000000"), +// "testDomain2", +// StrUtil.replace(REMOTE_APP_CONTRACT, "0x", "000000000000000000000000"), +// "this is a test monitor order", +// "nothing in extra" +// ); + Assert.assertTrue(crossChainMessageReceipt2.isSuccessful()); + waitForTxConfirmed(crossChainMessageReceipt2.getTxhash(), ethereumBBCService.getAcbEthClient().getWeb3j()); + CrossChainMessageReceipt crossChainMessageReceipt3 = ethereumBBCService.readCrossChainMessageReceipt(crossChainMessageReceipt2.getTxhash()); + Assert.assertEquals(crossChainMessageReceipt2.isSuccessful(), crossChainMessageReceipt3.isSuccessful()); + + // test app contract: it should be able to send message + try { + var receipt = appContract.sendUnorderedMessage("remoteDomain", DigestUtil.sha256(REMOTE_APP_CONTRACT), "UnorderedCrossChainMessage".getBytes()).send(); + Assert.assertTrue(receipt.isStatusOK()); + } catch (Exception e) { + log.error("[appContract.sendUnorderedMessage]: unexpected revert: {}", e.getMessage()); + Assert.fail("[appContract.sendUnorderedMessage]: expected transaction to execute successfully, but it failed"); + } + } + + + @Test + public void testQueryLatestBlockNumber() throws Exception { + EthereumBBCService ethereumBBCService = new EthereumBBCService(); + // start up + AbstractBBCContext mockValidCtx = mockValidCtx(); + ethereumBBCService.startup(mockValidCtx); + + BigInteger l = ethereumBBCService.getAcbEthClient().queryLatestBlockNumber(); + Assert.assertTrue(l.compareTo(BigInteger.ZERO) > 0); + } + @Test public void testReadCrossChainMessagesByHeight_sendUnordered() throws Exception { setupBbc(); @@ -684,7 +878,7 @@ public void testReadCrossChainMessagesByHeight_sendOrdered() throws Exception { Assert.assertNotNull(provableData); Assert.assertEquals(msgOnHeight, provableData.getHeightVal()); Assert.assertEquals(receipt.getTransactionHash(), Numeric.toHexString(provableData.getTxHash())); - Assert.assertEquals(receipt.getBlockHash(), Numeric.toHexString(provableData.getBlockHash())); + Assert.assertEquals(receipt.getBlockHash(), Numeric.toHexString(provableData.getBlockHash())); var proofObj = EthReceiptProof.decodeFromJson(new String(provableData.getProof())); Assert.assertNotNull(proofObj); Assert.assertNotNull(proofObj.getEthTransactionReceipt()); @@ -784,6 +978,9 @@ private void setupBbc() { // set up sdp ethereumBBCService.setupSDPMessageContract(); + // set up monitor + ethereumBBCService.setupMonitorContract(); + ethereumBBCService.setupPTCContract(); // set protocol to am (sdp type: 0) @@ -797,6 +994,19 @@ private void setupBbc() { // set local domain to sdp ethereumBBCService.setLocalDomain(CHAIN_DOMAIN); + // set monitor to sdp + ethereumBBCService.setMonitorContract(mockValidCtx.getMonitorContract().getContractAddress()); + + // set sdp to monitor + ethereumBBCService.setProtocolInMonitor(mockValidCtx.getSdpContract().getContractAddress()); + + // set monitorControl to monitor + ethereumBBCService.setMonitorControl(2); + + // set ptc hub to monitor verifier + ethereumBBCService.setPtcHubInMonitorVerifier(mockValidCtx.getPtcContract().getContractAddress()); + + // set ptc tp am ethereumBBCService.setPtcContract(mockValidCtx.getPtcContract().getContractAddress()); ethereumBBCService.updatePTCTrustRoot(ptcTrustRoot); @@ -807,6 +1017,7 @@ private void setupBbc() { AbstractBBCContext ctxCheck = ethereumBBCService.getContext(); Assert.assertEquals(ContractStatusEnum.CONTRACT_READY, ctxCheck.getAuthMessageContract().getStatus()); Assert.assertEquals(ContractStatusEnum.CONTRACT_READY, ctxCheck.getSdpContract().getStatus()); + Assert.assertEquals(ContractStatusEnum.CONTRACT_READY, ctxCheck.getMonitorContract().getStatus()); TransactionReceipt receipt = appContract.setProtocol(ethereumBBCService.getBbcContext().getSdpContract().getContractAddress()).send(); if (receipt.isStatusOK()) { @@ -819,6 +1030,17 @@ private void setupBbc() { ethereumBBCService.getBbcContext().getSdpContract().getContractAddress())); } + TransactionReceipt receipt1 = appContract.setMonitorContract(ethereumBBCService.getBbcContext().getMonitorContract().getContractAddress()).send(); + if (receipt1.isStatusOK()) { + log.info("set monitor contract({}) to app contract({})", + ethereumBBCService.getBbcContext().getMonitorContract().getContractAddress(), + appContract.getContractAddress()); + } else { + throw new Exception(String.format("failed to set monitor contract(%s) to app contract(%s)", + ethereumBBCService.getBbcContext().getMonitorContract().getContractAddress(), + appContract.getContractAddress())); + } + setupBBC = true; } @@ -860,13 +1082,14 @@ private AbstractBBCContext mockValidCtx() { return mockCtx; } - private AbstractBBCContext mockValidCtxWithPreDeployedContracts(String amAddr, String sdpAddr, String ptcAddr) { + private AbstractBBCContext mockValidCtxWithPreDeployedContracts(String amAddr, String sdpAddr, String monitorAddr, String ptcAddr) { EthereumConfig mockConf = new EthereumConfig(); mockConf.setUrl(VALID_URL); mockConf.setBeaconApiUrl(VALID_BEACON_URL); mockConf.setPrivateKey(BBC_ETH_PRIVATE_KEY_2); mockConf.setAmContractAddressDeployed(amAddr); mockConf.setSdpContractAddressDeployed(sdpAddr); + mockConf.setMonitorContractAddressDeployed(monitorAddr); mockConf.setPtcHubContractAddressDeployed(ptcAddr); mockConf.setMsgScanPolicy(scanPolicy); mockConf.setGasLimitPolicy(gasLimitPolicy); @@ -888,13 +1111,14 @@ private AbstractBBCContext mockValidCtxWithPreDeployedContracts(String amAddr, S return mockCtx; } - private AbstractBBCContext mockValidCtxWithPreReadyContracts(String amAddr, String sdpAddr, String ptcAddr) { + private AbstractBBCContext mockValidCtxWithPreReadyContracts(String amAddr, String sdpAddr, String monitorAddr, String ptcAddr) { EthereumConfig mockConf = new EthereumConfig(); mockConf.setUrl(VALID_URL); mockConf.setBeaconApiUrl(VALID_BEACON_URL); mockConf.setPrivateKey(BBC_ETH_PRIVATE_KEY_3); mockConf.setAmContractAddressDeployed(amAddr); mockConf.setSdpContractAddressDeployed(sdpAddr); + mockConf.setMonitorContractAddressDeployed(monitorAddr); mockConf.setPtcHubContractAddressDeployed(ptcAddr); mockConf.setMsgScanPolicy(scanPolicy); mockConf.setGasLimitPolicy(gasLimitPolicy); @@ -924,6 +1148,11 @@ private AbstractBBCContext mockValidCtxWithPreReadyContracts(String amAddr, Stri sdpContract.setStatus(ContractStatusEnum.CONTRACT_READY); mockCtx.setSdpContract(sdpContract); + MonitorContract monitorContract = new MonitorContract(); + monitorContract.setContractAddress(monitorAddr); + monitorContract.setStatus(ContractStatusEnum.CONTRACT_READY); + mockCtx.setMonitorContract(monitorContract); + return mockCtx; } @@ -953,13 +1182,20 @@ private void waitForTxConfirmed(String txhash, Web3j web3j) { } private byte[] getRawMsgFromRelayer(String receiverAddr) throws IOException { + // need to replace "awesome antchain-bridge" with monitor message + IMonitorMessage monitorMessage = MonitorMessageFactory.createMonitorMessage( + 1, + 2, + "this is a monitorMsg", + "awesome antchain-bridge".getBytes() + ); ISDPMessage sdpMessage = SDPMessageFactory.createSDPMessage( 1, new byte[32], crossChainLane.getReceiverDomain().getDomain(), Numeric.hexStringToByteArray(StrUtil.replace(receiverAddr, "0x", "000000000000000000000000")), -1, - "awesome antchain-bridge".getBytes() + monitorMessage.encode() ); IAuthMessage am = AuthMessageFactory.createAuthMessage( 1, @@ -982,7 +1218,14 @@ private byte[] getRawMsgFromRelayer(String receiverAddr) throws IOException { thirdPartyProof.getEncodedToSign() )).build(); CommitteeNodeProof node2Proof = CommitteeNodeProof.builder() - .nodeId("node2") + .nodeId("monitor-node") + .signAlgo(SignAlgoEnum.KECCAK256_WITH_SECP256K1) + .signature(SignAlgoEnum.KECCAK256_WITH_SECP256K1.getSigner().sign( + NODE_PTC_PRIVATE_KEY, + thirdPartyProof.getEncodedToSign() + )).build(); + CommitteeNodeProof node3Proof = CommitteeNodeProof.builder() + .nodeId("node3") .signAlgo(SignAlgoEnum.KECCAK256_WITH_SECP256K1) .signature(SignAlgoEnum.KECCAK256_WITH_SECP256K1.getSigner().sign( NODE_PTC_PRIVATE_KEY, @@ -991,7 +1234,7 @@ private byte[] getRawMsgFromRelayer(String receiverAddr) throws IOException { CommitteeEndorseProof endorseProof = new CommitteeEndorseProof(); endorseProof.setCommitteeId(COMMITTEE_ID); - endorseProof.setSigs(ListUtil.toList(node1Proof, node2Proof)); + endorseProof.setSigs(ListUtil.toList(node1Proof, node2Proof, node3Proof)); thirdPartyProof.setRawProof(endorseProof.encode()); diff --git a/acb-sdk/pluginset/ethereum2/onchain-plugin/solidity/sys/AppContract.sol b/acb-sdk/pluginset/ethereum2/onchain-plugin/solidity/sys/AppContract.sol index 5e60e4d2..bc996119 100644 --- a/acb-sdk/pluginset/ethereum2/onchain-plugin/solidity/sys/AppContract.sol +++ b/acb-sdk/pluginset/ethereum2/onchain-plugin/solidity/sys/AppContract.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.0; import "./interfaces/IContractUsingSDP.sol"; import "./interfaces/ISDPMessage.sol"; +import "./interfaces/IMonitor.sol"; import "./lib/utils/Ownable.sol"; contract AppContract is IContractUsingSDP, Ownable { @@ -15,6 +16,8 @@ contract AppContract is IContractUsingSDP, Ownable { mapping(bytes32 => bytes[]) public sendMsg; + address public monitorAddress; + address public sdpAddress; bytes32 public latest_msg_id_sent_order; @@ -31,7 +34,7 @@ contract AppContract is IContractUsingSDP, Ownable { event sendCrosschainMsg(string receiverDomain, bytes32 receiver, bytes message, bool isOrdered); modifier onlySdpMsg() { - require(msg.sender == sdpAddress, "INVALID_PERMISSION"); + require(msg.sender == sdpAddress, "INVALID_PERMISSION: only sdp message"); _; } @@ -39,20 +42,30 @@ contract AppContract is IContractUsingSDP, Ownable { sdpAddress = protocolAddress; } - function recvUnorderedMessage(string memory senderDomain, bytes32 author, bytes memory message) external override onlySdpMsg { + modifier onlyMonitorMsg() { + require(monitorAddress == msg.sender, "INVALID_PERMISSION: only monitor message"); + _; + } + + function setMonitorContract(address newMonitorAddress) external onlyOwner { + monitorAddress = newMonitorAddress; + } + + function recvUnorderedMessage(string memory senderDomain, bytes32 author, bytes memory message) external override onlyMonitorMsg { recvMsg[author].push(message); last_uo_msg = message; emit recvCrosschainMsg(senderDomain, author, message, false); } - function recvMessage(string memory senderDomain, bytes32 author, bytes memory message) external override onlySdpMsg { + function recvMessage(string memory senderDomain, bytes32 author, bytes memory message) external override onlyMonitorMsg { recvMsg[author].push(message); last_msg = message; emit recvCrosschainMsg(senderDomain, author, message, true); } + // notice: monitor only support the sendV1 version message function sendUnorderedMessage(string memory receiverDomain, bytes32 receiver, bytes memory message) external { - ISDPMessage(sdpAddress).sendUnorderedMessage(receiverDomain, receiver, message); + IMonitor(monitorAddress).sendUnorderedMonitorMessage(receiverDomain, receiver, message); sendMsg[receiver].push(message); emit sendCrosschainMsg(receiverDomain, receiver, message, false); @@ -60,7 +73,7 @@ contract AppContract is IContractUsingSDP, Ownable { function sendMessage(string memory receiverDomain, bytes32 receiver, bytes memory message) external{ - ISDPMessage(sdpAddress).sendMessage(receiverDomain, receiver, message); + IMonitor(monitorAddress).sendMonitorMessage(receiverDomain, receiver, message); sendMsg[receiver].push(message); emit sendCrosschainMsg(receiverDomain, receiver, message, true); diff --git a/acb-sdk/pluginset/ethereum2/onchain-plugin/solidity/sys/CommitteePtcVerifier.sol b/acb-sdk/pluginset/ethereum2/onchain-plugin/solidity/sys/CommitteePtcVerifier.sol index 2b783a42..24ba4198 100644 --- a/acb-sdk/pluginset/ethereum2/onchain-plugin/solidity/sys/CommitteePtcVerifier.sol +++ b/acb-sdk/pluginset/ethereum2/onchain-plugin/solidity/sys/CommitteePtcVerifier.sol @@ -11,8 +11,8 @@ contract CommitteePtcVerifier is IPtcVerifier { return CommitteeLib.verifyTpBta(va, tpBta); } - function verifyTpProof(TpBta memory tpBta, ThirdPartyProof memory tpProof) external override returns (bool) { - return CommitteeLib.verifyTpProof(tpBta, tpProof); + function verifyTpProof(TpBta memory tpBta, ThirdPartyProof memory tpProof, address monitorPtcAddr) external override returns (bool) { + return CommitteeLib.verifyTpProof(tpBta, tpProof, monitorPtcAddr); } function myPtcType() external pure override returns (PTCTypeEnum) { diff --git a/acb-sdk/pluginset/ethereum2/onchain-plugin/solidity/sys/Monitor.sol b/acb-sdk/pluginset/ethereum2/onchain-plugin/solidity/sys/Monitor.sol new file mode 100755 index 00000000..29e6a4c5 --- /dev/null +++ b/acb-sdk/pluginset/ethereum2/onchain-plugin/solidity/sys/Monitor.sol @@ -0,0 +1,298 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "./interfaces/IContractUsingSDP.sol"; +import "./interfaces/IContractUsingMonitor.sol"; +import "./interfaces/ISDPMessage.sol"; +import "./interfaces/IMonitor.sol"; +import "./interfaces/IMonitorVerifier.sol"; +import "./lib/monitor/MonitorLib.sol"; +import "./lib/utils/Ownable.sol"; +import "./@openzeppelin/contracts/proxy/utils/Initializable.sol"; + +// 监管节点部署该合约 +// 接受监管指令; 只在发送方进行事前监管 +contract Monitor is IMonitor, IContractUsingMonitor, Ownable, Initializable { + using MonitorLib for MonitorOrder; + using MonitorLib for MonitorMessage; + + // SDP合约地址 + address public sdpAddress; + + // MonitorVerifier合约地址 + address public monitorVerifierAddress; + + // 控制监管的字段 + uint32 public monitorControl; + + // 黑名单 + mapping(bytes32 => bool) public senderBlacklist; + mapping(bytes32 => bool) public receiverBlacklist; + + // uint32 public successfulCallInMonitorOPEN = 0; + // uint32 public successfulCallInMonitorCLOSE = 0; + // uint32 public successfulCallInMonitorROLLBACK = 0; + + modifier onlySubProtocols { + require( + msg.sender == sdpAddress, + "MonitorMsg: sender not valid sub-protocol" + ); + _; + } + + constructor() { + _disableInitializers(); + } + + function init() external initializer() { + _transferOwnership(_msgSender()); + } + + function setProtocol(address protocolAddress) override external onlyOwner { + require(protocolAddress != address(0), "MonitorMsg: invalid sdp contract"); + sdpAddress = protocolAddress; + } + + function setMonitorVerifier(address newMonitorVerifierAddress) override external { + require(newMonitorVerifierAddress != address(0), "MonitorMsg: invalid MonitorVerifier contract"); + monitorVerifierAddress = newMonitorVerifierAddress; + } + + function setMonitorControl(uint32 monitorType) override external onlyOwner { + monitorControl = monitorType == 0 ? MonitorLib.MONITOR_CLOSE : MonitorLib.MONITOR_OPEN; + } + + function getProtocol() external view returns (address) { + return sdpAddress; + } + + function getMonitorControl() external view returns (uint32) { + return monitorControl; + } + + function getMonitorVerifier() external view returns (address) { + return monitorVerifierAddress; + } + + // function getSuccessfulCallInMonitorOPEN() external view returns (uint32) { + // return successfulCallInMonitorOPEN; + // } + + // function getSuccessfulCallInMonitorCLOSE() external view returns (uint32) { + // return successfulCallInMonitorCLOSE; + // } + + // function getSuccessfulCallInMonitorROLLBACK() external view returns (uint32) { + // return successfulCallInMonitorROLLBACK; + // } + + function sendMonitorMessage(string calldata receiverDomain, bytes32 receiverID, bytes calldata message) override external { + _beforeSend(receiverDomain, receiverID, message); + + // 执行事前监管的检查 + if (monitorControl == MonitorLib.MONITOR_OPEN) { + bool monitorResult = false; + monitorResult = PreMonitoring(receiverDomain, receiverID, message); + if(monitorResult == false) { + revert("beforehand Monitor disapproval"); + } + } + + MonitorMessage memory monitorMessage = MonitorMessage( + { + monitorType: monitorControl, + monitorMsg: "", + message: message + } + ); + + bytes memory rawMsg = monitorMessage.encode(); + + ISDPMessage(sdpAddress).sendMessage(receiverDomain, receiverID, msg.sender, rawMsg); + + _afterSend(); + } + + function sendUnorderedMonitorMessage(string calldata receiverDomain, bytes32 receiverID, bytes calldata message) external { + _beforeSendUnordered(receiverDomain, receiverID, message); + + // 执行事前监管的检查 + if (monitorControl == MonitorLib.MONITOR_OPEN) { + bool monitorResult = false; + monitorResult = PreMonitoring(receiverDomain, receiverID, message); + if(monitorResult == false) { + revert("beforehand Monitor disapproval"); + } + } + + MonitorMessage memory monitorMessage = MonitorMessage( + { + monitorType: monitorControl, + monitorMsg: "", + message: message + } + ); + + bytes memory rawMsg = monitorMessage.encode(); + + ISDPMessage(sdpAddress).sendUnorderedMessage(receiverDomain, receiverID, msg.sender, rawMsg); + + _afterSendUnordered(); + } + + // IContractUsingMonitor.sol里面的接口 + function recvUnorderedMessageFromSDP(string memory senderDomain, bytes32 author, address receiverID, bytes memory message) external override onlySubProtocols { + // 去掉监管字段 再向上传给app合约 + MonitorMessage memory monitorMessage; + monitorMessage.decode(message); + + if (monitorMessage.monitorType == MonitorLib.MONITOR_OPEN) { + // 验证监管节点的签名 + bool res = IMonitorVerifier(monitorVerifierAddress).verifyMonitorNodeProofMessage(); + emit VerifyMonitorNodeProofMessage(senderDomain, author, receiverID, res); + // 监管不通过 构造回滚消息(仅修改监管字段 对原文不修改) + if (res == false) { + monitorMessage.monitorType = MonitorLib.MONITOR_ROLLBACK; + monitorMessage.monitorMsg = "MidMonitoring not pass"; + ISDPMessage(sdpAddress).sendUnorderedMessage(senderDomain, author, receiverID, monitorMessage.encode()); + emit sendMonitorRollbakMessage(monitorMessage.monitorType, MonitorLib.encodeAddressIntoCrossChainID(receiverID), senderDomain, author, monitorMessage.monitorMsg); + } else { + IContractUsingSDP(receiverID).recvUnorderedMessage(senderDomain, author, monitorMessage.message); + } + } else if (monitorMessage.monitorType == MonitorLib.MONITOR_ROLLBACK) { + IContractUsingSDP(receiverID).recvUnorderedMessage(senderDomain, author, monitorMessage.message); + emit receiveMonitorRollbackMessage(monitorMessage.monitorType, senderDomain, author, MonitorLib.encodeAddressIntoCrossChainID(receiverID), monitorMessage.monitorMsg); + } else if (monitorMessage.monitorType == MonitorLib.MONITOR_CLOSE) { + IContractUsingSDP(receiverID).recvUnorderedMessage(senderDomain, author, monitorMessage.message); + } else { + revert("Monitor_Msg: wrong monitor type"); + } + } + + // IContractUsingMonitor.sol里面的接口 + function recvMessageFromSDP(string memory senderDomain, bytes32 author, address receiverID, bytes memory message) external override onlySubProtocols { + // 去掉监管字段 再向上传给app合约 + MonitorMessage memory monitorMessage; + monitorMessage.decode(message); + + if (monitorMessage.monitorType == MonitorLib.MONITOR_OPEN) { + // 验证监管节点的签名 + bool res = IMonitorVerifier(monitorVerifierAddress).verifyMonitorNodeProofMessage(); + emit VerifyMonitorNodeProofMessage(senderDomain, author, receiverID, res); + // 监管不通过 构造回滚消息(仅修改监管字段 对原文不修改) + if (res == false) { + // revert("Monitor_Msg: MidMonitoring not pass"); + monitorMessage.monitorType = MonitorLib.MONITOR_ROLLBACK; + monitorMessage.monitorMsg = "MidMonitoring not pass"; + ISDPMessage(sdpAddress).sendMessage(senderDomain, author, receiverID, monitorMessage.encode()); + emit sendMonitorRollbakMessage(monitorMessage.monitorType, MonitorLib.encodeAddressIntoCrossChainID(receiverID), senderDomain, author, monitorMessage.monitorMsg); + } else { + IContractUsingSDP(receiverID).recvMessage(senderDomain, author, monitorMessage.message); + // successfulCallInMonitorOPEN += 1; + } + } else if (monitorMessage.monitorType == MonitorLib.MONITOR_ROLLBACK) { + IContractUsingSDP(receiverID).recvMessage(senderDomain, author, monitorMessage.message); + // successfulCallInMonitorROLLBACK += 1; + emit receiveMonitorRollbackMessage(monitorMessage.monitorType, senderDomain, author, MonitorLib.encodeAddressIntoCrossChainID(receiverID), monitorMessage.monitorMsg); + } else if (monitorMessage.monitorType == MonitorLib.MONITOR_CLOSE) { + IContractUsingSDP(receiverID).recvMessage(senderDomain, author, monitorMessage.message); + // successfulCallInMonitorCLOSE += 1; + } else { + revert("Monitor_Msg: wrong monitor type"); + } + } + + function PreMonitoring(string calldata receiverDomain, bytes32 receiverID, bytes calldata message) internal returns (bool) { + bool result = false; + + // 事前监管逻辑 + bytes32 senderID = MonitorLib.encodeAddressIntoCrossChainID(msg.sender); + if(senderBlacklist[senderID]) { + emit MonitorDisapproval(senderID, + receiverDomain, + receiverID, + MonitorLib.MAJOR_TYPE_CONTRACT_ADDRESS, + MonitorLib.SENDER_IN_BLACKLIST); + } else { + if (receiverBlacklist[receiverID]) { + emit MonitorDisapproval(senderID, + receiverDomain, + receiverID, + MonitorLib.MAJOR_TYPE_CONTRACT_ADDRESS, + MonitorLib.RECEIVER_IN_BLACKLIST); + } else { + emit MonitorApproval(senderID, + receiverDomain, + receiverID); + return true; + } + } + + return result; + } + + + function recvMonitorOrder(string calldata committeeId, string calldata signAlgo, bytes memory rawProof, bytes memory rawMonitorOrder) external override { + // 对监管节点发送的监管指令 验签 + bool res = IMonitorVerifier(monitorVerifierAddress).verifyMonitorOrder(committeeId, signAlgo, rawProof, rawMonitorOrder); + emit VerifyMonitorOrder(committeeId, res); + require(res == true, "Monitor_Msg: verify monitor node proof message failed"); + + MonitorOrder memory monitorOrder; + monitorOrder.decode(rawMonitorOrder); + + uint8[8] memory flags; + uint8[8] memory values; + for (uint8 i = 0; i < 8; i++) { + uint8 chunk = uint8((monitorOrder.monitorOrderType >> (28 - i * 4)) & 0xF); // 取出 4-bit + flags[i] = (chunk >> 3) & 0x1; // 最高位为主类型 + values[i] = chunk & 0x7; // 后3位为子类型 + } + + // 存储监管指令 目前只支持 加入和移除黑名单 控制监管开关 (合约部署时默认监管开启) + if(flags[0] == MonitorLib.MAJOR_TYPE_CONTRACT_ADDRESS) { + if(values[0] == MonitorLib.MINOR_TYPE_ADD_TO_BLACKLIST) { + senderBlacklist[monitorOrder.sender] = true; + receiverBlacklist[monitorOrder.receiver] = true; + } else if(values[0] == MonitorLib.MINOR_TYPE_REMOVE_FROM_BLACKLIST) { + senderBlacklist[monitorOrder.sender] = false; + receiverBlacklist[monitorOrder.receiver] = false; + } else { + revert("Monitor_Msg: not support monitor order containing this MINOR TYPE in MAJOR_TYPE_CONTRACT_ADDRESS yet"); + } + } + if(flags[1] == MonitorLib.MAJOR_TYPE_CONTROL) { + if(values[1] == MonitorLib.MINOR_TYPE_MONITOR_CLOSE) { + monitorControl = MonitorLib.MONITOR_CLOSE; + } else if(values[1] == MonitorLib.MINOR_TYPE_MONITOR_OPEN) { + monitorControl = MonitorLib.MONITOR_OPEN; + } else { + revert("Monitor_Msg: not support monitor order containing this MINOR TYPE in MAJOR_TYPE_CONTROL yet"); + } + } + if(flags[0] != MonitorLib.MAJOR_TYPE_CONTRACT_ADDRESS) { + if(flags[1] != MonitorLib.MAJOR_TYPE_CONTROL) { + revert("Monitor_Msg: not support monitor order containing this MAJOR TYPE yet"); + } + } + emit receiveMonitorOrder(monitorOrder.monitorOrderType, monitorOrder.senderDomain, monitorOrder.sender, + monitorOrder.receiverDomain, monitorOrder.receiver, + monitorOrder.transactionContent, monitorOrder.extra); + } + + function _beforeSend(string calldata receiverDomain, bytes32 receiverID, bytes calldata message) internal {} + + function _afterSend() internal {} + + function _beforeSendUnordered(string calldata receiverDomain, bytes32 receiverID, bytes calldata message) internal {} + + function _afterSendUnordered() internal {} + + /** + * @dev This empty reserved space is put in place to allow future versions to add new + * variables without shifting down storage in the inheritance chain. + * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps + */ + uint256[50] private __gap; +} \ No newline at end of file diff --git a/acb-sdk/pluginset/ethereum2/onchain-plugin/solidity/sys/MonitorVerifier.sol b/acb-sdk/pluginset/ethereum2/onchain-plugin/solidity/sys/MonitorVerifier.sol new file mode 100755 index 00000000..c416500d --- /dev/null +++ b/acb-sdk/pluginset/ethereum2/onchain-plugin/solidity/sys/MonitorVerifier.sol @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; +pragma experimental ABIEncoderV2; + +import "./interfaces/IMonitorVerifier.sol"; +import "./lib/ptc/CommitteeLib.sol"; +import "./lib/monitor/MonitorLib.sol"; +import "./@openzeppelin/contracts/access/Ownable.sol"; +import "./@openzeppelin/contracts/proxy/utils/Initializable.sol"; + +contract MonitorVerifier is Ownable, Initializable, IMonitorVerifier { + + using CommitteeLib for NodeEndorseInfo; + using CommitteeLib for CommitteeNodeProof; + using CommitteeLib for CrossChainLane; + + MonitorNodeProofMessage public monitorNodeProofMessage; + + address public ptcHubAddress; + + // key: committeeId, value: NodeEndorseInfo + mapping(string => NodeEndorseInfo) public monitorNodeEndorseInfoMap; + + modifier onlyPtcHub() { + require(_msgSender() == ptcHubAddress, "MonitorVerifierMsg: caller is not the ptcHub"); + _; + } + + constructor() { + _disableInitializers(); + } + + function init() external initializer { + _transferOwnership(_msgSender()); + } + + function setPtcHubAddress(address newPtcHubAddress) external override onlyOwner { + ptcHubAddress = newPtcHubAddress; + } + + function getPtcHubAddress() external view returns (address) { + return ptcHubAddress; + } + + function updateMonitorNodeEndorseInfo(bytes memory rawEndorseRoot) external override onlyPtcHub { + CommitteeEndorseRoot memory cer = CommitteeLib.decodeCommitteeEndorseRootFrom(rawEndorseRoot); + for (uint i = 0; i < cer.endorsers.length; i++) { + NodeEndorseInfo memory info = cer.endorsers[i]; + if (CommitteeLib.checkMonitorNode(info.nodeId)) { + monitorNodeEndorseInfoMap[cer.committeeId] = info; + emit UpdateMonitorNodeEndorseInfo(cer.committeeId, info.nodeId, info.publicKey.rawPublicKey); + break; + } + } + } + + // AM-SDP-Monitor合约这样一条跨合约调用链是一个原子性操作, 属于同一笔交易的执行上下文,在交易结束之前不会切换到下一笔交易 + // 所以此处不需要用modifier修饰符去保证来源的可靠性 + function receiveMonitorNodeProofMessage( + CrossChainLane calldata newCrossChainLane, + string calldata newCommitteeId, + CommitteeNodeProof calldata newMonitorNodeProof, + bytes calldata newEncodedToSign) external override { + monitorNodeProofMessage.crossChainLane = newCrossChainLane; + monitorNodeProofMessage.committeeId = newCommitteeId; + monitorNodeProofMessage.monitorNodeProof = newMonitorNodeProof; + monitorNodeProofMessage.encodedToSign = newEncodedToSign; + } + + function verifyMonitorNodeProofMessage() external override returns (bool) { + require(_hasMonitorNodeEndorseInfo(monitorNodeProofMessage.committeeId), "MonitorVerifierMsg: no monitor node endorse info"); + return CommitteeLib.verifyTpProofFromMonitorNode( + monitorNodeProofMessage.crossChainLane, + monitorNodeProofMessage.committeeId, + monitorNodeEndorseInfoMap[monitorNodeProofMessage.committeeId], + monitorNodeProofMessage.monitorNodeProof, + monitorNodeProofMessage.encodedToSign); + } + + function verifyMonitorOrder(string calldata committeeId, string calldata signAlgo, bytes calldata rawProof, bytes calldata rawMonitorOrder) external override returns (bool) { + require(_hasMonitorNodeEndorseInfo(committeeId), "MonitorVerifierMsg: no monitor node endorse info"); + return CommitteeLib.verifyMonitorOrder( + committeeId, + monitorNodeEndorseInfoMap[committeeId], + signAlgo, + rawProof, + rawMonitorOrder); + } + + function _hasMonitorNodeEndorseInfo(string memory committeeId) internal view returns (bool) { + return bytes(monitorNodeEndorseInfoMap[committeeId].nodeId).length > 0; + } + +} diff --git a/acb-sdk/pluginset/ethereum2/onchain-plugin/solidity/sys/PtcHub.sol b/acb-sdk/pluginset/ethereum2/onchain-plugin/solidity/sys/PtcHub.sol index a0995590..9ef2abd4 100644 --- a/acb-sdk/pluginset/ethereum2/onchain-plugin/solidity/sys/PtcHub.sol +++ b/acb-sdk/pluginset/ethereum2/onchain-plugin/solidity/sys/PtcHub.sol @@ -4,6 +4,7 @@ pragma experimental ABIEncoderV2; import "./interfaces/IPtcHub.sol"; import "./interfaces/IPtcVerifier.sol"; +import "./interfaces/IMonitorVerifier.sol"; import "./lib/ptc/PtcLib.sol"; import "./@openzeppelin/contracts/access/Ownable.sol"; import "./@openzeppelin/contracts/proxy/utils/Initializable.sol"; @@ -43,6 +44,8 @@ contract PtcHub is IPtcHub, Ownable, Initializable { PTCTypeEnum[] public ptcTypeSupported; + address public monitorVerifierAddr; + constructor(bytes memory rawRootBcdnsCert) { _initBcdns(rawRootBcdnsCert); _disableInitializers(); @@ -69,6 +72,15 @@ contract PtcHub is IPtcHub, Ownable, Initializable { emit NewBcdnsCert(k); } + function setMonitorVerifier(address newMonitorVerifierAddr) external override onlyOwner { + require(newMonitorVerifierAddr != address(0), "PtcHub: invalid monitorPtc contract"); + monitorVerifierAddr = newMonitorVerifierAddr; + } + + function getMonitorVerifier() external view returns (address) { + return monitorVerifierAddr; + } + function updatePTCTrustRoot(bytes calldata rawPtcTrustRoot) external override @@ -190,6 +202,7 @@ contract PtcHub is IPtcHub, Ownable, Initializable { } s.mapByVersion[newTpBta.tpbtaVersion] = rawTpBta; emit SaveTpBta(k, newTpBta.tpbtaVersion); + IMonitorVerifier(monitorVerifierAddr).updateMonitorNodeEndorseInfo(newTpBta.endorseRoot); } function getTpBta(bytes calldata tpbtaLane, uint32 tpBtaVersion) external override view returns (bytes memory) { @@ -231,7 +244,7 @@ contract PtcHub is IPtcHub, Ownable, Initializable { address verifier = verifierMap[AcbCommons.decodePTCCredentialSubjectFrom(ptr.ptcCrossChainCert.credentialSubject).ptcType]; require(verifier != address(0x0), "no ptc veifier set"); - bool result = IPtcVerifier(verifier).verifyTpProof(tpbta, tpProof); + bool result = IPtcVerifier(verifier).verifyTpProof(tpbta, tpProof, monitorVerifierAddr); emit VerifyProof(tpbta.crossChainLane, result); require(result, "verify not pass"); } diff --git a/acb-sdk/pluginset/ethereum2/onchain-plugin/solidity/sys/SDPMsg.sol b/acb-sdk/pluginset/ethereum2/onchain-plugin/solidity/sys/SDPMsg.sol index 898596c3..74a3f19c 100644 --- a/acb-sdk/pluginset/ethereum2/onchain-plugin/solidity/sys/SDPMsg.sol +++ b/acb-sdk/pluginset/ethereum2/onchain-plugin/solidity/sys/SDPMsg.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.0; import "./interfaces/ISDPMessage.sol"; import "./interfaces/IAuthMessage.sol"; +import "./interfaces/IContractUsingMonitor.sol"; import "./interfaces/IContractUsingSDP.sol"; import "./interfaces/IContractWithAcks.sol"; import "./lib/sdp/SDPLib.sol"; @@ -17,6 +18,8 @@ contract SDPMsg is ISDPMessage, Ownable, Initializable { using SDPLib for SDPMessageV3; using SDPLib for BlockState; + address public monitorAddress; + address public amAddress; bytes32 public localDomainHash; @@ -37,6 +40,11 @@ contract SDPMsg is ISDPMessage, Ownable, Initializable { */ mapping(bytes32 => bool) sendSDPV3Msgs; + modifier onlyMonitorMsg() { + require(monitorAddress == msg.sender, "SDPMsg: not valid monitor contract"); + _; + } + modifier onlyAM() { require( amAddress == msg.sender, @@ -53,6 +61,15 @@ contract SDPMsg is ISDPMessage, Ownable, Initializable { _transferOwnership(_msgSender()); } + function setMonitorContract(address newMonitorAddress) override external onlyOwner { + require(newMonitorAddress != address(0), "SDPMsg: invalid monitor contract"); + monitorAddress = newMonitorAddress; + } + + function getMonitorAddress() external view returns (address) { + return monitorAddress; + } + function setAmContract(address newAmContract) override external onlyOwner { require(newAmContract != address(0), "SDPMsg: invalid am contract"); amAddress = newAmContract; @@ -70,7 +87,7 @@ contract SDPMsg is ISDPMessage, Ownable, Initializable { return localDomainHash; } - function sendMessage(string calldata receiverDomain, bytes32 receiverID, bytes calldata message) override external { + function sendMessage(string calldata receiverDomain, bytes32 receiverID, address senderID, bytes calldata message) override external onlyMonitorMsg { _beforeSend(receiverDomain, receiverID, message); SDPMessage memory sdpMessage = SDPMessage( @@ -78,18 +95,18 @@ contract SDPMsg is ISDPMessage, Ownable, Initializable { receiveDomain: receiverDomain, receiver: receiverID, message: message, - sequence: _getAndUpdateSendSeq(receiverDomain, msg.sender, receiverID) + sequence: _getAndUpdateSendSeq(receiverDomain, senderID, receiverID) } ); bytes memory rawMsg = sdpMessage.encode(); - IAuthMessage(amAddress).recvFromProtocol(msg.sender, rawMsg); + IAuthMessage(amAddress).recvFromProtocol(senderID, rawMsg); _afterSend(); } - function sendUnorderedMessage(string calldata receiverDomain, bytes32 receiverID, bytes calldata message) override external { + function sendUnorderedMessage(string calldata receiverDomain, bytes32 receiverID, address senderID, bytes calldata message) override external onlyMonitorMsg { _beforeSendUnordered(receiverDomain, receiverID, message); SDPMessage memory sdpMessage = SDPMessage( @@ -101,7 +118,7 @@ contract SDPMsg is ISDPMessage, Ownable, Initializable { } ); - IAuthMessage(amAddress).recvFromProtocol(msg.sender, sdpMessage.encode()); + IAuthMessage(amAddress).recvFromProtocol(senderID, sdpMessage.encode()); _afterSendUnordered(); } @@ -291,7 +308,7 @@ contract SDPMsg is ISDPMessage, Ownable, Initializable { errMsg = "receiver has no code"; } else { try - IContractUsingSDP(receiver).recvMessage(senderDomain, senderID, sdpMessage.message) + IContractUsingMonitor(monitorAddress).recvMessageFromSDP(senderDomain, senderID, receiver, sdpMessage.message) { res = true; } catch Error( @@ -307,8 +324,8 @@ contract SDPMsg is ISDPMessage, Ownable, Initializable { } function _routeUnorderedMessage(string calldata senderDomain, bytes32 senderID, SDPMessage memory sdpMessage) internal { - IContractUsingSDP(sdpMessage.getReceiverAddress()) - .recvUnorderedMessage(senderDomain, senderID, sdpMessage.message); + IContractUsingMonitor(monitorAddress) + .recvUnorderedMessageFromSDP(senderDomain, senderID, sdpMessage.getReceiverAddress(), sdpMessage.message); } function _processSDPv2(string calldata senderDomain, bytes32 senderID, bytes memory pkg) internal { diff --git a/acb-sdk/pluginset/ethereum2/onchain-plugin/solidity/sys/interfaces/IContractUsingMonitor.sol b/acb-sdk/pluginset/ethereum2/onchain-plugin/solidity/sys/interfaces/IContractUsingMonitor.sol new file mode 100755 index 00000000..621d055b --- /dev/null +++ b/acb-sdk/pluginset/ethereum2/onchain-plugin/solidity/sys/interfaces/IContractUsingMonitor.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +// 供dapp合约编写的接口 由监管合约向上传递消息时调用 +interface IContractUsingMonitor { + + function recvUnorderedMessageFromSDP(string memory senderDomain, bytes32 author, address receiverID, bytes memory message) external; + + function recvMessageFromSDP(string memory senderDomain, bytes32 author, address receiverID, bytes memory message) external; +} \ No newline at end of file diff --git a/acb-sdk/pluginset/ethereum2/onchain-plugin/solidity/sys/interfaces/IMonitor.sol b/acb-sdk/pluginset/ethereum2/onchain-plugin/solidity/sys/interfaces/IMonitor.sol new file mode 100755 index 00000000..d3a44144 --- /dev/null +++ b/acb-sdk/pluginset/ethereum2/onchain-plugin/solidity/sys/interfaces/IMonitor.sol @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "../lib/monitor/MonitorLib.sol"; + +interface IMonitor { + // 发送方-监管通过 监管不通过 + event MonitorApproval( + bytes32 senderID, + string receiverDomain, + bytes32 receiverID + ); + + event MonitorDisapproval( + bytes32 senderID, + string receiverDomain, + bytes32 receiverID, + uint8 monitorMainType, + uint8 monitorSubType + ); + + // 收到监管指令 + event receiveMonitorOrder( + uint32 monitorType, + string senderDomain, + bytes32 sender, + string receiverDomain, + bytes32 receiver, + string transactionContent, + string extra + ); + + // 收到监管回滚消息 + event receiveMonitorRollbackMessage( + uint32 monitorType, + string senderDomain, + bytes32 sender, + bytes32 receiver, + string monitorMsg + ); + + // 监管不通过 构造监管回滚消息 + event sendMonitorRollbakMessage( + uint32 monitorType, + bytes32 sender, + string receiverDomain, + bytes32 receiver, + string monitorMsg + ); + + // 验证监管节点签名 + event VerifyMonitorNodeProofMessage( + string senderDomain, + bytes32 author, + address receiverID, + bool result + ); + + event VerifyMonitorOrder( + string committeeId, + bool result + ); + + // send接口:供dapp合约调用 + function sendMonitorMessage(string calldata receiverDomain, bytes32 receiverID, bytes calldata message) external; + + function sendUnorderedMonitorMessage(string calldata receiverDomain, bytes32 receiverID, bytes calldata message) external; + + function recvMonitorOrder(string calldata committeeId, string calldata signAlgo, bytes memory proof, bytes memory rawMonitorOrder) external; + + // 其他 + function setProtocol(address protocolAddress) external; + + function setMonitorVerifier(address newMonitorVerifierAddress) external; + + function setMonitorControl(uint32 monitorType) external; +} \ No newline at end of file diff --git a/acb-sdk/pluginset/ethereum2/onchain-plugin/solidity/sys/interfaces/IMonitorVerifier.sol b/acb-sdk/pluginset/ethereum2/onchain-plugin/solidity/sys/interfaces/IMonitorVerifier.sol new file mode 100755 index 00000000..7106fb5d --- /dev/null +++ b/acb-sdk/pluginset/ethereum2/onchain-plugin/solidity/sys/interfaces/IMonitorVerifier.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; +pragma experimental ABIEncoderV2; + +import "../lib/ptc/CommitteeLib.sol"; +import "../lib/commons/AcbCommons.sol"; + +interface IMonitorVerifier { + + struct MonitorNodeProofMessage { + CrossChainLane crossChainLane; + string committeeId; + CommitteeNodeProof monitorNodeProof; + bytes encodedToSign; + } + + event UpdateMonitorNodeEndorseInfo(string committeeId, string nodeId, bytes rawPublicKey); + + function setPtcHubAddress(address newPtcHubAddress) external; + + function updateMonitorNodeEndorseInfo(bytes memory rawEndorseRoot) external; + + function receiveMonitorNodeProofMessage( + CrossChainLane calldata newCrossChainLane, + string calldata newCommitteeId, + CommitteeNodeProof calldata newMonitorNodeProof, + bytes calldata encodedToSign) external; + + function verifyMonitorNodeProofMessage() external returns (bool); + + function verifyMonitorOrder(string calldata committeeId, string calldata signAlgo, bytes calldata rawProof, bytes calldata rawMonitorOrder) external returns (bool); +} \ No newline at end of file diff --git a/acb-sdk/pluginset/ethereum2/onchain-plugin/solidity/sys/interfaces/IPtcHub.sol b/acb-sdk/pluginset/ethereum2/onchain-plugin/solidity/sys/interfaces/IPtcHub.sol index ea01ca0b..6cedecc4 100644 --- a/acb-sdk/pluginset/ethereum2/onchain-plugin/solidity/sys/interfaces/IPtcHub.sol +++ b/acb-sdk/pluginset/ethereum2/onchain-plugin/solidity/sys/interfaces/IPtcHub.sol @@ -21,6 +21,8 @@ interface IPtcHub { event VerifyProof(CrossChainLane tpbtaLane, bool result); + function setMonitorVerifier(address newMonitorVerifierAddr) external; + function updatePTCTrustRoot(bytes calldata rawPtcTrustRoot) external; function getPTCTrustRoot(bytes calldata ptcOwnerOid) external view returns (bytes memory); diff --git a/acb-sdk/pluginset/ethereum2/onchain-plugin/solidity/sys/interfaces/IPtcVerifier.sol b/acb-sdk/pluginset/ethereum2/onchain-plugin/solidity/sys/interfaces/IPtcVerifier.sol index 7c0a8b78..558cdc53 100644 --- a/acb-sdk/pluginset/ethereum2/onchain-plugin/solidity/sys/interfaces/IPtcVerifier.sol +++ b/acb-sdk/pluginset/ethereum2/onchain-plugin/solidity/sys/interfaces/IPtcVerifier.sol @@ -8,7 +8,7 @@ interface IPtcVerifier { function verifyTpBta(PTCVerifyAnchor calldata va, TpBta calldata tpBta) external returns (bool); - function verifyTpProof(TpBta memory tpBta, ThirdPartyProof memory tpProof) external returns (bool); + function verifyTpProof(TpBta memory tpBta, ThirdPartyProof memory tpProof, address monitorPtcAddr) external returns (bool); function myPtcType() external returns (PTCTypeEnum); } \ No newline at end of file diff --git a/acb-sdk/pluginset/ethereum2/onchain-plugin/solidity/sys/interfaces/ISDPMessage.sol b/acb-sdk/pluginset/ethereum2/onchain-plugin/solidity/sys/interfaces/ISDPMessage.sol index 915d9826..335c79b7 100644 --- a/acb-sdk/pluginset/ethereum2/onchain-plugin/solidity/sys/interfaces/ISDPMessage.sol +++ b/acb-sdk/pluginset/ethereum2/onchain-plugin/solidity/sys/interfaces/ISDPMessage.sol @@ -129,7 +129,7 @@ interface ISDPMessage is ISubProtocol { * @param receiverID the address of the receiver. * @param message the raw message from DApp contracts */ - function sendMessage(string calldata receiverDomain, bytes32 receiverID, bytes calldata message) external; + function sendMessage(string calldata receiverDomain, bytes32 receiverID, address senderID, bytes calldata message) external; /** * @dev Smart contracts call this method to send cross-chain messages out of order in SDPv2. @@ -154,7 +154,7 @@ interface ISDPMessage is ISubProtocol { * @param receiverID the address of the receiver. * @param message the raw message from DApp contracts */ - function sendUnorderedMessage(string calldata receiverDomain, bytes32 receiverID, bytes calldata message) external; + function sendUnorderedMessage(string calldata receiverDomain, bytes32 receiverID, address senderID, bytes calldata message) external; /** * @dev Query the current sdp message sequence for the channel identited by `senderDomain`, diff --git a/acb-sdk/pluginset/ethereum2/onchain-plugin/solidity/sys/interfaces/ISubProtocol.sol b/acb-sdk/pluginset/ethereum2/onchain-plugin/solidity/sys/interfaces/ISubProtocol.sol index 4d25f4a1..53492ad9 100644 --- a/acb-sdk/pluginset/ethereum2/onchain-plugin/solidity/sys/interfaces/ISubProtocol.sol +++ b/acb-sdk/pluginset/ethereum2/onchain-plugin/solidity/sys/interfaces/ISubProtocol.sol @@ -18,4 +18,6 @@ interface ISubProtocol { * @param newAmContract the address of the AuthMessage contract. */ function setAmContract(address newAmContract) external; + + function setMonitorContract(address newMonitorAddress) external; } diff --git a/acb-sdk/pluginset/ethereum2/onchain-plugin/solidity/sys/lib/monitor/MonitorLib.sol b/acb-sdk/pluginset/ethereum2/onchain-plugin/solidity/sys/lib/monitor/MonitorLib.sol new file mode 100755 index 00000000..a05714d4 --- /dev/null +++ b/acb-sdk/pluginset/ethereum2/onchain-plugin/solidity/sys/lib/monitor/MonitorLib.sol @@ -0,0 +1,136 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "../utils/TypesToBytes.sol"; +import "../utils/BytesToTypes.sol"; +import "../utils/SizeOf.sol"; +import "../utils/TLVUtils.sol"; +import "../../@openzeppelin/contracts/utils/Strings.sol"; + +struct MonitorOrder { + string product; + string domain; + uint32 monitorOrderType; + string senderDomain; + bytes32 sender; + string receiverDomain; + bytes32 receiver; + string transactionContent; + string extra; +} + +struct MonitorMessage { + uint32 monitorType; + string monitorMsg; + bytes message; +} + +library MonitorLib { + // 监管字段(monitorType值) + uint32 constant public MONITOR_CLOSE = 1; + uint32 constant public MONITOR_OPEN = 2; + uint32 constant public MONITOR_ROLLBACK = 3; + + // 监管指令类型与子类型(32bit 每4bit为间隔 4bit中前1bit表示主类型 后3bit为子类型) + // 0000 0000 0000 0000 0000 0000 0000 0000: 从左到右共 8 个主类型 + // 第一个主类型-地址相关 + uint8 constant public MAJOR_TYPE_CONTRACT_ADDRESS = 1; + // 子类型-黑名单 + uint8 constant public MINOR_TYPE_ADD_TO_BLACKLIST = 0; + uint8 constant public MINOR_TYPE_REMOVE_FROM_BLACKLIST = 1; + + // 第二个主类型-控制相关 + uint8 constant public MAJOR_TYPE_CONTROL = 1; + // 子类型-是否开启监管 + uint8 constant public MINOR_TYPE_MONITOR_CLOSE = 0; + uint8 constant public MINOR_TYPE_MONITOR_OPEN = 1; + + // 事前监管失败类型 + uint8 constant public SENDER_IN_BLACKLIST = 0; + uint8 constant public RECEIVER_IN_BLACKLIST = 1; + + function encodeAddressIntoCrossChainID(address _address) internal pure returns (bytes32) { + bytes32 id = TypesToBytes.addressToBytes32(_address); + return id; + } + + function encodeCrossChainIDIntoAddress(bytes32 id) pure internal returns (address) { + bytes memory rawId = new bytes(32); + TypesToBytes.bytes32ToBytes(32, id, rawId); + return BytesToTypes.bytesToAddress(32, rawId); + } + + function decode(MonitorOrder memory monitorOrder, bytes memory rawMessage) internal pure { + uint256 offset = rawMessage.length; + + bytes memory raw_product = BytesToTypes.bytesToVarBytes(offset, rawMessage); + monitorOrder.product = string(raw_product); + offset -= 4 + raw_product.length; + + bytes memory raw_domain = BytesToTypes.bytesToVarBytes(offset, rawMessage); + monitorOrder.domain = string(raw_domain); + offset -= 4 + raw_domain.length; + + monitorOrder.monitorOrderType = BytesToTypes.bytesToUint32(offset, rawMessage); + offset -= 4; + + bytes memory raw_send_domain = BytesToTypes.bytesToVarBytes(offset, rawMessage); + monitorOrder.senderDomain = string(raw_send_domain); + offset -= 4 + raw_send_domain.length; + + monitorOrder.sender = BytesToTypes.bytesToBytes32(offset, rawMessage); + offset -= SizeOf.sizeOfBytes32(); + + bytes memory raw_recv_domain = BytesToTypes.bytesToVarBytes(offset, rawMessage); + monitorOrder.receiverDomain = string(raw_recv_domain); + offset -= 4 + raw_recv_domain.length; + + monitorOrder.receiver = BytesToTypes.bytesToBytes32(offset, rawMessage); + offset -= SizeOf.sizeOfBytes32(); + + bytes memory raw_tran_cont = BytesToTypes.bytesToVarBytes(offset, rawMessage); + monitorOrder.transactionContent = string(raw_tran_cont); + offset -= 4 + raw_tran_cont.length; + + bytes memory raw_extra = BytesToTypes.bytesToVarBytes(offset, rawMessage); + monitorOrder.extra = string(raw_extra); + offset -= 4 + raw_extra.length; + } + + function encode(MonitorMessage memory monitorMessage) pure internal returns (bytes memory) { + require( + monitorMessage.message.length <= 0xFFFFFFFF, + "encodeSDPMessage: body length overlimit" + ); + // 4 + (4 + monitorMsg) + (4 + payload) + uint total_size = 12 + bytes(monitorMessage.monitorMsg).length + monitorMessage.message.length; + bytes memory pkg = new bytes(total_size); + uint offset = total_size; + + TypesToBytes.uint32ToBytes(offset, monitorMessage.monitorType, pkg); + offset -= SizeOf.sizeOfUint(32); + + bytes memory raw_monitorMsg = bytes(monitorMessage.monitorMsg); + TypesToBytes.varBytesToBytes(offset, raw_monitorMsg, pkg); + offset -= 4 + raw_monitorMsg.length; + + TypesToBytes.varBytesToBytes(offset, monitorMessage.message, pkg); + offset -= 4 + monitorMessage.message.length; + + return pkg; + } + + function decode(MonitorMessage memory monitorMessage, bytes memory rawMessage) internal pure { + uint256 offset = rawMessage.length; + + monitorMessage.monitorType = BytesToTypes.bytesToUint32(offset, rawMessage); + offset -= SizeOf.sizeOfInt(32); + + bytes memory raw_monitorMsg = BytesToTypes.bytesToVarBytes(offset, rawMessage); + monitorMessage.monitorMsg = string(raw_monitorMsg); + offset -= 4 + raw_monitorMsg.length; + + monitorMessage.message = BytesToTypes.bytesToVarBytes(offset, rawMessage); + offset -= 4 + monitorMessage.message.length; + } +} \ No newline at end of file diff --git a/acb-sdk/pluginset/ethereum2/onchain-plugin/solidity/sys/lib/ptc/CommitteeLib.sol b/acb-sdk/pluginset/ethereum2/onchain-plugin/solidity/sys/lib/ptc/CommitteeLib.sol index 4212e9f9..34ed885c 100644 --- a/acb-sdk/pluginset/ethereum2/onchain-plugin/solidity/sys/lib/ptc/CommitteeLib.sol +++ b/acb-sdk/pluginset/ethereum2/onchain-plugin/solidity/sys/lib/ptc/CommitteeLib.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.0; pragma experimental ABIEncoderV2; import "./PtcLib.sol"; +import "../../interfaces/IMonitorVerifier.sol"; import "../../@openzeppelin/contracts/utils/Strings.sol"; // tags for CommitteeEndorseRoot @@ -94,6 +95,8 @@ library CommitteeLib { event DoVeriyTpBta(CrossChainLane laneKey, string committeeId, string nodeId, bool result); event DoVeriyTpProof(CrossChainLane laneKey, string committeeId, string nodeId, bool result); + event DoVerifyTpProofFromMonitorNode(CrossChainLane laneKey, string committeeId, string nodeId, bool result); + event DoVerifyMonitorOrder(string committeeId, string nodeId, bool res); using TLVUtils for TLVPacket; using TLVUtils for TLVItem; @@ -162,7 +165,7 @@ library CommitteeLib { return 3 * correct > 2 * cva.anchors.length; } - function verifyTpProof(TpBta memory tpBta, ThirdPartyProof memory tpProof) internal returns (bool) { + function verifyTpProof(TpBta memory tpBta, ThirdPartyProof memory tpProof , address monitorPtcAddr) internal returns (bool) { CommitteeEndorseRoot memory cer = decodeCommitteeEndorseRootFrom(tpBta.endorseRoot); CommitteeEndorseProof memory ceProof = decodeCommitteeEndorseProofFrom(tpProof.rawProof); @@ -173,9 +176,17 @@ library CommitteeLib { for (uint i = 0; i < cer.endorsers.length; i++) { NodeEndorseInfo memory info = cer.endorsers[i]; + bool isMonitorNode = false; bool res = false; for (uint j = 0; j < ceProof.sigs.length; j++) { if (info.nodeId.equal(ceProof.sigs[j].nodeId)) { + // if it is monitor node proof, trans it to MonitorPTC + if (checkMonitorNode(info.nodeId)) { + isMonitorNode = true; + IMonitorVerifier(monitorPtcAddr).receiveMonitorNodeProofMessage(tpBta.crossChainLane, cer.committeeId, ceProof.sigs[j], encodedToSign); + res = true; + break; + } res = AcbCommons.verifySig( ceProof.sigs[j].signAlgo, info.publicKey.getRawPublicKey(), @@ -188,8 +199,9 @@ library CommitteeLib { } } } - - emit DoVeriyTpProof(tpBta.crossChainLane, cer.committeeId, info.nodeId, res); + if (!isMonitorNode) { + emit DoVeriyTpProof(tpBta.crossChainLane, cer.committeeId, info.nodeId, res); + } if (!res && info.required) { return false; } @@ -198,6 +210,59 @@ library CommitteeLib { return cer.policy.threshold.check(optinalCorrect); } + function checkMonitorNode(string memory nodeId) internal pure returns (bool) { + bytes memory prefix = bytes("monitor"); + bytes memory nodeIdBytes = bytes(nodeId); + + if (nodeIdBytes.length < prefix.length) { + return false; + } + + for (uint i = 0; i < prefix.length; i++) { + if (nodeIdBytes[i] != prefix[i]) { + return false; + } + } + + return true; + } + + function verifyTpProofFromMonitorNode( + CrossChainLane memory crossChainLane, + string memory committeeId, + NodeEndorseInfo memory monitorNodeEndorseInfo, + CommitteeNodeProof memory monitorNodeProof, + bytes memory encodedToSign + ) internal returns (bool) { + bool res = false; + res = AcbCommons.verifySig( + monitorNodeProof.signAlgo, + monitorNodeEndorseInfo.publicKey.getRawPublicKey(), + encodedToSign, + monitorNodeProof.signature + ); + emit DoVerifyTpProofFromMonitorNode(crossChainLane, committeeId, monitorNodeEndorseInfo.nodeId, res); + return res; + } + + function verifyMonitorOrder( + string memory committeeId, + NodeEndorseInfo memory monitorNodeEndorseInfo, + string memory signAlgo, + bytes memory rawProof, + bytes memory rawMonitorOrder + ) internal returns (bool) { + bool res = false; + res = AcbCommons.verifySig( + signAlgo, + monitorNodeEndorseInfo.publicKey.getRawPublicKey(), + rawMonitorOrder, + rawProof + ); + emit DoVerifyMonitorOrder(committeeId, monitorNodeEndorseInfo.nodeId, res); + return res; + } + function getRawPublicKey( NodePublicKeyEntry memory entry ) internal pure returns (bytes memory) { diff --git "a/docs/images/\345\220\253\347\233\221\347\256\241\347\232\204\350\267\250\351\223\276\346\265\201\347\250\213\345\233\276.png" "b/docs/images/\345\220\253\347\233\221\347\256\241\347\232\204\350\267\250\351\223\276\346\265\201\347\250\213\345\233\276.png" new file mode 100644 index 0000000000000000000000000000000000000000..66f099487bc261352e6ec4ad7d8905a9f2cd6fba GIT binary patch literal 385054 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%0D;o2k-%dci>bsT+C3@@ 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 literal 0 HcmV?d00001 diff --git "a/docs/images/\345\220\253\347\233\221\347\256\241\347\232\204\350\267\250\351\223\276\346\266\210\346\201\257\347\273\223\346\236\204.png" "b/docs/images/\345\220\253\347\233\221\347\256\241\347\232\204\350\267\250\351\223\276\346\266\210\346\201\257\347\273\223\346\236\204.png" new file mode 100644 index 0000000000000000000000000000000000000000..d2b0553109d772085432c38703784d52b437beca GIT binary patch literal 40790 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 ZFjos6p3q<@dtd+n0000000000003}}L(u>L literal 0 HcmV?d00001 diff --git "a/docs/images/\347\233\221\347\256\241\346\214\207\344\273\244\347\261\273\345\236\213monitorOrderType\347\232\204\347\273\223\346\236\204.png" "b/docs/images/\347\233\221\347\256\241\346\214\207\344\273\244\347\261\273\345\236\213monitorOrderType\347\232\204\347\273\223\346\236\204.png" new file mode 100644 index 0000000000000000000000000000000000000000..c8be7e8fee57f119c9ea1342230647a8014be244 GIT binary patch literal 15492 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