diff --git a/Sources/blocks/Domain/Block.swift b/Sources/blocks/Domain/Block.swift index 09cf012..987a90d 100755 --- a/Sources/blocks/Domain/Block.swift +++ b/Sources/blocks/Domain/Block.swift @@ -383,7 +383,7 @@ public struct Block { Log() if let transactionSignature = $0.signature, let transactionId = $0.transactionId, let date = $0.date, let publicKey = $0.publicKey { Log() - let validTransactionCauseAdded = addTransaction(claim: $0.claim, claimObject: $0.claimObject, type: $0.type.rawValue, makerDhtAddressAsHexString: $0.makerDhtAddressAsHexString, signature: transactionSignature, publicKeyAsData: publicKey, transactionId: transactionId, date: date, chainable: chainable, branchChainHash: branchChainHash, indexInBranchChain: indexInBranchChain) + let validTransactionCauseAdded = addTransaction(claim: $0.claim, claimObject: $0.claimObject, type: $0.type.rawValue, makerDhtAddressAsHexString: $0.makerDhtAddressAsHexString, signature: transactionSignature, publicKeyAsData: publicKey, transactionId: transactionId, date: date, chainable: chainable, branchChainHash: branchChainHash, indexInBranchChain: indexInBranchChain, debitOnLeft: $0.debitOnLeft, creditOnRight: $0.creditOnRight, withdrawalDhtAddressOnLeft: $0.withdrawalDhtAddressOnLeft.toString, depositDhtAddressOnRight: $0.depositDhtAddressOnRight.toString) if addedAll && validTransactionCauseAdded { addedAll = true } else { @@ -416,7 +416,7 @@ public struct Block { let bookerFeeTransactionPublicKey = bookerFeeTransaction.publicKey, let date = bookerFeeTransaction.date { let validTransactionCauseAdded = addTransaction(claim: bookerFeeTransaction.claim, claimObject: bookerFeeTransaction.claimObject, type: bookerFeeTransaction.type.rawValue, makerDhtAddressAsHexString: bookerFeeTransaction.makerDhtAddressAsHexString, signature: bookerFeeTransactionSignature, - publicKeyAsData: bookerFeeTransactionPublicKey, transactionId: transactionId, date: date, chainable: chainable, branchChainHash: branchChainHash, indexInBranchChain: indexInBranchChain) + publicKeyAsData: bookerFeeTransactionPublicKey, transactionId: transactionId, date: date, chainable: chainable, branchChainHash: branchChainHash, indexInBranchChain: indexInBranchChain, debitOnLeft: bookerFeeTransaction.debitOnLeft, creditOnRight: bookerFeeTransaction.creditOnRight, withdrawalDhtAddressOnLeft: bookerFeeTransaction.withdrawalDhtAddressOnLeft.toString, depositDhtAddressOnRight: bookerFeeTransaction.depositDhtAddressOnRight.toString) if addedAll && validTransactionCauseAdded { addedAll = true } else { @@ -434,7 +434,7 @@ public struct Block { & Add A Transaction to Block */ - public mutating func addTransaction(claim: any Claim, claimObject: any ClaimObject, type: String, makerDhtAddressAsHexString: OverlayNetworkAddressAsHexString, signature: Signature, publicKeyAsData: PublicKey, transactionId: TransactionIdentification, date: Date, chainable: Book.ChainableResult, branchChainHash: HashedString?, indexInBranchChain: Int?) -> Bool { + public mutating func addTransaction(claim: any Claim, claimObject: any ClaimObject, type: String, makerDhtAddressAsHexString: OverlayNetworkAddressAsHexString, signature: Signature, publicKeyAsData: PublicKey, transactionId: TransactionIdentification, date: Date, chainable: Book.ChainableResult, branchChainHash: HashedString?, indexInBranchChain: Int?, debitOnLeft: BK = Decimal.zero, creditOnRight: BK = Decimal.zero, withdrawalDhtAddressOnLeft: String = "", depositDhtAddressOnRight: String = "") -> Bool { Log() if isThereSameTransaction(signature: signature) { Log("Duplicate Transaction in Block.") @@ -456,9 +456,9 @@ public struct Block { if let type = TransactionType(rawValue: type) { Log() - if let transaction = type.construct(claim: claim, claimObject: claimObject, makerDhtAddressAsHexString: makerDhtAddressAsHexString, publicKey: publicKeyAsData, signature: signatureData, book: self.book, signer: signer, transactionId: transactionId, date: date) { + if let transaction = type.construct(claim: claim, claimObject: claimObject, makerDhtAddressAsHexString: makerDhtAddressAsHexString, publicKey: publicKeyAsData, signature: signatureData, book: self.book, signer: signer, transactionId: transactionId, date: date, debitOnLeft: debitOnLeft, creditOnRight: creditOnRight, withdrawalDhtAddressOnLeft: withdrawalDhtAddressOnLeft, depositDhtAddressOnRight: depositDhtAddressOnRight) { Log(transaction.jsonString) - if transaction.validate(chainable: chainable, branchChainHash: branchChainHash, indexInBranchChain: indexInBranchChain) { + if transaction.validate(chainable: chainable, branchChainHash: branchChainHash, indexInBranchChain: indexInBranchChain, transactionsInSameBlock: self.transactions) { Log("Valid Transaction Cause Add to Block. \(String(describing: transaction.transactionId))") self.transactions += [transaction] return true @@ -810,10 +810,12 @@ public struct Block { return false } var validated = true + var validatedTransactions = [any Transaction]() transactions.forEach { Log() - if $0.validate(chainable: chainable, branchChainHash: branchChainHash, indexInBranchChain: indexInBranchChain) { + if $0.validate(chainable: chainable, branchChainHash: branchChainHash, indexInBranchChain: indexInBranchChain, transactionsInSameBlock: validatedTransactions) { Log() + validatedTransactions += [$0] } else { Log() validated = false diff --git a/Sources/blocks/Domain/Transaction.swift b/Sources/blocks/Domain/Transaction.swift index a1f0033..8156fde 100755 --- a/Sources/blocks/Domain/Transaction.swift +++ b/Sources/blocks/Domain/Transaction.swift @@ -163,7 +163,7 @@ public extension Transaction { get { if let signature = self.signature?.toString, let transactionId = self.transactionId, let dateString = self.date?.utcTimeString, let claimObject = self.claimObject.toJsonString(signer: self.signer, peerSigner: self.peerSigner), let claim = self.claim.rawValue { var json = """ -{"transactionId":"\(transactionId)","date":"\(dateString)","type":"\(self.type.rawValue)","claim":"\(claim)","claimObject":\(claimObject),"signature":"\(signature)"} +{"transactionId":"\(transactionId)","date":"\(dateString)","type":"\(self.type.rawValue)","claim":"\(claim)","claimObject":\(claimObject),"signature":"\(signature)","debitOnLeft":"\(self.debitOnLeft.canonicalDecimalString)","withdrawalDhtAddressOnLeft":"\(self.withdrawalDhtAddressOnLeft)","creditOnRight":"\(self.creditOnRight.canonicalDecimalString)","depositDhtAddressOnRight":"\(self.depositDhtAddressOnRight)"} """ Log(json) //remove \n @@ -179,7 +179,7 @@ public extension Transaction { get { if let signature = self.signature?.toString, let transactionId = self.transactionId, let dateString = self.date?.utcTimeString, let claimObject = self.claimObject.toJsonString(signer: self.signer, peerSigner: self.peerSigner), let claim = self.claim.rawValue { var json = """ -{"transactionId":"\(transactionId)","date":"\(dateString)","type":"\(self.type.rawValue)","makerDhtAddressAsHexString":"\(self.makerDhtAddressAsHexString)","publicKey":"\(self.publicKey?.publicKeyToString ?? "")","claim":"\(claim)","claimObject":\(claimObject),"signature":"\(signature)"} +{"transactionId":"\(transactionId)","date":"\(dateString)","type":"\(self.type.rawValue)","makerDhtAddressAsHexString":"\(self.makerDhtAddressAsHexString)","publicKey":"\(self.publicKey?.publicKeyToString ?? "")","claim":"\(claim)","claimObject":\(claimObject),"signature":"\(signature)","debitOnLeft":"\(self.debitOnLeft.canonicalDecimalString)","withdrawalDhtAddressOnLeft":"\(self.withdrawalDhtAddressOnLeft)","creditOnRight":"\(self.creditOnRight.canonicalDecimalString)","depositDhtAddressOnRight":"\(self.depositDhtAddressOnRight)"} """ Log(json) //remove \n @@ -305,7 +305,7 @@ public extension Transaction { Paper: 5) ノードは、ブロック内のすべてのトランザクションが有効で、まだ使用されていない場合にのみブロックを受け入れます。 */ - func validate(chainable: Book.ChainableResult = .chainableBlock, branchChainHash: HashedString?, indexInBranchChain: Int?) -> Bool { + func validate(chainable: Book.ChainableResult = .chainableBlock, branchChainHash: HashedString?, indexInBranchChain: Int?, transactionsInSameBlock: [any Transaction] = []) -> Bool { Log() guard let contentData = self.canonicalSignaturePayloadData, let contentHashedData = contentData.hashedData?.toData, let signature = self.signature else { Log("transaction signature false") @@ -387,7 +387,7 @@ public extension Transaction { //transactionの送金金額+手数料 <= balance Log() - let balancedAmount = self.book.balance(dhtAddressAsHexString: self.makerDhtAddressAsHexString) + let balancedAmount = self.availableBalance(transactionsInSameBlock: transactionsInSameBlock) Log("\(self.debitOnLeft) + \(self.feeForBooker) <= \(balancedAmount)") guard self.debitOnLeft.asDecimal + self.feeForBooker.asDecimal <= balancedAmount.asDecimal else { //Short of balances. @@ -401,6 +401,27 @@ public extension Transaction { Log("transaction validate true") return true } + + private func availableBalance(transactionsInSameBlock: [any Transaction]) -> BK { + let dhtAddress = self.makerDhtAddressAsHexString + let confirmedBalance = self.book.balance(dhtAddressAsHexString: dhtAddress).asDecimal + let sameBlockBalanceDelta = transactionsInSameBlock.reduce(Decimal.zero) { balanceDelta, transaction in + balanceDelta + transaction.balanceDelta(for: dhtAddress) + } + return confirmedBalance + sameBlockBalanceDelta + } + + private func balanceDelta(for dhtAddress: OverlayNetworkAddressAsHexString) -> Decimal { + var balanceDelta = Decimal.zero + if self.withdrawalDhtAddressOnLeft.equal(dhtAddress) { + balanceDelta -= self.debitOnLeft.asDecimal + balanceDelta -= self.feeForBooker.asDecimal + } + if self.depositDhtAddressOnRight.equal(dhtAddress) { + balanceDelta += self.creditOnRight.asDecimal + } + return balanceDelta + } /* Sign 署名する diff --git a/Sources/blocks/Domain/Transactions.swift b/Sources/blocks/Domain/Transactions.swift index 38fccc1..97cd5f9 100755 --- a/Sources/blocks/Domain/Transactions.swift +++ b/Sources/blocks/Domain/Transactions.swift @@ -87,12 +87,43 @@ public struct Transactions { Log() if let claimObject = claimObject { Log() - return typeAsTransactionType.construct(claim: claim, claimObject: claimObject, makerDhtAddressAsHexString: signer.makerDhtAddressAsHexString, publicKey: signer.publicKeyAsData, signature: signature, book: self.book, signer: signer, transactionId: transactionId, date: dateString.date) + return typeAsTransactionType.construct( + claim: claim, + claimObject: claimObject, + makerDhtAddressAsHexString: signer.makerDhtAddressAsHexString, + publicKey: signer.publicKeyAsData, + signature: signature, + book: self.book, + signer: signer, + transactionId: transactionId, + date: dateString.date, + debitOnLeft: decimalValue(for: "debitOnLeft", in: dictionary), + creditOnRight: decimalValue(for: "creditOnRight", in: dictionary), + withdrawalDhtAddressOnLeft: stringValue(for: "withdrawalDhtAddressOnLeft", in: dictionary), + depositDhtAddressOnRight: stringValue(for: "depositDhtAddressOnRight", in: dictionary) + ) } } return nil } + private func decimalValue(for key: String, in dictionary: [String: Any]) -> Decimal { + if let decimal = dictionary[key] as? Decimal { + return decimal + } + if let number = dictionary[key] as? NSNumber { + return number.decimalValue + } + if let string = dictionary[key] as? String, let decimal = Decimal(string: string) { + return decimal + } + return Decimal.zero + } + + private func stringValue(for key: String, in dictionary: [String: Any]) -> String { + dictionary[key] as? String ?? "" + } + public var stringToTransactions: [any Transaction]? { do { Log(self.string as Any) diff --git a/Tests/blocksTests/SameBlockBalanceValidationTest.swift b/Tests/blocksTests/SameBlockBalanceValidationTest.swift new file mode 100644 index 0000000..e6fbd35 --- /dev/null +++ b/Tests/blocksTests/SameBlockBalanceValidationTest.swift @@ -0,0 +1,76 @@ +// +// SameBlockBalanceValidationTest.swift +// blocksTests +// +// Created by Codex on 2026/05/24. +// + +import XCTest +@testable import blocks +import overlayNetwork + +final class SameBlockBalanceValidationTest: XCTestCase { + + func testRejectsSecondSpendWhenSameBlockTotalExceedsBalance() throws { + let signer = Signer(newPrivateKeyOn: "maker-account" as OverlayNetworkAddressAsHexString) + let book = try fundedBook(for: signer, amount: Decimal(102)) + let firstSpend = try payment(from: signer, book: book, id: "BK-first-spend", amount: Decimal(60)) + let secondSpend = try payment(from: signer, book: book, id: "BK-second-spend", amount: Decimal(60)) + + XCTAssertTrue(firstSpend.validate(branchChainHash: nil, indexInBranchChain: nil)) + XCTAssertFalse(secondSpend.validate(branchChainHash: nil, indexInBranchChain: nil, transactionsInSameBlock: [firstSpend])) + } + + func testAllowsSameBlockSpendsWithinAvailableBalance() throws { + let signer = Signer(newPrivateKeyOn: "maker-account" as OverlayNetworkAddressAsHexString) + let book = try fundedBook(for: signer, amount: Decimal(102)) + let firstSpend = try payment(from: signer, book: book, id: "BK-first-spend", amount: Decimal(40)) + let secondSpend = try payment(from: signer, book: book, id: "BK-second-spend", amount: Decimal(60)) + + XCTAssertTrue(firstSpend.validate(branchChainHash: nil, indexInBranchChain: nil)) + XCTAssertTrue(secondSpend.validate(branchChainHash: nil, indexInBranchChain: nil, transactionsInSameBlock: [firstSpend])) + } + + private func fundedBook(for signer: Signer, amount: Decimal) throws -> Book { + let publicKey = try XCTUnwrap(signer.publicKeyAsData) + let credit = Pay( + claim: ClaimOnPay.bookerFee, + claimObject: ClaimOnPay.Object(destination: signer.makerDhtAddressAsHexString), + makerDhtAddressAsHexString: signer.makerDhtAddressAsHexString, + publicKey: publicKey, + book: Book(signature: Data.DataNull), + signer: signer, + transactionId: "BK-initial-credit", + date: Date(timeIntervalSince1970: 1_700_000_000), + debitOnLeft: amount, + creditOnRight: amount, + withdrawalDhtAddressOnLeft: Signer.moneySupplyUnMoverAccount, + depositDhtAddressOnRight: signer.makerDhtAddressAsHexString.toString + ) + + var block = Block.genesis + block.transactions = [credit] + + var book = Book(signature: Data.DataNull) + book.blocks = [block] + return book + } + + private func payment(from signer: Signer, book: Book, id: TransactionIdentification, amount: Decimal) throws -> Pay { + let publicKey = try XCTUnwrap(signer.publicKeyAsData) + return Pay( + claim: ClaimOnPay.bookerFeeReply, + claimObject: ClaimOnPay.Object(destination: "deposit-account"), + makerDhtAddressAsHexString: signer.makerDhtAddressAsHexString, + publicKey: publicKey, + book: book, + signer: signer, + transactionId: id, + date: Date(timeIntervalSince1970: 1_700_000_100), + debitOnLeft: amount, + creditOnRight: amount, + withdrawalDhtAddressOnLeft: signer.makerDhtAddressAsHexString.toString, + depositDhtAddressOnRight: "deposit-account" + ) + } +}