Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions Sources/blocks/Domain/Block.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand All @@ -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.")
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
29 changes: 25 additions & 4 deletions Sources/blocks/Domain/Transaction.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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.
Expand All @@ -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 署名する
Expand Down
33 changes: 32 additions & 1 deletion Sources/blocks/Domain/Transactions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
76 changes: 76 additions & 0 deletions Tests/blocksTests/SameBlockBalanceValidationTest.swift
Original file line number Diff line number Diff line change
@@ -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"
)
}
}