gitlab.com/jokerrs1/Sia@v1.3.2/modules/consensus/accept_txntypes_test.go (about) 1 package consensus 2 3 import ( 4 "testing" 5 6 "github.com/NebulousLabs/Sia/crypto" 7 "github.com/NebulousLabs/Sia/types" 8 "github.com/NebulousLabs/fastrand" 9 ) 10 11 // testBlockSuite tests a wide variety of blocks. 12 func (cst *consensusSetTester) testBlockSuite() { 13 cst.testSimpleBlock() 14 cst.testSpendSiacoinsBlock() 15 cst.testValidStorageProofBlocks() 16 cst.testMissedStorageProofBlocks() 17 cst.testFileContractRevision() 18 cst.testSpendSiafunds() 19 } 20 21 // testSimpleBlock mines a simple block (no transactions except those 22 // automatically added by the miner) and adds it to the consnesus set. 23 func (cst *consensusSetTester) testSimpleBlock() { 24 // Get the starting hash of the consenesus set. 25 initialChecksum := cst.cs.dbConsensusChecksum() 26 initialHeight := cst.cs.dbBlockHeight() 27 initialBlockID := cst.cs.dbCurrentBlockID() 28 29 // Mine and submit a block 30 block, err := cst.miner.AddBlock() 31 if err != nil { 32 panic(err) 33 } 34 35 // Check that the consensus info functions changed as expected. 36 resultingChecksum := cst.cs.dbConsensusChecksum() 37 if initialChecksum == resultingChecksum { 38 panic("checksum is unchanged after mining a block") 39 } 40 resultingHeight := cst.cs.dbBlockHeight() 41 if resultingHeight != initialHeight+1 { 42 panic("height of consensus set did not increase as expected") 43 } 44 currentPB := cst.cs.dbCurrentProcessedBlock() 45 if currentPB.Block.ParentID != initialBlockID { 46 panic("new processed block does not have correct information") 47 } 48 if currentPB.Block.ID() != block.ID() { 49 panic("the state's current block is not reporting as the recently mined block.") 50 } 51 if currentPB.Height != initialHeight+1 { 52 panic("the processed block is not reporting the correct height") 53 } 54 pathID, err := cst.cs.dbGetPath(currentPB.Height) 55 if err != nil { 56 panic(err) 57 } 58 if pathID != block.ID() { 59 panic("current path does not point to the correct block") 60 } 61 62 // Revert the block that was just added to the consensus set and check for 63 // parity with the original state of consensus. 64 parent, err := cst.cs.dbGetBlockMap(currentPB.Block.ParentID) 65 if err != nil { 66 panic(err) 67 } 68 _, _, err = cst.cs.dbForkBlockchain(parent) 69 if err != nil { 70 panic(err) 71 } 72 if cst.cs.dbConsensusChecksum() != initialChecksum { 73 panic("adding and reverting a block changed the consensus set") 74 } 75 // Re-add the block and check for parity with the first time it was added. 76 // This test is useful because a different codepath is followed if the 77 // diffs have already been generated. 78 _, _, err = cst.cs.dbForkBlockchain(currentPB) 79 if err != nil { 80 panic(err) 81 } 82 if cst.cs.dbConsensusChecksum() != resultingChecksum { 83 panic("adding, reverting, and reading a block was inconsistent with just adding the block") 84 } 85 } 86 87 // TestIntegrationSimpleBlock creates a consensus set tester and uses it to 88 // call testSimpleBlock. 89 func TestIntegrationSimpleBlock(t *testing.T) { 90 if testing.Short() { 91 t.SkipNow() 92 } 93 t.Parallel() 94 cst, err := createConsensusSetTester(t.Name()) 95 if err != nil { 96 t.Fatal(err) 97 } 98 defer cst.Close() 99 cst.testSimpleBlock() 100 } 101 102 // testSpendSiacoinsBlock mines a block with a transaction spending siacoins 103 // and adds it to the consensus set. 104 func (cst *consensusSetTester) testSpendSiacoinsBlock() { 105 // Create a random destination address for the output in the transaction. 106 destAddr := randAddress() 107 108 // Create a block containing a transaction with a valid siacoin output. 109 txnValue := types.NewCurrency64(1200) 110 txnBuilder := cst.wallet.StartTransaction() 111 err := txnBuilder.FundSiacoins(txnValue) 112 if err != nil { 113 panic(err) 114 } 115 outputIndex := txnBuilder.AddSiacoinOutput(types.SiacoinOutput{Value: txnValue, UnlockHash: destAddr}) 116 txnSet, err := txnBuilder.Sign(true) 117 if err != nil { 118 panic(err) 119 } 120 err = cst.tpool.AcceptTransactionSet(txnSet) 121 if err != nil { 122 panic(err) 123 } 124 125 // Mine and apply the block to the consensus set. 126 _, err = cst.miner.AddBlock() 127 if err != nil { 128 panic(err) 129 } 130 131 // See that the destination output was created. 132 outputID := txnSet[len(txnSet)-1].SiacoinOutputID(outputIndex) 133 sco, err := cst.cs.dbGetSiacoinOutput(outputID) 134 if err != nil { 135 panic(err) 136 } 137 if !sco.Value.Equals(txnValue) { 138 panic("output added with wrong value") 139 } 140 if sco.UnlockHash != destAddr { 141 panic("output sent to the wrong address") 142 } 143 } 144 145 // TestIntegrationSpendSiacoinsBlock creates a consensus set tester and uses it 146 // to call testSpendSiacoinsBlock. 147 func TestIntegrationSpendSiacoinsBlock(t *testing.T) { 148 if testing.Short() { 149 t.SkipNow() 150 } 151 t.Parallel() 152 cst, err := createConsensusSetTester(t.Name()) 153 if err != nil { 154 t.Fatal(err) 155 } 156 defer cst.Close() 157 cst.testSpendSiacoinsBlock() 158 } 159 160 // testValidStorageProofBlocks adds a block with a file contract, and then 161 // submits a storage proof for that file contract. 162 func (cst *consensusSetTester) testValidStorageProofBlocks() { 163 // COMPATv0.4.0 - Step the block height up past the hardfork amount. This 164 // code stops nondeterministic failures when producing storage proofs that 165 // is related to buggy old code. 166 for cst.cs.dbBlockHeight() <= 10 { 167 _, err := cst.miner.AddBlock() 168 if err != nil { 169 panic(err) 170 } 171 } 172 173 // Create a file (as a bytes.Buffer) that will be used for the file 174 // contract. 175 filesize := uint64(4e3) 176 file := fastrand.Bytes(int(filesize)) 177 merkleRoot := crypto.MerkleRoot(file) 178 179 // Create a file contract that will be successful. 180 validProofDest := randAddress() 181 payout := types.NewCurrency64(400e6) 182 fc := types.FileContract{ 183 FileSize: filesize, 184 FileMerkleRoot: merkleRoot, 185 WindowStart: cst.cs.dbBlockHeight() + 1, 186 WindowEnd: cst.cs.dbBlockHeight() + 2, 187 Payout: payout, 188 ValidProofOutputs: []types.SiacoinOutput{{ 189 UnlockHash: validProofDest, 190 Value: types.PostTax(cst.cs.dbBlockHeight(), payout), 191 }}, 192 MissedProofOutputs: []types.SiacoinOutput{{ 193 UnlockHash: types.UnlockHash{}, 194 Value: types.PostTax(cst.cs.dbBlockHeight(), payout), 195 }}, 196 } 197 198 // Submit a transaction with the file contract. 199 oldSiafundPool := cst.cs.dbGetSiafundPool() 200 txnBuilder := cst.wallet.StartTransaction() 201 err := txnBuilder.FundSiacoins(payout) 202 if err != nil { 203 panic(err) 204 } 205 fcIndex := txnBuilder.AddFileContract(fc) 206 txnSet, err := txnBuilder.Sign(true) 207 if err != nil { 208 panic(err) 209 } 210 err = cst.tpool.AcceptTransactionSet(txnSet) 211 if err != nil { 212 panic(err) 213 } 214 _, err = cst.miner.AddBlock() 215 if err != nil { 216 panic(err) 217 } 218 219 // Check that the siafund pool was increased by the tax on the payout. 220 siafundPool := cst.cs.dbGetSiafundPool() 221 if !siafundPool.Equals(oldSiafundPool.Add(types.Tax(cst.cs.dbBlockHeight()-1, payout))) { 222 panic("siafund pool was not increased correctly") 223 } 224 225 // Check that the file contract made it into the database. 226 ti := len(txnSet) - 1 227 fcid := txnSet[ti].FileContractID(fcIndex) 228 _, err = cst.cs.dbGetFileContract(fcid) 229 if err != nil { 230 panic(err) 231 } 232 233 // Create and submit a storage proof for the file contract. 234 segmentIndex, err := cst.cs.StorageProofSegment(fcid) 235 if err != nil { 236 panic(err) 237 } 238 segment, hashSet := crypto.MerkleProof(file, segmentIndex) 239 sp := types.StorageProof{ 240 ParentID: fcid, 241 HashSet: hashSet, 242 } 243 copy(sp.Segment[:], segment) 244 txnBuilder = cst.wallet.StartTransaction() 245 txnBuilder.AddStorageProof(sp) 246 txnSet, err = txnBuilder.Sign(true) 247 if err != nil { 248 panic(err) 249 } 250 err = cst.tpool.AcceptTransactionSet(txnSet) 251 if err != nil { 252 panic(err) 253 } 254 _, err = cst.miner.AddBlock() 255 if err != nil { 256 panic(err) 257 } 258 259 // Check that the file contract has been removed. 260 _, err = cst.cs.dbGetFileContract(fcid) 261 if err != errNilItem { 262 panic("file contract should not exist in the database") 263 } 264 265 // Check that the siafund pool has not changed. 266 postProofPool := cst.cs.dbGetSiafundPool() 267 if !postProofPool.Equals(siafundPool) { 268 panic("siafund pool should not change after submitting a storage proof") 269 } 270 271 // Check that a delayed output was created for the valid proof. 272 spoid := fcid.StorageProofOutputID(types.ProofValid, 0) 273 dsco, err := cst.cs.dbGetDSCO(cst.cs.dbBlockHeight()+types.MaturityDelay, spoid) 274 if err != nil { 275 panic(err) 276 } 277 if dsco.UnlockHash != fc.ValidProofOutputs[0].UnlockHash { 278 panic("wrong unlock hash in dsco") 279 } 280 if !dsco.Value.Equals(fc.ValidProofOutputs[0].Value) { 281 panic("wrong sco value in dsco") 282 } 283 } 284 285 // TestIntegrationValidStorageProofBlocks creates a consensus set tester and 286 // uses it to call testValidStorageProofBlocks. 287 func TestIntegrationValidStorageProofBlocks(t *testing.T) { 288 if testing.Short() { 289 t.SkipNow() 290 } 291 t.Parallel() 292 cst, err := createConsensusSetTester(t.Name()) 293 if err != nil { 294 t.Fatal(err) 295 } 296 defer cst.Close() 297 cst.testValidStorageProofBlocks() 298 } 299 300 // testMissedStorageProofBlocks adds a block with a file contract, and then 301 // fails to submit a storage proof before expiration. 302 func (cst *consensusSetTester) testMissedStorageProofBlocks() { 303 // Create a file contract that will be successful. 304 filesize := uint64(4e3) 305 payout := types.NewCurrency64(400e6) 306 missedProofDest := randAddress() 307 fc := types.FileContract{ 308 FileSize: filesize, 309 FileMerkleRoot: crypto.Hash{}, 310 WindowStart: cst.cs.dbBlockHeight() + 1, 311 WindowEnd: cst.cs.dbBlockHeight() + 2, 312 Payout: payout, 313 ValidProofOutputs: []types.SiacoinOutput{{ 314 UnlockHash: types.UnlockHash{}, 315 Value: types.PostTax(cst.cs.dbBlockHeight(), payout), 316 }}, 317 MissedProofOutputs: []types.SiacoinOutput{{ 318 UnlockHash: missedProofDest, 319 Value: types.PostTax(cst.cs.dbBlockHeight(), payout), 320 }}, 321 } 322 323 // Submit a transaction with the file contract. 324 oldSiafundPool := cst.cs.dbGetSiafundPool() 325 txnBuilder := cst.wallet.StartTransaction() 326 err := txnBuilder.FundSiacoins(payout) 327 if err != nil { 328 panic(err) 329 } 330 fcIndex := txnBuilder.AddFileContract(fc) 331 txnSet, err := txnBuilder.Sign(true) 332 if err != nil { 333 panic(err) 334 } 335 err = cst.tpool.AcceptTransactionSet(txnSet) 336 if err != nil { 337 panic(err) 338 } 339 _, err = cst.miner.AddBlock() 340 if err != nil { 341 panic(err) 342 } 343 344 // Check that the siafund pool was increased by the tax on the payout. 345 siafundPool := cst.cs.dbGetSiafundPool() 346 if !siafundPool.Equals(oldSiafundPool.Add(types.Tax(cst.cs.dbBlockHeight()-1, payout))) { 347 panic("siafund pool was not increased correctly") 348 } 349 350 // Check that the file contract made it into the database. 351 ti := len(txnSet) - 1 352 fcid := txnSet[ti].FileContractID(fcIndex) 353 _, err = cst.cs.dbGetFileContract(fcid) 354 if err != nil { 355 panic(err) 356 } 357 358 // Mine a block to close the storage proof window. 359 _, err = cst.miner.AddBlock() 360 if err != nil { 361 panic(err) 362 } 363 364 // Check that the file contract has been removed. 365 _, err = cst.cs.dbGetFileContract(fcid) 366 if err != errNilItem { 367 panic("file contract should not exist in the database") 368 } 369 370 // Check that the siafund pool has not changed. 371 postProofPool := cst.cs.dbGetSiafundPool() 372 if !postProofPool.Equals(siafundPool) { 373 panic("siafund pool should not change after submitting a storage proof") 374 } 375 376 // Check that a delayed output was created for the missed proof. 377 spoid := fcid.StorageProofOutputID(types.ProofMissed, 0) 378 dsco, err := cst.cs.dbGetDSCO(cst.cs.dbBlockHeight()+types.MaturityDelay, spoid) 379 if err != nil { 380 panic(err) 381 } 382 if dsco.UnlockHash != fc.MissedProofOutputs[0].UnlockHash { 383 panic("wrong unlock hash in dsco") 384 } 385 if !dsco.Value.Equals(fc.MissedProofOutputs[0].Value) { 386 panic("wrong sco value in dsco") 387 } 388 } 389 390 // TestIntegrationMissedStorageProofBlocks creates a consensus set tester and 391 // uses it to call testMissedStorageProofBlocks. 392 func TestIntegrationMissedStorageProofBlocks(t *testing.T) { 393 if testing.Short() { 394 t.SkipNow() 395 } 396 t.Parallel() 397 cst, err := createConsensusSetTester(t.Name()) 398 if err != nil { 399 t.Fatal(err) 400 } 401 defer cst.Close() 402 cst.testMissedStorageProofBlocks() 403 } 404 405 // testFileContractRevision creates and revises a file contract on the 406 // blockchain. 407 func (cst *consensusSetTester) testFileContractRevision() { 408 // COMPATv0.4.0 - Step the block height up past the hardfork amount. This 409 // code stops nondeterministic failures when producing storage proofs that 410 // is related to buggy old code. 411 for cst.cs.dbBlockHeight() <= 10 { 412 _, err := cst.miner.AddBlock() 413 if err != nil { 414 panic(err) 415 } 416 } 417 418 // Create a file (as a bytes.Buffer) that will be used for the file 419 // contract. 420 filesize := uint64(4e3) 421 file := fastrand.Bytes(int(filesize)) 422 merkleRoot := crypto.MerkleRoot(file) 423 424 // Create a spendable unlock hash for the file contract. 425 sk, pk := crypto.GenerateKeyPair() 426 uc := types.UnlockConditions{ 427 PublicKeys: []types.SiaPublicKey{{ 428 Algorithm: types.SignatureEd25519, 429 Key: pk[:], 430 }}, 431 SignaturesRequired: 1, 432 } 433 434 // Create a file contract that will be revised. 435 validProofDest := randAddress() 436 payout := types.NewCurrency64(400e6) 437 fc := types.FileContract{ 438 FileSize: filesize, 439 FileMerkleRoot: crypto.Hash{}, 440 WindowStart: cst.cs.dbBlockHeight() + 2, 441 WindowEnd: cst.cs.dbBlockHeight() + 3, 442 Payout: payout, 443 ValidProofOutputs: []types.SiacoinOutput{{ 444 UnlockHash: validProofDest, 445 Value: types.PostTax(cst.cs.dbBlockHeight(), payout), 446 }}, 447 MissedProofOutputs: []types.SiacoinOutput{{ 448 UnlockHash: types.UnlockHash{}, 449 Value: types.PostTax(cst.cs.dbBlockHeight(), payout), 450 }}, 451 UnlockHash: uc.UnlockHash(), 452 } 453 454 // Submit a transaction with the file contract. 455 txnBuilder := cst.wallet.StartTransaction() 456 err := txnBuilder.FundSiacoins(payout) 457 if err != nil { 458 panic(err) 459 } 460 fcIndex := txnBuilder.AddFileContract(fc) 461 txnSet, err := txnBuilder.Sign(true) 462 if err != nil { 463 panic(err) 464 } 465 err = cst.tpool.AcceptTransactionSet(txnSet) 466 if err != nil { 467 panic(err) 468 } 469 _, err = cst.miner.AddBlock() 470 if err != nil { 471 panic(err) 472 } 473 474 // Submit a revision for the file contract. 475 ti := len(txnSet) - 1 476 fcid := txnSet[ti].FileContractID(fcIndex) 477 fcr := types.FileContractRevision{ 478 ParentID: fcid, 479 UnlockConditions: uc, 480 NewRevisionNumber: 69292, 481 482 NewFileSize: filesize, 483 NewFileMerkleRoot: merkleRoot, 484 NewWindowStart: cst.cs.dbBlockHeight() + 1, 485 NewWindowEnd: cst.cs.dbBlockHeight() + 2, 486 NewValidProofOutputs: fc.ValidProofOutputs, 487 NewMissedProofOutputs: fc.MissedProofOutputs, 488 NewUnlockHash: uc.UnlockHash(), 489 } 490 ts := types.TransactionSignature{ 491 ParentID: crypto.Hash(fcid), 492 CoveredFields: types.CoveredFields{WholeTransaction: true}, 493 PublicKeyIndex: 0, 494 } 495 txn := types.Transaction{ 496 FileContractRevisions: []types.FileContractRevision{fcr}, 497 TransactionSignatures: []types.TransactionSignature{ts}, 498 } 499 encodedSig := crypto.SignHash(txn.SigHash(0), sk) 500 txn.TransactionSignatures[0].Signature = encodedSig[:] 501 err = cst.tpool.AcceptTransactionSet([]types.Transaction{txn}) 502 if err != nil { 503 panic(err) 504 } 505 _, err = cst.miner.AddBlock() 506 if err != nil { 507 panic(err) 508 } 509 510 // Create and submit a storage proof for the file contract. 511 segmentIndex, err := cst.cs.StorageProofSegment(fcid) 512 if err != nil { 513 panic(err) 514 } 515 segment, hashSet := crypto.MerkleProof(file, segmentIndex) 516 sp := types.StorageProof{ 517 ParentID: fcid, 518 HashSet: hashSet, 519 } 520 copy(sp.Segment[:], segment) 521 txnBuilder = cst.wallet.StartTransaction() 522 txnBuilder.AddStorageProof(sp) 523 txnSet, err = txnBuilder.Sign(true) 524 if err != nil { 525 panic(err) 526 } 527 err = cst.tpool.AcceptTransactionSet(txnSet) 528 if err != nil { 529 panic(err) 530 } 531 _, err = cst.miner.AddBlock() 532 if err != nil { 533 panic(err) 534 } 535 536 // Check that the file contract has been removed. 537 _, err = cst.cs.dbGetFileContract(fcid) 538 if err != errNilItem { 539 panic("file contract should not exist in the database") 540 } 541 } 542 543 // TestIntegrationFileContractRevision creates a consensus set tester and uses 544 // it to call testFileContractRevision. 545 func TestIntegrationFileContractRevision(t *testing.T) { 546 if testing.Short() { 547 t.SkipNow() 548 } 549 t.Parallel() 550 cst, err := createConsensusSetTester(t.Name()) 551 if err != nil { 552 t.Fatal(err) 553 } 554 defer cst.Close() 555 cst.testFileContractRevision() 556 } 557 558 // testSpendSiafunds spends siafunds on the blockchain. 559 func (cst *consensusSetTester) testSpendSiafunds() { 560 // Create a random destination address for the output in the transaction. 561 destAddr := randAddress() 562 563 // Create a block containing a transaction with a valid siafund output. 564 txnValue := types.NewCurrency64(3) 565 txnBuilder := cst.wallet.StartTransaction() 566 err := txnBuilder.FundSiafunds(txnValue) 567 if err != nil { 568 panic(err) 569 } 570 outputIndex := txnBuilder.AddSiafundOutput(types.SiafundOutput{Value: txnValue, UnlockHash: destAddr}) 571 txnSet, err := txnBuilder.Sign(true) 572 if err != nil { 573 panic(err) 574 } 575 err = cst.tpool.AcceptTransactionSet(txnSet) 576 if err != nil { 577 panic(err) 578 } 579 580 // Find the siafund inputs used in the txn set. 581 var claimValues []types.Currency 582 var claimIDs []types.SiacoinOutputID 583 for _, txn := range txnSet { 584 for _, sfi := range txn.SiafundInputs { 585 sfo, err := cst.cs.dbGetSiafundOutput(sfi.ParentID) 586 if err != nil { 587 // It's not in the database because it's in an earlier 588 // transaction: disregard it - testing the first layer of 589 // dependencies is sufficient. 590 continue 591 } 592 poolDiff := cst.cs.dbGetSiafundPool().Sub(sfo.ClaimStart) 593 value := poolDiff.Div(types.SiafundCount).Mul(sfo.Value) 594 claimValues = append(claimValues, value) 595 claimIDs = append(claimIDs, sfi.ParentID.SiaClaimOutputID()) 596 } 597 } 598 if len(claimValues) == 0 { 599 panic("no siafund outputs created?") 600 } 601 602 // Mine and apply the block to the consensus set. 603 _, err = cst.miner.AddBlock() 604 if err != nil { 605 panic(err) 606 } 607 608 // See that the destination output was created. 609 outputID := txnSet[len(txnSet)-1].SiafundOutputID(outputIndex) 610 sfo, err := cst.cs.dbGetSiafundOutput(outputID) 611 if err != nil { 612 panic(err) 613 } 614 if !sfo.Value.Equals(txnValue) { 615 panic("output added with wrong value") 616 } 617 if sfo.UnlockHash != destAddr { 618 panic("output sent to the wrong address") 619 } 620 if !sfo.ClaimStart.Equals(cst.cs.dbGetSiafundPool()) { 621 panic("ClaimStart is not being set correctly") 622 } 623 624 // Verify that all expected claims were created and added to the set of 625 // delayed siacoin outputs. 626 for i, id := range claimIDs { 627 dsco, err := cst.cs.dbGetDSCO(cst.cs.dbBlockHeight()+types.MaturityDelay, id) 628 if err != nil { 629 panic(err) 630 } 631 if !dsco.Value.Equals(claimValues[i]) { 632 panic("expected a different claim value on the siaclaim") 633 } 634 } 635 } 636 637 // TestIntegrationSpendSiafunds creates a consensus set tester and uses it 638 // to call testSpendSiafunds. 639 func (cst *consensusSetTester) TestIntegrationSpendSiafunds(t *testing.T) { 640 if testing.Short() { 641 t.SkipNow() 642 } 643 t.Parallel() 644 cst, err := createConsensusSetTester(t.Name()) 645 if err != nil { 646 t.Fatal(err) 647 } 648 defer cst.Close() 649 cst.testSpendSiafunds() 650 } 651 652 // testDelayedOutputMaturity adds blocks that result in many delayed outputs 653 // maturing at the same time, verifying that bulk maturity is handled 654 // correctly. 655 656 // TestRegressionDelayedOutputMaturity creates a consensus set tester and uses 657 // it to call testDelayedOutputMaturity. In the past, bolt's ForEach function 658 // had been used incorrectly resulting in the incorrect processing of bulk 659 // delayed outputs. 660 661 // testFileContractMaturity adds blocks that result in many file contracts 662 // being closed at the same time. 663 664 // TestRegressionFileContractMaturity creates a consensus set tester and uses 665 // it to call testFileContractMaturity. In the past, bolt's ForEach function 666 // had been used incorrectly, resulting in the incorrect processing of bulk 667 // file contracts. 668 669 /* 670 // testPaymentChannelBlocks submits blocks to set up, use, and close a payment 671 // channel. 672 func (cst *consensusSetTester) testPaymentChannelBlocks() error { 673 // The current method of doing payment channels is gimped because public 674 // keys do not have timelocks. We will be hardforking to include timelocks 675 // in public keys in 0.4.0, but in the meantime we need an alternate 676 // method. 677 678 // Gimped payment channels: 2-of-2 multisig where one key is controlled by 679 // the funding entity, and one key is controlled by the receiving entity. An 680 // address is created containing both keys, and then the funding entity 681 // creates, but does not sign, a transaction sending coins to the channel 682 // address. A second transaction is created that sends all the coins in the 683 // funding output back to the funding entity. The receiving entity signs the 684 // transaction with a timelocked signature. The funding entity will get the 685 // refund after T blocks as long as the output is not double spent. The 686 // funding entity then signs the first transaction and opens the channel. 687 // 688 // Creating the channel: 689 // 1. Create a 2-of-2 unlock conditions, one key held by each entity. 690 // 2. Funding entity creates, but does not sign, a transaction sending 691 // money to the payment channel address. (txn A) 692 // 3. Funding entity creates and signs a transaction spending the output 693 // created in txn A that sends all the money back as a refund. (txn B) 694 // 4. Receiving entity signs txn B with a timelocked signature, so that the 695 // funding entity cannot get the refund for several days. The funding entity 696 // is given a fully signed and eventually-spendable txn B. 697 // 5. The funding entity signs and broadcasts txn A. 698 // 699 // Using the channel: 700 // Each the receiving entity and the funding entity keeps a record of how 701 // much has been sent down the unclosed channel, and watches the 702 // blockchain for a channel closing transaction. To send more money down 703 // the channel, the funding entity creates and signs a transaction sending 704 // X+y coins to the receiving entity from the channel address. The 705 // transaction is sent to the receiving entity, who will keep it and 706 // potentially sign and broadcast it later. The funding entity will only 707 // send money down the channel if 'work' or some other sort of event has 708 // completed that indicates the receiving entity should get more money. 709 // 710 // Closing the channel: 711 // The receiving entity will sign the transaction that pays them the most 712 // money and then broadcast that transaction. This will spend the output 713 // and close the channel, invalidating txn B and preventing any future 714 // transactions from being made over the channel. The channel must be 715 // closed before the timelock expires on the second signature in txn B, 716 // otherwise the funding entity will be able to get a full refund. 717 // 718 // The funding entity should be waiting until either the receiving entity 719 // closes the channel or the timelock expires. If the receiving entity 720 // closes the channel, all is good. If not, then the funding entity can 721 // close the channel and get a full refund. 722 723 // Create a 2-of-2 unlock conditions, 1 key for each the sender and the 724 // receiver in the payment channel. 725 sk1, pk1, err := crypto.StdKeyGen.Generate() // Funding entity. 726 if err != nil { 727 return err 728 } 729 sk2, pk2, err := crypto.StdKeyGen.Generate() // Receiving entity. 730 if err != nil { 731 return err 732 } 733 uc := types.UnlockConditions{ 734 PublicKeys: []types.SiaPublicKey{ 735 { 736 Algorithm: types.SignatureEd25519, 737 Key: pk1[:], 738 }, 739 { 740 Algorithm: types.SignatureEd25519, 741 Key: pk2[:], 742 }, 743 }, 744 SignaturesRequired: 2, 745 } 746 channelAddress := uc.UnlockHash() 747 748 // Funding entity creates but does not sign a transaction that funds the 749 // channel address. Because the wallet is not very flexible, the channel 750 // txn needs to be fully custom. To get a custom txn, manually create an 751 // address and then use the wallet to fund that address. 752 channelSize := types.NewCurrency64(10e3) 753 channelFundingSK, channelFundingPK, err := crypto.StdKeyGen.Generate() 754 if err != nil { 755 return err 756 } 757 channelFundingUC := types.UnlockConditions{ 758 PublicKeys: []types.SiaPublicKey{{ 759 Algorithm: types.SignatureEd25519, 760 Key: channelFundingPK[:], 761 }}, 762 SignaturesRequired: 1, 763 } 764 channelFundingAddr := channelFundingUC.UnlockHash() 765 fundTxnBuilder := cst.wallet.StartTransaction() 766 if err != nil { 767 return err 768 } 769 err = fundTxnBuilder.FundSiacoins(channelSize) 770 if err != nil { 771 return err 772 } 773 scoFundIndex := fundTxnBuilder.AddSiacoinOutput(types.SiacoinOutput{Value: channelSize, UnlockHash: channelFundingAddr}) 774 fundTxnSet, err := fundTxnBuilder.Sign(true) 775 if err != nil { 776 return err 777 } 778 fundOutputID := fundTxnSet[len(fundTxnSet)-1].SiacoinOutputID(int(scoFundIndex)) 779 channelTxn := types.Transaction{ 780 SiacoinInputs: []types.SiacoinInput{{ 781 ParentID: fundOutputID, 782 UnlockConditions: channelFundingUC, 783 }}, 784 SiacoinOutputs: []types.SiacoinOutput{{ 785 Value: channelSize, 786 UnlockHash: channelAddress, 787 }}, 788 TransactionSignatures: []types.TransactionSignature{{ 789 ParentID: crypto.Hash(fundOutputID), 790 PublicKeyIndex: 0, 791 CoveredFields: types.CoveredFields{WholeTransaction: true}, 792 }}, 793 } 794 795 // Funding entity creates and signs a transaction that spends the full 796 // channel output. 797 channelOutputID := channelTxn.SiacoinOutputID(0) 798 refundUC, err := cst.wallet.NextAddress() 799 refundAddr := refundUC.UnlockHash() 800 if err != nil { 801 return err 802 } 803 refundTxn := types.Transaction{ 804 SiacoinInputs: []types.SiacoinInput{{ 805 ParentID: channelOutputID, 806 UnlockConditions: uc, 807 }}, 808 SiacoinOutputs: []types.SiacoinOutput{{ 809 Value: channelSize, 810 UnlockHash: refundAddr, 811 }}, 812 TransactionSignatures: []types.TransactionSignature{{ 813 ParentID: crypto.Hash(channelOutputID), 814 PublicKeyIndex: 0, 815 CoveredFields: types.CoveredFields{WholeTransaction: true}, 816 }}, 817 } 818 sigHash := refundTxn.SigHash(0) 819 cryptoSig1, err := crypto.SignHash(sigHash, sk1) 820 if err != nil { 821 return err 822 } 823 refundTxn.TransactionSignatures[0].Signature = cryptoSig1[:] 824 825 // Receiving entity signs the transaction that spends the full channel 826 // output, but with a timelock. 827 refundTxn.TransactionSignatures = append(refundTxn.TransactionSignatures, types.TransactionSignature{ 828 ParentID: crypto.Hash(channelOutputID), 829 PublicKeyIndex: 1, 830 Timelock: cst.cs.dbBlockHeight() + 2, 831 CoveredFields: types.CoveredFields{WholeTransaction: true}, 832 }) 833 sigHash = refundTxn.SigHash(1) 834 cryptoSig2, err := crypto.SignHash(sigHash, sk2) 835 if err != nil { 836 return err 837 } 838 refundTxn.TransactionSignatures[1].Signature = cryptoSig2[:] 839 840 // Funding entity will now sign and broadcast the funding transaction. 841 sigHash = channelTxn.SigHash(0) 842 cryptoSig0, err := crypto.SignHash(sigHash, channelFundingSK) 843 if err != nil { 844 return err 845 } 846 channelTxn.TransactionSignatures[0].Signature = cryptoSig0[:] 847 err = cst.tpool.AcceptTransactionSet(append(fundTxnSet, channelTxn)) 848 if err != nil { 849 return err 850 } 851 // Put the txn in a block. 852 _, err = cst.miner.AddBlock() 853 if err != nil { 854 return err 855 } 856 857 // Try to submit the refund transaction before the timelock has expired. 858 err = cst.tpool.AcceptTransactionSet([]types.Transaction{refundTxn}) 859 if err != types.ErrPrematureSignature { 860 return err 861 } 862 863 // Create a transaction that has partially used the channel, and submit it 864 // to the blockchain to close the channel. 865 closeTxn := types.Transaction{ 866 SiacoinInputs: []types.SiacoinInput{{ 867 ParentID: channelOutputID, 868 UnlockConditions: uc, 869 }}, 870 SiacoinOutputs: []types.SiacoinOutput{ 871 { 872 Value: channelSize.Sub(types.NewCurrency64(5)), 873 UnlockHash: refundAddr, 874 }, 875 { 876 Value: types.NewCurrency64(5), 877 }, 878 }, 879 TransactionSignatures: []types.TransactionSignature{ 880 { 881 ParentID: crypto.Hash(channelOutputID), 882 PublicKeyIndex: 0, 883 CoveredFields: types.CoveredFields{WholeTransaction: true}, 884 }, 885 { 886 ParentID: crypto.Hash(channelOutputID), 887 PublicKeyIndex: 1, 888 CoveredFields: types.CoveredFields{WholeTransaction: true}, 889 }, 890 }, 891 } 892 sigHash = closeTxn.SigHash(0) 893 cryptoSig3, err := crypto.SignHash(sigHash, sk1) 894 if err != nil { 895 return err 896 } 897 closeTxn.TransactionSignatures[0].Signature = cryptoSig3[:] 898 sigHash = closeTxn.SigHash(1) 899 cryptoSig4, err := crypto.SignHash(sigHash, sk2) 900 if err != nil { 901 return err 902 } 903 closeTxn.TransactionSignatures[1].Signature = cryptoSig4[:] 904 err = cst.tpool.AcceptTransactionSet([]types.Transaction{closeTxn}) 905 if err != nil { 906 return err 907 } 908 909 // Mine the block with the transaction. 910 _, err = cst.miner.AddBlock() 911 if err != nil { 912 return err 913 } 914 closeRefundID := closeTxn.SiacoinOutputID(0) 915 closePaymentID := closeTxn.SiacoinOutputID(1) 916 exists := cst.cs.db.inSiacoinOutputs(closeRefundID) 917 if !exists { 918 return errors.New("close txn refund output doesn't exist") 919 } 920 exists = cst.cs.db.inSiacoinOutputs(closePaymentID) 921 if !exists { 922 return errors.New("close txn payment output doesn't exist") 923 } 924 925 // Create a payment channel where the receiving entity never responds to 926 // the initial transaction. 927 { 928 // Funding entity creates but does not sign a transaction that funds the 929 // channel address. Because the wallet is not very flexible, the channel 930 // txn needs to be fully custom. To get a custom txn, manually create an 931 // address and then use the wallet to fund that address. 932 channelSize := types.NewCurrency64(10e3) 933 channelFundingSK, channelFundingPK, err := crypto.StdKeyGen.Generate() 934 if err != nil { 935 return err 936 } 937 channelFundingUC := types.UnlockConditions{ 938 PublicKeys: []types.SiaPublicKey{{ 939 Algorithm: types.SignatureEd25519, 940 Key: channelFundingPK[:], 941 }}, 942 SignaturesRequired: 1, 943 } 944 channelFundingAddr := channelFundingUC.UnlockHash() 945 fundTxnBuilder := cst.wallet.StartTransaction() 946 err = fundTxnBuilder.FundSiacoins(channelSize) 947 if err != nil { 948 return err 949 } 950 scoFundIndex := fundTxnBuilder.AddSiacoinOutput(types.SiacoinOutput{Value: channelSize, UnlockHash: channelFundingAddr}) 951 fundTxnSet, err := fundTxnBuilder.Sign(true) 952 if err != nil { 953 return err 954 } 955 fundOutputID := fundTxnSet[len(fundTxnSet)-1].SiacoinOutputID(int(scoFundIndex)) 956 channelTxn := types.Transaction{ 957 SiacoinInputs: []types.SiacoinInput{{ 958 ParentID: fundOutputID, 959 UnlockConditions: channelFundingUC, 960 }}, 961 SiacoinOutputs: []types.SiacoinOutput{{ 962 Value: channelSize, 963 UnlockHash: channelAddress, 964 }}, 965 TransactionSignatures: []types.TransactionSignature{{ 966 ParentID: crypto.Hash(fundOutputID), 967 PublicKeyIndex: 0, 968 CoveredFields: types.CoveredFields{WholeTransaction: true}, 969 }}, 970 } 971 972 // Funding entity creates and signs a transaction that spends the full 973 // channel output. 974 channelOutputID := channelTxn.SiacoinOutputID(0) 975 refundUC, err := cst.wallet.NextAddress() 976 refundAddr := refundUC.UnlockHash() 977 if err != nil { 978 return err 979 } 980 refundTxn := types.Transaction{ 981 SiacoinInputs: []types.SiacoinInput{{ 982 ParentID: channelOutputID, 983 UnlockConditions: uc, 984 }}, 985 SiacoinOutputs: []types.SiacoinOutput{{ 986 Value: channelSize, 987 UnlockHash: refundAddr, 988 }}, 989 TransactionSignatures: []types.TransactionSignature{{ 990 ParentID: crypto.Hash(channelOutputID), 991 PublicKeyIndex: 0, 992 CoveredFields: types.CoveredFields{WholeTransaction: true}, 993 }}, 994 } 995 sigHash := refundTxn.SigHash(0) 996 cryptoSig1, err := crypto.SignHash(sigHash, sk1) 997 if err != nil { 998 return err 999 } 1000 refundTxn.TransactionSignatures[0].Signature = cryptoSig1[:] 1001 1002 // Receiving entity never communitcates, funding entity must reclaim 1003 // the 'channelSize' coins that were intended to go to the channel. 1004 reclaimUC, err := cst.wallet.NextAddress() 1005 reclaimAddr := reclaimUC.UnlockHash() 1006 if err != nil { 1007 return err 1008 } 1009 reclaimTxn := types.Transaction{ 1010 SiacoinInputs: []types.SiacoinInput{{ 1011 ParentID: fundOutputID, 1012 UnlockConditions: channelFundingUC, 1013 }}, 1014 SiacoinOutputs: []types.SiacoinOutput{{ 1015 Value: channelSize, 1016 UnlockHash: reclaimAddr, 1017 }}, 1018 TransactionSignatures: []types.TransactionSignature{{ 1019 ParentID: crypto.Hash(fundOutputID), 1020 PublicKeyIndex: 0, 1021 CoveredFields: types.CoveredFields{WholeTransaction: true}, 1022 }}, 1023 } 1024 sigHash = reclaimTxn.SigHash(0) 1025 cryptoSig, err := crypto.SignHash(sigHash, channelFundingSK) 1026 if err != nil { 1027 return err 1028 } 1029 reclaimTxn.TransactionSignatures[0].Signature = cryptoSig[:] 1030 err = cst.tpool.AcceptTransactionSet(append(fundTxnSet, reclaimTxn)) 1031 if err != nil { 1032 return err 1033 } 1034 block, _ := cst.miner.FindBlock() 1035 err = cst.cs.AcceptBlock(block) 1036 if err != nil { 1037 return err 1038 } 1039 reclaimOutputID := reclaimTxn.SiacoinOutputID(0) 1040 exists := cst.cs.db.inSiacoinOutputs(reclaimOutputID) 1041 if !exists { 1042 return errors.New("failed to reclaim an output that belongs to the funding entity") 1043 } 1044 } 1045 1046 // Create a channel and the open the channel, but close the channel using 1047 // the timelocked signature. 1048 { 1049 // Funding entity creates but does not sign a transaction that funds the 1050 // channel address. Because the wallet is not very flexible, the channel 1051 // txn needs to be fully custom. To get a custom txn, manually create an 1052 // address and then use the wallet to fund that address. 1053 channelSize := types.NewCurrency64(10e3) 1054 channelFundingSK, channelFundingPK, err := crypto.StdKeyGen.Generate() 1055 if err != nil { 1056 return err 1057 } 1058 channelFundingUC := types.UnlockConditions{ 1059 PublicKeys: []types.SiaPublicKey{{ 1060 Algorithm: types.SignatureEd25519, 1061 Key: channelFundingPK[:], 1062 }}, 1063 SignaturesRequired: 1, 1064 } 1065 channelFundingAddr := channelFundingUC.UnlockHash() 1066 fundTxnBuilder := cst.wallet.StartTransaction() 1067 err = fundTxnBuilder.FundSiacoins(channelSize) 1068 if err != nil { 1069 return err 1070 } 1071 scoFundIndex := fundTxnBuilder.AddSiacoinOutput(types.SiacoinOutput{Value: channelSize, UnlockHash: channelFundingAddr}) 1072 fundTxnSet, err := fundTxnBuilder.Sign(true) 1073 if err != nil { 1074 return err 1075 } 1076 fundOutputID := fundTxnSet[len(fundTxnSet)-1].SiacoinOutputID(int(scoFundIndex)) 1077 channelTxn := types.Transaction{ 1078 SiacoinInputs: []types.SiacoinInput{{ 1079 ParentID: fundOutputID, 1080 UnlockConditions: channelFundingUC, 1081 }}, 1082 SiacoinOutputs: []types.SiacoinOutput{{ 1083 Value: channelSize, 1084 UnlockHash: channelAddress, 1085 }}, 1086 TransactionSignatures: []types.TransactionSignature{{ 1087 ParentID: crypto.Hash(fundOutputID), 1088 PublicKeyIndex: 0, 1089 CoveredFields: types.CoveredFields{WholeTransaction: true}, 1090 }}, 1091 } 1092 1093 // Funding entity creates and signs a transaction that spends the full 1094 // channel output. 1095 channelOutputID := channelTxn.SiacoinOutputID(0) 1096 refundUC, err := cst.wallet.NextAddress() 1097 refundAddr := refundUC.UnlockHash() 1098 if err != nil { 1099 return err 1100 } 1101 refundTxn := types.Transaction{ 1102 SiacoinInputs: []types.SiacoinInput{{ 1103 ParentID: channelOutputID, 1104 UnlockConditions: uc, 1105 }}, 1106 SiacoinOutputs: []types.SiacoinOutput{{ 1107 Value: channelSize, 1108 UnlockHash: refundAddr, 1109 }}, 1110 TransactionSignatures: []types.TransactionSignature{{ 1111 ParentID: crypto.Hash(channelOutputID), 1112 PublicKeyIndex: 0, 1113 CoveredFields: types.CoveredFields{WholeTransaction: true}, 1114 }}, 1115 } 1116 sigHash := refundTxn.SigHash(0) 1117 cryptoSig1, err := crypto.SignHash(sigHash, sk1) 1118 if err != nil { 1119 return err 1120 } 1121 refundTxn.TransactionSignatures[0].Signature = cryptoSig1[:] 1122 1123 // Receiving entity signs the transaction that spends the full channel 1124 // output, but with a timelock. 1125 refundTxn.TransactionSignatures = append(refundTxn.TransactionSignatures, types.TransactionSignature{ 1126 ParentID: crypto.Hash(channelOutputID), 1127 PublicKeyIndex: 1, 1128 Timelock: cst.cs.dbBlockHeight() + 2, 1129 CoveredFields: types.CoveredFields{WholeTransaction: true}, 1130 }) 1131 sigHash = refundTxn.SigHash(1) 1132 cryptoSig2, err := crypto.SignHash(sigHash, sk2) 1133 if err != nil { 1134 return err 1135 } 1136 refundTxn.TransactionSignatures[1].Signature = cryptoSig2[:] 1137 1138 // Funding entity will now sign and broadcast the funding transaction. 1139 sigHash = channelTxn.SigHash(0) 1140 cryptoSig0, err := crypto.SignHash(sigHash, channelFundingSK) 1141 if err != nil { 1142 return err 1143 } 1144 channelTxn.TransactionSignatures[0].Signature = cryptoSig0[:] 1145 err = cst.tpool.AcceptTransactionSet(append(fundTxnSet, channelTxn)) 1146 if err != nil { 1147 return err 1148 } 1149 // Put the txn in a block. 1150 block, _ := cst.miner.FindBlock() 1151 err = cst.cs.AcceptBlock(block) 1152 if err != nil { 1153 return err 1154 } 1155 1156 // Receiving entity never signs another transaction, so the funding 1157 // entity waits until the timelock is complete, and then submits the 1158 // refundTxn. 1159 for i := 0; i < 3; i++ { 1160 block, _ := cst.miner.FindBlock() 1161 err = cst.cs.AcceptBlock(block) 1162 if err != nil { 1163 return err 1164 } 1165 } 1166 err = cst.tpool.AcceptTransactionSet([]types.Transaction{refundTxn}) 1167 if err != nil { 1168 return err 1169 } 1170 block, _ = cst.miner.FindBlock() 1171 err = cst.cs.AcceptBlock(block) 1172 if err != nil { 1173 return err 1174 } 1175 refundOutputID := refundTxn.SiacoinOutputID(0) 1176 exists := cst.cs.db.inSiacoinOutputs(refundOutputID) 1177 if !exists { 1178 return errors.New("timelocked refund transaction did not get spent correctly") 1179 } 1180 } 1181 1182 return nil 1183 } 1184 */ 1185 1186 /* 1187 // TestPaymentChannelBlocks creates a consensus set tester and uses it to call 1188 // testPaymentChannelBlocks. 1189 func TestPaymentChannelBlocks(t *testing.T) { 1190 if testing.Short() { 1191 t.SkipNow() 1192 } 1193 cst, err := createConsensusSetTester(t.Name()) 1194 if err != nil { 1195 t.Fatal(err) 1196 } 1197 defer cst.closeCst() 1198 err = cst.testPaymentChannelBlocks() 1199 if err != nil { 1200 t.Fatal(err) 1201 } 1202 } 1203 */