12 "github.com/vapor/account"
13 "github.com/vapor/blockchain/pseudohsm"
14 "github.com/vapor/blockchain/signers"
15 "github.com/vapor/blockchain/txbuilder"
16 "github.com/vapor/common"
17 "github.com/vapor/consensus"
18 "github.com/vapor/crypto/ed25519/chainkd"
19 dbm "github.com/vapor/database/leveldb"
20 "github.com/vapor/errors"
21 "github.com/vapor/protocol/bc"
22 "github.com/vapor/protocol/bc/types"
25 // MockBlock mock a block
26 func MockBlock(txs []*types.Tx) *types.Block {
28 BlockHeader: types.BlockHeader{Timestamp: uint64(time.Now().Nanosecond())},
33 func MockSimpleUtxo(index uint64, assetID *bc.AssetID, amount uint64, ctrlProg *account.CtrlProgram) *account.UTXO {
35 ctrlProg = &account.CtrlProgram{
39 ControlProgram: []byte{81},
44 utxo := &account.UTXO{
45 OutputID: bc.Hash{V0: 1},
46 SourceID: bc.Hash{V0: 1},
50 ControlProgram: ctrlProg.ControlProgram,
51 ControlProgramIndex: ctrlProg.KeyIndex,
52 AccountID: ctrlProg.AccountID,
53 Address: ctrlProg.Address,
60 func AddInput(sourceID bc.Hash, assetID bc.AssetID, amount uint64, pos uint64, controlProgram []byte) *types.TxInput {
61 return types.NewSpendInput(nil, sourceID, assetID, amount, pos, controlProgram)
64 func AddTxOutput(assetID bc.AssetID, amount uint64, controlProgram []byte) *types.TxOutput {
65 return types.NewIntraChainOutput(assetID, amount, controlProgram)
68 func BuildTx(baseUtxo *account.UTXO, signer *signers.Signer) (*txbuilder.Template, error) {
69 tplBuilder, err := CreateTxBuilder(baseUtxo, signer)
74 tpl, _, err := tplBuilder.Build()
82 func CreateTxBuilder(baseUtxo *account.UTXO, signer *signers.Signer) (*txbuilder.TemplateBuilder, error) {
83 tplBuilder := txbuilder.NewBuilder(time.Now())
84 txInput := AddInput(bc.Hash{V0: 1}, baseUtxo.AssetID, 10000, uint64(1), baseUtxo.ControlProgram)
85 txOutput := AddTxOutput(baseUtxo.AssetID, 100, baseUtxo.ControlProgram)
86 tplBuilder.AddInput(txInput, &txbuilder.SigningInstruction{Position: 0})
87 tplBuilder.AddOutput(txOutput)
88 return tplBuilder, nil
91 func MockTxsP2PKH(acctMgr *account.Manager, xPub chainkd.XPub, multiTypeAccount bool) ([]*types.Tx, error) {
93 accts := []*account.Account{}
94 for i := uint32(1); i < 32; i = i + 1 + rand.Uint32()%5 {
95 alias := fmt.Sprintf("testAccount%d", i)
96 deriveRule := signers.BIP0044
98 deriveRule = uint8(rand.Uint32() % 2)
100 acct, err := account.CreateAccount([]chainkd.XPub{xPub}, 1, alias, uint64(i), deriveRule)
105 if err := acctMgr.SaveAccount(acct); err != nil {
109 accts = append(accts, acct)
112 for _, acct := range accts {
113 for i := uint32(1); i < 256; i = i + 1 + rand.Uint32()%16 {
114 controlProg, err := account.CreateCtrlProgram(acct, uint64(i), false)
119 if err := acctMgr.SaveControlPrograms(controlProg); err != nil {
123 utxo := MockSimpleUtxo(0, consensus.BTMAssetID, 1000000000, controlProg)
124 tpl, err := BuildTx(utxo, acct.Signer)
129 txs = append(txs, tpl.Transaction)
136 func TestXPubsRecoveryLock(t *testing.T) {
137 dirPath, err := ioutil.TempDir(".", "")
141 defer os.RemoveAll(dirPath)
143 testDB := dbm.NewDB("testdb", "leveldb", dirPath)
144 hsm, err := pseudohsm.New(dirPath)
149 xpub, _, err := hsm.XCreate("test_pub", "password", "en")
154 acctMgr := account.NewManager(testDB, nil)
155 recoveryMgr := newRecoveryManager(testDB, acctMgr)
156 recoveryMgr.state = newRecoveryState()
157 recoveryMgr.state.XPubs = []chainkd.XPub{xpub.XPub}
158 recoveryMgr.state.XPubsStatus = newBranchRecoveryState(acctRecoveryWindow)
160 recoveryMgr.state.StartTime = time.Now()
161 recoveryMgr.commitStatusInfo()
163 if err := recoveryMgr.AcctResurrect([]chainkd.XPub{xpub.XPub}); err != nil {
164 t.Fatal("TestXPubsRecoveryLock err:", err)
167 if err := recoveryMgr.AcctResurrect([]chainkd.XPub{xpub.XPub}); err != errors.Root(ErrRecoveryBusy) {
168 t.Fatal("TestXPubsRecoveryLock err:", err)
171 if err := recoveryMgr.LoadStatusInfo(); err != errors.Root(ErrRecoveryBusy) {
172 t.Fatal("TestXPubsRecoveryLock err:", err)
175 recoveryMgr.stopXPubsRec()
176 if err := recoveryMgr.LoadStatusInfo(); err != nil {
177 t.Fatal("TestXPubsRecoveryLock err:", err)
179 recoveryMgr.finished()
180 if err := recoveryMgr.AcctResurrect([]chainkd.XPub{xpub.XPub}); err != nil {
181 t.Fatal("TestXPubsRecoveryLock err:", err)
185 func TestExtendScanAddresses(t *testing.T) {
186 dirPath, err := ioutil.TempDir(".", "")
190 defer os.RemoveAll(dirPath)
192 testDB := dbm.NewDB("testdb", "leveldb", dirPath)
193 hsm, err := pseudohsm.New(dirPath)
198 xpub, _, err := hsm.XCreate("test_pub", "password", "en")
203 acctMgr := account.NewManager(testDB, nil)
204 recoveryMgr := newRecoveryManager(testDB, acctMgr)
205 acc1 := &account.Account{ID: "testA", Alias: "test1", Signer: &signers.Signer{XPubs: []chainkd.XPub{xpub.XPub}, KeyIndex: 1, DeriveRule: signers.BIP0044}}
206 acc2 := &account.Account{ID: "testB", Alias: "test2"}
207 acc3 := &account.Account{ID: "testC", Alias: "test3", Signer: &signers.Signer{XPubs: []chainkd.XPub{xpub.XPub}, KeyIndex: 2, DeriveRule: 3}}
208 acc4 := &account.Account{ID: "testD", Alias: "test4", Signer: &signers.Signer{XPubs: []chainkd.XPub{xpub.XPub}, KeyIndex: 3, DeriveRule: signers.BIP0032}}
210 recoveryMgr.state.stateForScope(acc1)
211 recoveryMgr.state.stateForScope(acc3)
212 recoveryMgr.state.stateForScope(acc4)
215 acct *account.Account
219 {acc1, nil, addrRecoveryWindow * 2},
220 {acc2, ErrInvalidAcctID, addrRecoveryWindow * 2},
221 {acc3, signers.ErrDeriveRule, addrRecoveryWindow * 2},
222 {acc4, nil, addrRecoveryWindow * 3},
225 for _, c := range cases {
226 if err := recoveryMgr.extendScanAddresses(c.acct.ID, true); err != c.err {
227 t.Fatal("extend scan addresses err:", err)
230 if err := recoveryMgr.extendScanAddresses(c.acct.ID, false); err != c.err {
231 t.Fatal("extend scan addresses err:", err)
234 if uint64(len(recoveryMgr.addresses)) != c.addressLen {
235 t.Fatalf("extend scan addresses err: len:%d,want:%d", len(recoveryMgr.addresses), c.addressLen)
240 func TestRecoveryFromXPubs(t *testing.T) {
241 dirPath, err := ioutil.TempDir(".", "")
245 defer os.RemoveAll(dirPath)
247 testDB := dbm.NewDB("testdb", "leveldb", dirPath)
248 recoveryDB := dbm.NewDB("recdb", "leveldb", dirPath)
249 hsm, err := pseudohsm.New(dirPath)
254 xpub, _, err := hsm.XCreate("test_pub", "password", "en")
259 acctMgr := account.NewManager(testDB, nil)
260 txs, err := MockTxsP2PKH(acctMgr, xpub.XPub, false)
261 recAcctMgr := account.NewManager(recoveryDB, nil)
262 recoveryMgr := newRecoveryManager(recoveryDB, recAcctMgr)
268 {[]chainkd.XPub{xpub.XPub}, nil},
269 {[]chainkd.XPub{xpub.XPub, xpub.XPub}, signers.ErrDupeXPub},
270 {[]chainkd.XPub{}, signers.ErrNoXPubs},
273 for _, c := range cases {
274 if err := recoveryMgr.AcctResurrect(c.xPubs); errors.Root(err) != c.err {
275 t.Fatal("recovery from XPubs err:", err)
279 recoveryMgr.finished()
282 if err := recoveryMgr.FilterRecoveryTxs(MockBlock(txs)); err != nil {
283 t.Fatal("recovery from XPubs err:", err)
286 Accounts, err := acctMgr.ListAccounts("")
288 t.Fatal("recovery from XPubs err:", err)
291 for _, acct := range Accounts {
292 tmp, err := recAcctMgr.GetAccountByXPubsIndex(acct.XPubs, acct.KeyIndex)
294 t.Fatal("recovery from XPubs err:", err)
298 t.Fatal("accout recovery from xpubs err:", acct.KeyIndex)
301 if acctMgr.GetBip44ContractIndex(acct.ID, true) != recAcctMgr.GetBip44ContractIndex(tmp.ID, true) {
302 t.Fatal("bip44 internal address index recovery from xpubs err")
305 if acctMgr.GetBip44ContractIndex(acct.ID, false) != recAcctMgr.GetBip44ContractIndex(tmp.ID, false) {
306 t.Fatal("bip44 external address index recovery from xpubs err")
310 recoveryMgr.finished()
314 func TestRecoveryByRescanAccount(t *testing.T) {
315 dirPath, err := ioutil.TempDir(".", "")
319 defer os.RemoveAll(dirPath)
321 testDB := dbm.NewDB("testdb", "leveldb", dirPath)
322 recoveryDB := dbm.NewDB("recdb", "leveldb", dirPath)
323 hsm, err := pseudohsm.New(dirPath)
328 xpub, _, err := hsm.XCreate("test_pub", "password", "en")
333 acctMgr := account.NewManager(testDB, nil)
334 txs, err := MockTxsP2PKH(acctMgr, xpub.XPub, true)
336 t.Fatal("recovery by rescan account err:", err)
339 allAccounts, err := acctMgr.ListAccounts("")
341 t.Fatal("recovery by rescan account err:", err)
344 recAcctMgr := account.NewManager(recoveryDB, nil)
345 for _, acct := range allAccounts {
346 if err := recAcctMgr.SaveAccount(acct); err != nil {
347 t.Fatal("recovery by rescan account err:", err)
351 recoveryMgr := newRecoveryManager(recoveryDB, recAcctMgr)
353 acct := &account.Account{ID: "testA", Alias: "test1", Signer: &signers.Signer{XPubs: []chainkd.XPub{xpub.XPub}, KeyIndex: 1, DeriveRule: 3}}
356 accounts []*account.Account
360 {[]*account.Account{acct}, signers.ErrDeriveRule},
363 for _, c := range cases {
364 if err := recoveryMgr.AddrResurrect(c.accounts); errors.Root(err) != c.err {
365 t.Fatal("recovery by rescan account err:", err)
371 recoveryMgr.FilterRecoveryTxs(MockBlock(txs))
372 accounts, err := acctMgr.ListAccounts("")
374 t.Fatal("recovery from XPubs err:", err)
377 for _, acct := range accounts {
378 tmp, err := recAcctMgr.GetAccountByXPubsIndex(acct.XPubs, acct.KeyIndex)
380 t.Fatal("recovery from XPubs err:", err)
384 t.Fatal("accout recovery from xpubs err:", acct.KeyIndex)
387 if acctMgr.GetBip44ContractIndex(acct.ID, true) != recAcctMgr.GetBip44ContractIndex(tmp.ID, true) {
388 t.Fatal("bip44 internal address index recovery from xpubs err")
391 if acctMgr.GetBip44ContractIndex(acct.ID, false) != recAcctMgr.GetBip44ContractIndex(tmp.ID, false) {
392 t.Fatal("bip44 external address index recovery from xpubs err")
395 if acctMgr.GetContractIndex(acct.ID) != recAcctMgr.GetContractIndex(tmp.ID) {
396 t.Fatal("bip32 address index recovery from xpubs err")
403 func TestReportFound(t *testing.T) {
404 dirPath, err := ioutil.TempDir(".", "")
408 defer os.RemoveAll(dirPath)
410 testDB := dbm.NewDB("testdb", "leveldb", dirPath)
411 hsm, err := pseudohsm.New(dirPath)
416 xpub1, _, err := hsm.XCreate("test_pub1", "password", "en")
421 xpub2, _, err := hsm.XCreate("test_pub2", "password", "en")
426 acctMgr := account.NewManager(testDB, nil)
427 recoveryMgr := newRecoveryManager(testDB, acctMgr)
428 acc1 := &account.Account{ID: "testA", Alias: "test1", Signer: &signers.Signer{XPubs: []chainkd.XPub{xpub1.XPub}, KeyIndex: 1, DeriveRule: signers.BIP0044}}
429 acc2 := &account.Account{ID: "testB", Alias: "test2", Signer: &signers.Signer{XPubs: []chainkd.XPub{xpub2.XPub}, KeyIndex: 1, DeriveRule: signers.BIP0032}}
430 acc3 := &account.Account{ID: "testC", Alias: "test3", Signer: &signers.Signer{XPubs: []chainkd.XPub{xpub2.XPub}, KeyIndex: 2, DeriveRule: signers.BIP0044}}
432 cp1 := &account.CtrlProgram{AccountID: acc1.ID, Address: "address1", KeyIndex: 10, Change: false}
433 cp2 := &account.CtrlProgram{AccountID: acc1.ID, Address: "address1", KeyIndex: 20, Change: true}
434 cp3 := &account.CtrlProgram{AccountID: acc2.ID, Address: "address1", KeyIndex: 30, Change: false}
435 cp4 := &account.CtrlProgram{AccountID: acc2.ID, Address: "address1", KeyIndex: 40, Change: true}
436 cp5 := &account.CtrlProgram{AccountID: acc3.ID, Address: "address1", KeyIndex: 50, Change: false}
437 cp6 := &account.CtrlProgram{AccountID: acc3.ID, Address: "address1", KeyIndex: 60, Change: true}
439 if err := acctMgr.SaveAccount(acc2); err != nil {
440 t.Fatal("ReportFound test err:", err)
443 if err := acctMgr.SaveAccount(acc3); err != nil {
444 t.Fatal("ReportFound test err:", err)
447 recoveryMgr.state.XPubsStatus = newBranchRecoveryState(acctRecoveryWindow)
448 recoveryMgr.state.XPubs = []chainkd.XPub{xpub1.XPub}
449 recoveryMgr.state.stateForScope(acc1)
450 recoveryMgr.state.stateForScope(acc2)
451 recoveryMgr.state.stateForScope(acc3)
454 acct *account.Account
455 cp *account.CtrlProgram
457 status *addressRecoveryState
460 &addressRecoveryState{InternalBranch: &branchRecoveryState{addrRecoveryWindow, 1, 1}, ExternalBranch: &branchRecoveryState{addrRecoveryWindow, 139, 11}, Account: acc1}},
462 &addressRecoveryState{InternalBranch: &branchRecoveryState{addrRecoveryWindow, 1, 1}, ExternalBranch: &branchRecoveryState{addrRecoveryWindow, 159, 31}, Account: acc2}},
464 &addressRecoveryState{InternalBranch: &branchRecoveryState{addrRecoveryWindow, 149, 21}, ExternalBranch: &branchRecoveryState{addrRecoveryWindow, 139, 11}, Account: acc1}},
466 &addressRecoveryState{InternalBranch: &branchRecoveryState{addrRecoveryWindow, 169, 41}, ExternalBranch: &branchRecoveryState{addrRecoveryWindow, 159, 31}, Account: acc2}},
468 &addressRecoveryState{InternalBranch: &branchRecoveryState{addrRecoveryWindow, 1, 1}, ExternalBranch: &branchRecoveryState{addrRecoveryWindow, 179, 51}, Account: acc3}},
470 &addressRecoveryState{InternalBranch: &branchRecoveryState{addrRecoveryWindow, 189, 61}, ExternalBranch: &branchRecoveryState{addrRecoveryWindow, 179, 51}, Account: acc3}},
473 for _, c := range cases {
474 if err := recoveryMgr.reportFound(c.acct, c.cp); err != c.err {
475 t.Fatal("ReportFound test err:", err, c.acct.ID)
478 status, ok := recoveryMgr.state.AccountsStatus[c.acct.ID]
480 t.Fatal("ReportFound test err: can not find status")
482 if !reflect.DeepEqual(status, c.status) {
483 t.Log(c.status.Account, c.status.InternalBranch, c.status.ExternalBranch)
484 t.Log(status.Account, status.InternalBranch, status.ExternalBranch)
485 t.Fatal("ReportFound test err: recovery status error")
490 func TestLoadStatusInfo(t *testing.T) {
491 dirPath, err := ioutil.TempDir(".", "")
495 defer os.RemoveAll(dirPath)
497 testDB := dbm.NewDB("testdb", "leveldb", "temp")
498 defer os.RemoveAll("temp")
500 hsm, err := pseudohsm.New(dirPath)
505 xpub, _, err := hsm.XCreate("test_pub", "password", "en")
510 acctMgr := account.NewManager(testDB, nil)
511 recoveryMgr := newRecoveryManager(testDB, acctMgr)
512 // StatusInit init recovery status manager.
513 recoveryMgr.state = newRecoveryState()
514 recoveryMgr.state.XPubs = []chainkd.XPub{xpub.XPub}
515 recoveryMgr.state.XPubsStatus = newBranchRecoveryState(acctRecoveryWindow)
517 recoveryMgr.state.StartTime = time.Now()
518 if err := recoveryMgr.LoadStatusInfo(); err != nil {
519 t.Fatal("TestLoadStatusInfo err:", err)
522 recoveryMgr.commitStatusInfo()
524 recoveryMgrRestore := newRecoveryManager(testDB, acctMgr)
525 if err := recoveryMgrRestore.LoadStatusInfo(); err != nil {
526 t.Fatal("TestLoadStatusInfo err:", err)
529 if !reflect.DeepEqual(recoveryMgrRestore.state.XPubsStatus, recoveryMgr.state.XPubsStatus) {
530 t.Fatalf("TestLoadStatusInfo XPubsStatus reload err")
533 if !reflect.DeepEqual(recoveryMgrRestore.state.XPubs, recoveryMgr.state.XPubs) {
534 t.Fatalf("TestLoadStatusInfo XPubs reload err")
537 if !reflect.DeepEqual(recoveryMgrRestore.state.AccountsStatus, recoveryMgr.state.AccountsStatus) {
538 t.Fatalf("TestLoadStatusInfo AccountsStatus reload err")
541 if !recoveryMgrRestore.state.StartTime.Equal(recoveryMgr.state.StartTime) {
542 t.Fatalf("TestLoadStatusInfo StartTime reload err")
545 acct := &account.Account{ID: "testA", Alias: "test1", Signer: &signers.Signer{XPubs: []chainkd.XPub{xpub.XPub}, KeyIndex: 1, DeriveRule: 3}}
546 recoveryMgr.state.AccountsStatus[acct.ID] = newAddressRecoveryState(addrRecoveryWindow, acct)
547 if err := recoveryMgr.commitStatusInfo(); err != nil {
548 t.Fatal("TestLoadStatusInfo err:", err)
550 if err := recoveryMgr.LoadStatusInfo(); err == nil {
551 t.Fatal("TestLoadStatusInfo err")
554 recoveryMgr.state = nil
555 if err := recoveryMgr.commitStatusInfo(); err != nil {
556 t.Fatal("TestLoadStatusInfo err:", err)
559 if err := recoveryMgr.LoadStatusInfo(); err == nil {
560 t.Fatal("TestLoadStatusInfo err")
564 func TestLock(t *testing.T) {
565 dirPath, err := ioutil.TempDir(".", "")
569 defer os.RemoveAll(dirPath)
571 testDB := dbm.NewDB("testdb", "leveldb", "temp")
572 defer os.RemoveAll("temp")
574 acctMgr := account.NewManager(testDB, nil)
575 recoveryMgr := newRecoveryManager(testDB, acctMgr)
576 if !recoveryMgr.tryStartXPubsRec() {
577 t.Fatal("recovery manager try lock test err")
580 if recoveryMgr.tryStartXPubsRec() {
581 t.Fatal("recovery manager relock test err")
584 recoveryMgr.stopXPubsRec()
586 if !recoveryMgr.tryStartXPubsRec() {
587 t.Fatal("recovery manager try lock test err")
591 func TestStateForScope(t *testing.T) {
592 state := newRecoveryState()
593 acc1 := &account.Account{ID: "test1", Alias: "testA"}
594 state.stateForScope(acc1)
595 if !reflect.DeepEqual(state.AccountsStatus[acc1.ID].Account, acc1) {
596 t.Fatal("state for scope test err")
599 acc2 := &account.Account{ID: "test1", Alias: "testB"}
600 state.stateForScope(acc2)
602 if reflect.DeepEqual(state.AccountsStatus[acc2.ID].Account, acc2) {
603 t.Fatal("state for scope test err")
606 acc3 := &account.Account{ID: "test2", Alias: "testC"}
607 state.stateForScope(acc3)
608 if !reflect.DeepEqual(state.AccountsStatus[acc3.ID].Account, acc3) {
609 t.Fatal("state for scope test err")
613 func bip44ContractIndexKey(accountID string, change bool) []byte {
614 contractIndexPrefix := []byte("ContractIndex")
615 key := append(contractIndexPrefix, accountID...)
617 return append(key, []byte{1}...)
619 return append(key, []byte{0}...)
622 func TestContractIndexResidue(t *testing.T) {
623 dirPath, err := ioutil.TempDir(".", "")
627 defer os.RemoveAll(dirPath)
629 testDB := dbm.NewDB("testdb", "leveldb", dirPath)
630 hsm, err := pseudohsm.New(dirPath)
635 xpub1, _, err := hsm.XCreate("test_pub1", "password", "en")
640 contractIndexResidue := uint64(5)
641 acctMgr := account.NewManager(testDB, nil)
642 recoveryMgr := newRecoveryManager(testDB, acctMgr)
643 acct := &account.Account{ID: "testA", Alias: "test1", Signer: &signers.Signer{XPubs: []chainkd.XPub{xpub1.XPub}, KeyIndex: 1, DeriveRule: signers.BIP0044}}
645 cp1 := &account.CtrlProgram{AccountID: acct.ID, Address: "address1", KeyIndex: 10, Change: false}
647 setContractIndexKey := func(acctMgr *account.Manager, accountID string, change bool) {
648 testDB.Set(bip44ContractIndexKey(accountID, change), common.Unit64ToBytes(contractIndexResidue))
651 delAccount := func(acctMgr *account.Manager, accountID string, change bool) {
652 acctMgr.DeleteAccount(accountID)
655 recoveryMgr.state.XPubsStatus = newBranchRecoveryState(acctRecoveryWindow)
656 recoveryMgr.state.XPubs = []chainkd.XPub{xpub1.XPub}
657 recoveryMgr.state.stateForScope(acct)
660 acct *account.Account
661 cp *account.CtrlProgram
662 preProcess func(acctMgr *account.Manager, accountID string, change bool)
666 {acct, cp1, setContractIndexKey, nil, 5},
667 {acct, cp1, delAccount, nil, 10},
670 for _, c := range cases {
671 if c.preProcess != nil {
672 c.preProcess(acctMgr, c.acct.ID, c.cp.Change)
675 if err := acctMgr.SaveAccount(acct); err != nil {
676 t.Fatal("ReportFound test err:", err)
679 if err := recoveryMgr.reportFound(c.acct, c.cp); err != c.err {
680 t.Fatal("ContractIndexResidue test err:", err, c.acct.ID)
682 cps, err := acctMgr.ListControlProgram()
684 t.Fatal("list control program err:", err)
688 for _, cp := range cps {
689 if cp.Address == "" || cp.AccountID != c.acct.ID {
695 if cpNum != c.wantCPNum {
696 t.Fatal("Test contract index residue cp num err want:", c.wantCPNum, " got:", cpNum)