// HARDFORK 21,000 // // TestPreForkValidStorageProofs checks that storage proofs which are invalid // before the hardfork (but valid afterwards) are still rejected before the // hardfork). func TestPreForkValidStorageProofs(t *testing.T) { if testing.Short() { t.SkipNow() } t.Parallel() cst, err := createConsensusSetTester("TestPreForkValidStorageProofs") if err != nil { t.Fatal(err) } defer cst.Close() // Try a proof set where there is padding on the last segment in the file. file := make([]byte, 100) _, err = rand.Read(file) if err != nil { t.Fatal(err) } root := crypto.MerkleRoot(file) fc := types.FileContract{ FileSize: 100, FileMerkleRoot: root, Payout: types.NewCurrency64(1), WindowStart: 2, WindowEnd: 1200, } // Find a proofIndex that has the value '1'. var fcid types.FileContractID var proofIndex uint64 for { fcid[0]++ cst.cs.dbAddFileContract(fcid, fc) proofIndex, err = cst.cs.dbStorageProofSegment(fcid) if err != nil { t.Fatal(err) } if proofIndex == 1 { break } } base, proofSet := crypto.MerkleProof(file, proofIndex) txn := types.Transaction{ StorageProofs: []types.StorageProof{{ ParentID: fcid, HashSet: proofSet, }}, } copy(txn.StorageProofs[0].Segment[:], base) err = cst.cs.dbValidStorageProofs(txn) if err != errInvalidStorageProof { t.Log(cst.cs.dbBlockHeight()) t.Fatal(err) } }
// TestStorageProofBoundaries creates file contracts and submits storage proofs // for them, probing segment boundaries (first segment, last segment, // incomplete segment, etc.). func TestStorageProofBoundaries(t *testing.T) { if testing.Short() { t.SkipNow() } t.Parallel() cst, err := createConsensusSetTester("TestStorageProofBoundaries") if err != nil { t.Fatal(err) } defer cst.Close() // Mine enough blocks to put us beyond the testing hardfork. for i := 0; i < 10; i++ { _, err = cst.miner.AddBlock() if err != nil { t.Fatal(err) } } // Try storage proofs on data between 0 bytes and 128 bytes (0 segments and // 1 segment). Perform the operation five times because we can't control // which segment gets selected - it is randomly decided by the block. segmentRange := []int{0, 1, 2, 3, 4, 5, 15, 25, 30, 32, 62, 63, 64, 65, 66, 70, 81, 89, 90, 126, 127, 128, 129} for i := 0; i < 3; i++ { randData, err := crypto.RandBytes(140) if err != nil { t.Fatal(err) } // Create a file contract for all sizes of the data between 0 and 2 // segments and put them in the transaction pool. var fcids []types.FileContractID for _, k := range segmentRange { // Create the data and the file contract around it. truncatedData := randData[:k] fc := types.FileContract{ FileSize: uint64(k), FileMerkleRoot: crypto.MerkleRoot(truncatedData), WindowStart: cst.cs.dbBlockHeight() + 2, WindowEnd: cst.cs.dbBlockHeight() + 4, Payout: types.NewCurrency64(500), // Too small to be subject to siafund fee. ValidProofOutputs: []types.SiacoinOutput{{Value: types.NewCurrency64(500)}}, MissedProofOutputs: []types.SiacoinOutput{{Value: types.NewCurrency64(500)}}, } // Create a transaction around the file contract and add it to the // transaction pool. b := cst.wallet.StartTransaction() err = b.FundSiacoins(types.NewCurrency64(500)) if err != nil { t.Fatal(err) } b.AddFileContract(fc) txnSet, err := b.Sign(true) if err != nil { t.Fatal(err) } err = cst.tpool.AcceptTransactionSet(txnSet) if err != nil { t.Fatal(err) } // Store the file contract id for later when building the storage // proof. fcids = append(fcids, txnSet[len(txnSet)-1].FileContractID(0)) } // Mine blocks to get the file contracts into the blockchain and // confirming. for j := 0; j < 2; j++ { _, err = cst.miner.AddBlock() if err != nil { t.Fatal(err) } } // Create storage proofs for the file contracts and submit the proofs // to the blockchain. for j, k := range segmentRange { // Build the storage proof. truncatedData := randData[:k] proofIndex, err := cst.cs.StorageProofSegment(fcids[j]) if err != nil { t.Fatal(err) } base, hashSet := crypto.MerkleProof(truncatedData, proofIndex) sp := types.StorageProof{ ParentID: fcids[j], HashSet: hashSet, } copy(sp.Segment[:], base) if k > 0 { // Try submitting an empty storage proof, to make sure that the // hardfork code didn't accidentally allow empty storage proofs // in situations other than file sizes with 0 bytes. badSP := types.StorageProof{ParentID: fcids[j]} badTxn := types.Transaction{ StorageProofs: []types.StorageProof{badSP}, } if sp.Segment == badSP.Segment { continue } err = cst.tpool.AcceptTransactionSet([]types.Transaction{badTxn}) if err == nil { t.Fatal("An empty storage proof got into the transaction pool with non-empty data") } } // Submit the storage proof to the blockchain in a transaction. txn := types.Transaction{ StorageProofs: []types.StorageProof{sp}, } err = cst.tpool.AcceptTransactionSet([]types.Transaction{txn}) if err != nil { t.Fatal(err, "-", j, k) } } // Mine blocks to get the storage proofs on the blockchain. for j := 0; j < 2; j++ { _, err = cst.miner.AddBlock() if err != nil { t.Fatal(err) } } } }
// TestValidStorageProofs probes the validStorageProofs method of the consensus // set. func TestValidStorageProofs(t *testing.T) { if testing.Short() { t.SkipNow() } t.Parallel() cst, err := createConsensusSetTester("TestValidStorageProofs") if err != nil { t.Fatal(err) } defer cst.Close() // COMPATv0.4.0 // // Mine 10 blocks so that the post-hardfork rules are in effect. for i := 0; i < 10; i++ { block, _ := cst.miner.FindBlock() err = cst.cs.AcceptBlock(block) if err != nil { t.Fatal(err) } } // Create a file contract for which a storage proof can be created. var fcid types.FileContractID fcid[0] = 12 simFile := make([]byte, 64*1024) _, err = rand.Read(simFile) if err != nil { t.Fatal(err) } root := crypto.MerkleRoot(simFile) fc := types.FileContract{ FileSize: 64 * 1024, FileMerkleRoot: root, Payout: types.NewCurrency64(1), WindowStart: 2, WindowEnd: 1200, } cst.cs.dbAddFileContract(fcid, fc) // Create a transaction with a storage proof. proofIndex, err := cst.cs.dbStorageProofSegment(fcid) if err != nil { t.Fatal(err) } base, proofSet := crypto.MerkleProof(simFile, proofIndex) txn := types.Transaction{ StorageProofs: []types.StorageProof{{ ParentID: fcid, HashSet: proofSet, }}, } copy(txn.StorageProofs[0].Segment[:], base) err = cst.cs.dbValidStorageProofs(txn) if err != nil { t.Error(err) } // Corrupt the proof set. proofSet[0][0]++ txn = types.Transaction{ StorageProofs: []types.StorageProof{{ ParentID: fcid, HashSet: proofSet, }}, } copy(txn.StorageProofs[0].Segment[:], base) err = cst.cs.dbValidStorageProofs(txn) if err != errInvalidStorageProof { t.Error(err) } // Try to validate a proof for a file contract that doesn't exist. txn.StorageProofs[0].ParentID = types.FileContractID{} err = cst.cs.dbValidStorageProofs(txn) if err != errUnrecognizedFileContractID { t.Error(err) } // Try a proof set where there is padding on the last segment in the file. file := make([]byte, 100) _, err = rand.Read(file) if err != nil { t.Fatal(err) } root = crypto.MerkleRoot(file) fc = types.FileContract{ FileSize: 100, FileMerkleRoot: root, Payout: types.NewCurrency64(1), WindowStart: 2, WindowEnd: 1200, } // Find a proofIndex that has the value '1'. for { fcid[0]++ cst.cs.dbAddFileContract(fcid, fc) proofIndex, err = cst.cs.dbStorageProofSegment(fcid) if err != nil { t.Fatal(err) } if proofIndex == 1 { break } } base, proofSet = crypto.MerkleProof(file, proofIndex) txn = types.Transaction{ StorageProofs: []types.StorageProof{{ ParentID: fcid, HashSet: proofSet, }}, } copy(txn.StorageProofs[0].Segment[:], base) err = cst.cs.dbValidStorageProofs(txn) if err != nil { t.Fatal(err) } }
// threadedHandleActionItem will look at a storage obligation and determine // which action is necessary for the storage obligation to succeed. func (h *Host) threadedHandleActionItem(soid types.FileContractID, wg *sync.WaitGroup) { // The calling thread is responsible for calling Add to the thread group. defer wg.Done() // Lock the storage obligation in question. h.managedLockStorageObligation(soid) defer func() { h.managedUnlockStorageObligation(soid) }() // Convert the storage obligation id into a storage obligation. var err error var so storageObligation h.mu.RLock() blockHeight := h.blockHeight err = h.db.View(func(tx *bolt.Tx) error { so, err = getStorageObligation(tx, soid) return err }) h.mu.RUnlock() if err != nil { h.log.Println("Could not get storage obligation:", err) return } // Check whether the storage obligation has already been completed. if so.ObligationStatus != obligationUnresolved { // Storage obligation has already been completed, skip action item. return } // Check whether the file contract has been seen. If not, resubmit and // queue another action item. Check for death. (signature should have a // kill height) if !so.OriginConfirmed { // Submit the transaction set again, try to get the transaction // confirmed. err := h.tpool.AcceptTransactionSet(so.OriginTransactionSet) if err != nil { h.log.Debugln("Could not get origin transaction set accepted", err) // Check if the transaction is invalid with the current consensus set. // If so, the transaction is highly unlikely to ever be confirmed, and // the storage obligation should be removed. This check should come // after logging the errror so that the function can quit. // // TODO: If the host or tpool is behind consensus, might be difficult // to have certainty about the issue. If some but not all of the // parents are confirmed, might be some difficulty. _, t := err.(modules.ConsensusConflict) if t { h.log.Println("Consensus conflict on the origin transaction set, id", so.id()) h.mu.Lock() err = h.removeStorageObligation(so, obligationRejected) h.mu.Unlock() if err != nil { h.log.Println("Error removing storage obligation:", err) } return } } // Queue another action item to check the status of the transaction. h.mu.Lock() err = h.queueActionItem(h.blockHeight+resubmissionTimeout, so.id()) h.mu.Unlock() if err != nil { h.log.Println("Error queuing action item:", err) } } // Check if the file contract revision is ready for submission. Check for death. if !so.RevisionConfirmed && len(so.RevisionTransactionSet) > 0 && blockHeight > so.expiration()-revisionSubmissionBuffer { // Sanity check - there should be a file contract revision. rtsLen := len(so.RevisionTransactionSet) if rtsLen < 1 || len(so.RevisionTransactionSet[rtsLen-1].FileContractRevisions) != 1 { h.log.Critical("transaction revision marked as unconfirmed, yet there is no transaction revision") return } // Check if the revision has failed to submit correctly. if blockHeight > so.expiration() { // TODO: Check this error. // // TODO: this is not quite right, because a previous revision may // be confirmed, and the origin transaction may be confirmed, which // would confuse the revenue stuff a bit. Might happen frequently // due to the dynamic fee pool. h.log.Println("Full time has elapsed, but the revision transaction could not be submitted to consensus, id", so.id()) h.mu.Lock() h.removeStorageObligation(so, obligationRejected) h.mu.Unlock() return } // Queue another action item to check the status of the transaction. h.mu.Lock() err := h.queueActionItem(blockHeight+resubmissionTimeout, so.id()) h.mu.Unlock() if err != nil { h.log.Println("Error queuing action item:", err) } // Add a miner fee to the transaction and submit it to the blockchain. revisionTxnIndex := len(so.RevisionTransactionSet) - 1 revisionParents := so.RevisionTransactionSet[:revisionTxnIndex] revisionTxn := so.RevisionTransactionSet[revisionTxnIndex] builder := h.wallet.RegisterTransaction(revisionTxn, revisionParents) _, feeRecommendation := h.tpool.FeeEstimation() if so.value().Div64(2).Cmp(feeRecommendation) < 0 { // There's no sense submitting the revision if the fee is more than // half of the anticipated revenue - fee market went up // unexpectedly, and the money that the renter paid to cover the // fees is no longer enough. return } txnSize := uint64(len(encoding.MarshalAll(so.RevisionTransactionSet)) + 300) requiredFee := feeRecommendation.Mul64(txnSize) err = builder.FundSiacoins(requiredFee) if err != nil { h.log.Println("Error funding transaction fees", err) } builder.AddMinerFee(requiredFee) if err != nil { h.log.Println("Error adding miner fees", err) } feeAddedRevisionTransactionSet, err := builder.Sign(true) if err != nil { h.log.Println("Error signing transaction", err) } err = h.tpool.AcceptTransactionSet(feeAddedRevisionTransactionSet) if err != nil { h.log.Println("Error submitting transaction to transaction pool", err) } so.TransactionFeesAdded = so.TransactionFeesAdded.Add(requiredFee) // return } // Check whether a storage proof is ready to be provided, and whether it // has been accepted. Check for death. if !so.ProofConfirmed && blockHeight >= so.expiration()+resubmissionTimeout { h.log.Debugln("Host is attempting a storage proof for", so.id()) // If the window has closed, the host has failed and the obligation can // be removed. if so.proofDeadline() < blockHeight || len(so.SectorRoots) == 0 { h.log.Debugln("storage proof not confirmed by deadline, id", so.id()) h.mu.Lock() err := h.removeStorageObligation(so, obligationFailed) h.mu.Unlock() if err != nil { h.log.Println("Error removing storage obligation:", err) } return } // Get the index of the segment, and the index of the sector containing // the segment. segmentIndex, err := h.cs.StorageProofSegment(so.id()) if err != nil { h.log.Debugln("Host got an error when fetching a storage proof segment:", err) return } sectorIndex := segmentIndex / (modules.SectorSize / crypto.SegmentSize) // Pull the corresponding sector into memory. sectorRoot := so.SectorRoots[sectorIndex] sectorBytes, err := h.ReadSector(sectorRoot) if err != nil { h.log.Debugln(err) return } // Build the storage proof for just the sector. sectorSegment := segmentIndex % (modules.SectorSize / crypto.SegmentSize) base, cachedHashSet := crypto.MerkleProof(sectorBytes, sectorSegment) // Using the sector, build a cached root. log2SectorSize := uint64(0) for 1<<log2SectorSize < (modules.SectorSize / crypto.SegmentSize) { log2SectorSize++ } ct := crypto.NewCachedTree(log2SectorSize) ct.SetIndex(segmentIndex) for _, root := range so.SectorRoots { ct.Push(root) } hashSet := ct.Prove(base, cachedHashSet) sp := types.StorageProof{ ParentID: so.id(), HashSet: hashSet, } copy(sp.Segment[:], base) // Create and build the transaction with the storage proof. builder := h.wallet.StartTransaction() _, feeRecommendation := h.tpool.FeeEstimation() if so.value().Cmp(feeRecommendation) < 0 { // There's no sense submitting the storage proof if the fee is more // than the anticipated revenue. h.log.Debugln("Host not submitting storage proof due to a value that does not sufficiently exceed the fee cost") return } txnSize := uint64(len(encoding.Marshal(sp)) + 300) requiredFee := feeRecommendation.Mul64(txnSize) err = builder.FundSiacoins(requiredFee) if err != nil { h.log.Println("Host error when funding a storage proof transaction fee:", err) return } builder.AddMinerFee(requiredFee) builder.AddStorageProof(sp) storageProofSet, err := builder.Sign(true) if err != nil { h.log.Println("Host error when signing the storage proof transaction:", err) return } err = h.tpool.AcceptTransactionSet(storageProofSet) if err != nil { h.log.Println("Host unable to submit storage proof transaction to transaction pool:", err) return } so.TransactionFeesAdded = so.TransactionFeesAdded.Add(requiredFee) // Queue another action item to check whether there the storage proof // got confirmed. h.mu.Lock() err = h.queueActionItem(so.proofDeadline(), so.id()) h.mu.Unlock() if err != nil { h.log.Println("Error queuing action item:", err) } } // Save the storage obligation to account for any fee changes. err = h.db.Update(func(tx *bolt.Tx) error { soBytes, err := json.Marshal(so) if err != nil { return err } return tx.Bucket(bucketStorageObligations).Put(soid[:], soBytes) }) if err != nil { h.log.Println("Error updating the storage obligations", err) } // Check if all items have succeeded with the required confirmations. Report // success, delete the obligation. if so.ProofConfirmed && blockHeight >= so.proofDeadline() { h.log.Println("file contract complete, id", so.id()) h.mu.Lock() h.removeStorageObligation(so, obligationSucceeded) h.mu.Unlock() } }
// testFileContractRevision creates and revises a file contract on the // blockchain. func (cst *consensusSetTester) testFileContractRevision() { // COMPATv0.4.0 - Step the block height up past the hardfork amount. This // code stops nondeterministic failures when producing storage proofs that // is related to buggy old code. for cst.cs.dbBlockHeight() <= 10 { _, err := cst.miner.AddBlock() if err != nil { panic(err) } } // Create a file (as a bytes.Buffer) that will be used for the file // contract. filesize := uint64(4e3) file, err := crypto.RandBytes(int(filesize)) if err != nil { panic(err) } merkleRoot := crypto.MerkleRoot(file) // Create a spendable unlock hash for the file contract. sk, pk, err := crypto.GenerateKeyPair() if err != nil { panic(err) } uc := types.UnlockConditions{ PublicKeys: []types.SiaPublicKey{{ Algorithm: types.SignatureEd25519, Key: pk[:], }}, SignaturesRequired: 1, } // Create a file contract that will be revised. validProofDest := randAddress() payout := types.NewCurrency64(400e6) fc := types.FileContract{ FileSize: filesize, FileMerkleRoot: crypto.Hash{}, WindowStart: cst.cs.dbBlockHeight() + 2, WindowEnd: cst.cs.dbBlockHeight() + 3, Payout: payout, ValidProofOutputs: []types.SiacoinOutput{{ UnlockHash: validProofDest, Value: types.PostTax(cst.cs.dbBlockHeight(), payout), }}, MissedProofOutputs: []types.SiacoinOutput{{ UnlockHash: types.UnlockHash{}, Value: types.PostTax(cst.cs.dbBlockHeight(), payout), }}, UnlockHash: uc.UnlockHash(), } // Submit a transaction with the file contract. txnBuilder := cst.wallet.StartTransaction() err = txnBuilder.FundSiacoins(payout) if err != nil { panic(err) } fcIndex := txnBuilder.AddFileContract(fc) txnSet, err := txnBuilder.Sign(true) if err != nil { panic(err) } err = cst.tpool.AcceptTransactionSet(txnSet) if err != nil { panic(err) } _, err = cst.miner.AddBlock() if err != nil { panic(err) } // Submit a revision for the file contract. ti := len(txnSet) - 1 fcid := txnSet[ti].FileContractID(fcIndex) fcr := types.FileContractRevision{ ParentID: fcid, UnlockConditions: uc, NewRevisionNumber: 69292, NewFileSize: filesize, NewFileMerkleRoot: merkleRoot, NewWindowStart: cst.cs.dbBlockHeight() + 1, NewWindowEnd: cst.cs.dbBlockHeight() + 2, NewValidProofOutputs: fc.ValidProofOutputs, NewMissedProofOutputs: fc.MissedProofOutputs, NewUnlockHash: uc.UnlockHash(), } ts := types.TransactionSignature{ ParentID: crypto.Hash(fcid), CoveredFields: types.CoveredFields{WholeTransaction: true}, PublicKeyIndex: 0, } txn := types.Transaction{ FileContractRevisions: []types.FileContractRevision{fcr}, TransactionSignatures: []types.TransactionSignature{ts}, } encodedSig, err := crypto.SignHash(txn.SigHash(0), sk) if err != nil { panic(err) } txn.TransactionSignatures[0].Signature = encodedSig[:] err = cst.tpool.AcceptTransactionSet([]types.Transaction{txn}) if err != nil { panic(err) } _, err = cst.miner.AddBlock() if err != nil { panic(err) } // Create and submit a storage proof for the file contract. segmentIndex, err := cst.cs.StorageProofSegment(fcid) if err != nil { panic(err) } segment, hashSet := crypto.MerkleProof(file, segmentIndex) sp := types.StorageProof{ ParentID: fcid, HashSet: hashSet, } copy(sp.Segment[:], segment) txnBuilder = cst.wallet.StartTransaction() txnBuilder.AddStorageProof(sp) txnSet, err = txnBuilder.Sign(true) if err != nil { panic(err) } err = cst.tpool.AcceptTransactionSet(txnSet) if err != nil { panic(err) } _, err = cst.miner.AddBlock() if err != nil { panic(err) } // Check that the file contract has been removed. _, err = cst.cs.dbGetFileContract(fcid) if err != errNilItem { panic("file contract should not exist in the database") } }
// testValidStorageProofBlocks adds a block with a file contract, and then // submits a storage proof for that file contract. func (cst *consensusSetTester) testValidStorageProofBlocks() { // COMPATv0.4.0 - Step the block height up past the hardfork amount. This // code stops nondeterministic failures when producing storage proofs that // is related to buggy old code. for cst.cs.dbBlockHeight() <= 10 { _, err := cst.miner.AddBlock() if err != nil { panic(err) } } // Create a file (as a bytes.Buffer) that will be used for the file // contract. filesize := uint64(4e3) file, err := crypto.RandBytes(int(filesize)) if err != nil { panic(err) } merkleRoot := crypto.MerkleRoot(file) // Create a file contract that will be successful. validProofDest := randAddress() payout := types.NewCurrency64(400e6) fc := types.FileContract{ FileSize: filesize, FileMerkleRoot: merkleRoot, WindowStart: cst.cs.dbBlockHeight() + 1, WindowEnd: cst.cs.dbBlockHeight() + 2, Payout: payout, ValidProofOutputs: []types.SiacoinOutput{{ UnlockHash: validProofDest, Value: types.PostTax(cst.cs.dbBlockHeight(), payout), }}, MissedProofOutputs: []types.SiacoinOutput{{ UnlockHash: types.UnlockHash{}, Value: types.PostTax(cst.cs.dbBlockHeight(), payout), }}, } // Submit a transaction with the file contract. oldSiafundPool := cst.cs.dbGetSiafundPool() txnBuilder := cst.wallet.StartTransaction() err = txnBuilder.FundSiacoins(payout) if err != nil { panic(err) } fcIndex := txnBuilder.AddFileContract(fc) txnSet, err := txnBuilder.Sign(true) if err != nil { panic(err) } err = cst.tpool.AcceptTransactionSet(txnSet) if err != nil { panic(err) } _, err = cst.miner.AddBlock() if err != nil { panic(err) } // Check that the siafund pool was increased by the tax on the payout. siafundPool := cst.cs.dbGetSiafundPool() if siafundPool.Cmp(oldSiafundPool.Add(types.Tax(cst.cs.dbBlockHeight()-1, payout))) != 0 { panic("siafund pool was not increased correctly") } // Check that the file contract made it into the database. ti := len(txnSet) - 1 fcid := txnSet[ti].FileContractID(fcIndex) _, err = cst.cs.dbGetFileContract(fcid) if err != nil { panic(err) } // Create and submit a storage proof for the file contract. segmentIndex, err := cst.cs.StorageProofSegment(fcid) if err != nil { panic(err) } segment, hashSet := crypto.MerkleProof(file, segmentIndex) sp := types.StorageProof{ ParentID: fcid, HashSet: hashSet, } copy(sp.Segment[:], segment) txnBuilder = cst.wallet.StartTransaction() txnBuilder.AddStorageProof(sp) txnSet, err = txnBuilder.Sign(true) if err != nil { panic(err) } err = cst.tpool.AcceptTransactionSet(txnSet) if err != nil { panic(err) } _, err = cst.miner.AddBlock() if err != nil { panic(err) } // Check that the file contract has been removed. _, err = cst.cs.dbGetFileContract(fcid) if err != errNilItem { panic("file contract should not exist in the database") } // Check that the siafund pool has not changed. postProofPool := cst.cs.dbGetSiafundPool() if postProofPool.Cmp(siafundPool) != 0 { panic("siafund pool should not change after submitting a storage proof") } // Check that a delayed output was created for the valid proof. spoid := fcid.StorageProofOutputID(types.ProofValid, 0) dsco, err := cst.cs.dbGetDSCO(cst.cs.dbBlockHeight()+types.MaturityDelay, spoid) if err != nil { panic(err) } if dsco.UnlockHash != fc.ValidProofOutputs[0].UnlockHash { panic("wrong unlock hash in dsco") } if dsco.Value.Cmp(fc.ValidProofOutputs[0].Value) != 0 { panic("wrong sco value in dsco") } }