diff --git a/core/account/builder.go b/core/account/builder.go index a41d48c7da..b8391bde10 100644 --- a/core/account/builder.go +++ b/core/account/builder.go @@ -93,11 +93,10 @@ func (a *spendAction) Build(ctx context.Context, b *txbuilder.TemplateBuilder) e return nil } -func (m *Manager) NewSpendUTXOAction(outpoint bc.Outpoint) txbuilder.Action { +func (m *Manager) NewSpendUTXOAction(outputID bc.OutputID) txbuilder.Action { return &spendUTXOAction{ accounts: m, - TxHash: &outpoint.Hash, - TxOut: &outpoint.Index, + OutputID: &outputID, } } @@ -109,27 +108,29 @@ func (m *Manager) DecodeSpendUTXOAction(data []byte) (txbuilder.Action, error) { type spendUTXOAction struct { accounts *Manager - TxHash *bc.Hash `json:"transaction_id"` - TxOut *uint32 `json:"position"` + OutputID *bc.OutputID `json:"output_id"` + TxHash *bc.Hash `json:"transaction_id"` + TxOut *uint32 `json:"position"` ReferenceData chainjson.Map `json:"reference_data"` ClientToken *string `json:"client_token"` } func (a *spendUTXOAction) Build(ctx context.Context, b *txbuilder.TemplateBuilder) error { - var missing []string - if a.TxHash == nil { - missing = append(missing, "transaction_id") - } - if a.TxOut == nil { - missing = append(missing, "position") - } - if len(missing) > 0 { - return txbuilder.MissingFieldsError(missing...) + var outid bc.OutputID + + if a.OutputID != nil { + outid = *a.OutputID + } else if a.TxHash != nil && a.TxOut != nil { + // This is compatibility layer - legacy apps can spend outputs via the raw pair. + outid = bc.ComputeOutputID(*a.TxHash, *a.TxOut) + } else { + // Note: here we do not attempt to check if txid is present, but position is missing, or vice versa. + // Instead, the user has to update their code to use the new API anyway. + return txbuilder.MissingFieldsError("output_id") } - out := bc.Outpoint{Hash: *a.TxHash, Index: *a.TxOut} - res, err := a.accounts.utxoDB.ReserveUTXO(ctx, out, a.ClientToken, b.MaxTime()) + res, err := a.accounts.utxoDB.ReserveUTXO(ctx, outid, a.ClientToken, b.MaxTime()) if err != nil { return err } @@ -161,7 +162,7 @@ func utxoToInputs(ctx context.Context, account *signers.Signer, u *utxo, refData *txbuilder.SigningInstruction, error, ) { - txInput := bc.NewSpendInput(u.Hash, u.Index, nil, u.AssetID, u.Amount, u.ControlProgram, refData) + txInput := bc.NewSpendInput(u.OutputID, nil, u.AssetID, u.Amount, u.ControlProgram, refData) sigInst := &txbuilder.SigningInstruction{ AssetAmount: u.AssetAmount, diff --git a/core/account/builder_test.go b/core/account/builder_test.go index eb4a59a836..ce93b55c83 100644 --- a/core/account/builder_test.go +++ b/core/account/builder_test.go @@ -59,7 +59,7 @@ func TestAccountSourceReserve(t *testing.T) { t.Fatal(err) } - wantTxIns := []*bc.TxInput{bc.NewSpendInput(out.Hash, out.Index, nil, out.AssetID, out.Amount, out.ControlProgram, nil)} + wantTxIns := []*bc.TxInput{bc.NewSpendInput(out.OutputID, nil, out.AssetID, out.Amount, out.ControlProgram, nil)} if !testutil.DeepEqual(tx.Inputs, wantTxIns) { t.Errorf("build txins\ngot:\n\t%+v\nwant:\n\t%+v", tx.Inputs, wantTxIns) } @@ -98,7 +98,7 @@ func TestAccountSourceUTXOReserve(t *testing.T) { prottest.MakeBlock(t, c, g.PendingTxs()) <-pinStore.PinWaiter(account.PinName, c.Height()) - source := accounts.NewSpendUTXOAction(out.Outpoint) + source := accounts.NewSpendUTXOAction(out.OutputID) var builder txbuilder.TemplateBuilder err := source.Build(ctx, &builder) @@ -110,7 +110,7 @@ func TestAccountSourceUTXOReserve(t *testing.T) { t.Fatal(err) } - wantTxIns := []*bc.TxInput{bc.NewSpendInput(out.Hash, out.Index, nil, out.AssetID, out.Amount, out.ControlProgram, nil)} + wantTxIns := []*bc.TxInput{bc.NewSpendInput(out.OutputID, nil, out.AssetID, out.Amount, out.ControlProgram, nil)} if !testutil.DeepEqual(tx.Inputs, wantTxIns) { t.Errorf("build txins\ngot:\n\t%+v\nwant:\n\t%+v", tx.Inputs, wantTxIns) diff --git a/core/account/indexer.go b/core/account/indexer.go index 3d7cc7c01e..1c2fd34a5a 100644 --- a/core/account/indexer.go +++ b/core/account/indexer.go @@ -51,8 +51,14 @@ func (m *Manager) indexAnnotatedAccount(ctx context.Context, a *Account) error { }) } -type output struct { +type rawOutput struct { state.Output + txHash bc.Hash + outputIndex uint32 +} + +type accountOutput struct { + rawOutput AccountID string keyIndex uint64 } @@ -66,16 +72,20 @@ func (m *Manager) ProcessBlocks(ctx context.Context) { func (m *Manager) indexAccountUTXOs(ctx context.Context, b *bc.Block) error { // Upsert any UTXOs belonging to accounts managed by this Core. - outs := make([]*state.Output, 0, len(b.Transactions)) + outs := make([]*rawOutput, 0, len(b.Transactions)) blockPositions := make(map[bc.Hash]uint32, len(b.Transactions)) for i, tx := range b.Transactions { blockPositions[tx.Hash] = uint32(i) for j, out := range tx.Outputs { - stateOutput := &state.Output{ - TxOutput: *out, - Outpoint: bc.Outpoint{Hash: tx.Hash, Index: uint32(j)}, + out := &rawOutput{ + Output: state.Output{ + TxOutput: *out, + OutputID: tx.OutputID(uint32(j)), + }, + txHash: tx.Hash, + outputIndex: uint32(j), } - outs = append(outs, stateOutput) + outs = append(outs, out) } } accOuts, err := m.loadAccountInfo(ctx, outs) @@ -89,24 +99,23 @@ func (m *Manager) indexAccountUTXOs(ctx context.Context, b *bc.Block) error { } // Delete consumed account UTXOs. - deltxhash, delindex := prevoutDBKeys(b.Transactions...) + delOutputIDs := prevoutDBKeys(b.Transactions...) const delQ = ` DELETE FROM account_utxos - WHERE (tx_hash, index) IN (SELECT unnest($1::bytea[]), unnest($2::integer[])) + WHERE output_id IN (SELECT unnest($1::bytea[])) ` - _, err = m.db.Exec(ctx, delQ, deltxhash, delindex) + _, err = m.db.Exec(ctx, delQ, delOutputIDs) return errors.Wrap(err, "deleting spent account utxos") } -func prevoutDBKeys(txs ...*bc.Tx) (txhash pq.ByteaArray, index pg.Uint32s) { +func prevoutDBKeys(txs ...*bc.Tx) (outputIDs pq.ByteaArray) { for _, tx := range txs { for _, in := range tx.Inputs { if in.IsIssuance() { continue } - o := in.Outpoint() - txhash = append(txhash, o.Hash[:]) - index = append(index, o.Index) + o := in.SpentOutputID() + outputIDs = append(outputIDs, o.Bytes()) } } return @@ -115,8 +124,8 @@ func prevoutDBKeys(txs ...*bc.Tx) (txhash pq.ByteaArray, index pg.Uint32s) { // loadAccountInfo turns a set of state.Outputs into a set of // outputs by adding account annotations. Outputs that can't be // annotated are excluded from the result. -func (m *Manager) loadAccountInfo(ctx context.Context, outs []*state.Output) ([]*output, error) { - outsByScript := make(map[string][]*state.Output, len(outs)) +func (m *Manager) loadAccountInfo(ctx context.Context, outs []*rawOutput) ([]*accountOutput, error) { + outsByScript := make(map[string][]*rawOutput, len(outs)) for _, out := range outs { scriptStr := string(out.ControlProgram) outsByScript[scriptStr] = append(outsByScript[scriptStr], out) @@ -127,7 +136,7 @@ func (m *Manager) loadAccountInfo(ctx context.Context, outs []*state.Output) ([] scripts = append(scripts, []byte(s)) } - result := make([]*output, 0, len(outs)) + result := make([]*accountOutput, 0, len(outs)) const q = ` SELECT signer_id, key_index, control_program @@ -136,8 +145,8 @@ func (m *Manager) loadAccountInfo(ctx context.Context, outs []*state.Output) ([] ` err := pg.ForQueryRows(ctx, m.db, q, scripts, func(accountID string, keyIndex uint64, program []byte) { for _, out := range outsByScript[string(program)] { - newOut := &output{ - Output: *out, + newOut := &accountOutput{ + rawOutput: *out, AccountID: accountID, keyIndex: keyIndex, } @@ -154,10 +163,12 @@ func (m *Manager) loadAccountInfo(ctx context.Context, outs []*state.Output) ([] // upsertConfirmedAccountOutputs records the account data for confirmed utxos. // If the account utxo already exists (because it's from a local tx), the // block confirmation data will in the row will be updated. -func (m *Manager) upsertConfirmedAccountOutputs(ctx context.Context, outs []*output, pos map[bc.Hash]uint32, block *bc.Block) error { +func (m *Manager) upsertConfirmedAccountOutputs(ctx context.Context, outs []*accountOutput, pos map[bc.Hash]uint32, block *bc.Block) error { var ( txHash pq.ByteaArray index pg.Uint32s + outputID pq.ByteaArray + unspentID pq.ByteaArray assetID pq.ByteaArray amount pq.Int64Array accountID pq.StringArray @@ -165,8 +176,10 @@ func (m *Manager) upsertConfirmedAccountOutputs(ctx context.Context, outs []*out program pq.ByteaArray ) for _, out := range outs { - txHash = append(txHash, out.Outpoint.Hash[:]) - index = append(index, out.Outpoint.Index) + txHash = append(txHash, out.txHash[:]) + index = append(index, out.outputIndex) + outputID = append(outputID, out.OutputID.Bytes()) + unspentID = append(unspentID, out.UnspentID().Bytes()) assetID = append(assetID, out.AssetID[:]) amount = append(amount, int64(out.Amount)) accountID = append(accountID, out.AccountID) @@ -175,15 +188,17 @@ func (m *Manager) upsertConfirmedAccountOutputs(ctx context.Context, outs []*out } const q = ` - INSERT INTO account_utxos (tx_hash, index, asset_id, amount, account_id, control_program_index, + INSERT INTO account_utxos (tx_hash, index, output_id, unspent_id, asset_id, amount, account_id, control_program_index, control_program, confirmed_in) - SELECT unnest($1::bytea[]), unnest($2::bigint[]), unnest($3::bytea[]), unnest($4::bigint[]), - unnest($5::text[]), unnest($6::bigint[]), unnest($7::bytea[]), $8 + SELECT unnest($1::bytea[]), unnest($2::bigint[]), unnest($3::bytea[]), unnest($4::bytea[]), unnest($5::bytea[]), unnest($6::bigint[]), + unnest($7::text[]), unnest($8::bigint[]), unnest($9::bytea[]), $10 ON CONFLICT (tx_hash, index) DO NOTHING ` _, err := m.db.Exec(ctx, q, txHash, index, + outputID, + unspentID, assetID, amount, accountID, diff --git a/core/account/indexer_test.go b/core/account/indexer_test.go index b7b54c7c2b..e4bef9c4e2 100644 --- a/core/account/indexer_test.go +++ b/core/account/indexer_test.go @@ -22,10 +22,14 @@ func TestLoadAccountInfo(t *testing.T) { to1 := bc.NewTxOutput(bc.AssetID{}, 0, acp, nil) to2 := bc.NewTxOutput(bc.AssetID{}, 0, []byte("notfound"), nil) - outs := []*state.Output{{ - TxOutput: *to1, + outs := []*rawOutput{{ + Output: state.Output{ + TxOutput: *to1, + }, }, { - TxOutput: *to2, + Output: state.Output{ + TxOutput: *to2, + }, }} got, err := m.loadAccountInfo(ctx, outs) @@ -61,7 +65,7 @@ func TestDeleteUTXOs(t *testing.T) { block2 := &bc.Block{Transactions: []*bc.Tx{ bc.NewTx(bc.TxData{ Inputs: []*bc.TxInput{ - bc.NewSpendInput(block1.Transactions[0].Hash, 0, nil, assetID, 1, nil, nil), + bc.NewSpendInput(bc.ComputeOutputID(block1.Transactions[0].Hash, 0), nil, assetID, 1, nil, nil), }, }), }} diff --git a/core/account/reserve.go b/core/account/reserve.go index bbe033375a..fceb0c5e7a 100644 --- a/core/account/reserve.go +++ b/core/account/reserve.go @@ -13,7 +13,6 @@ import ( "chain/errors" "chain/protocol" "chain/protocol/bc" - "chain/protocol/state" "chain/sync/idempotency" ) @@ -35,7 +34,9 @@ var ( // utxo describes an individual account utxo. type utxo struct { - bc.Outpoint + bc.Outpoint // TODO(oleg): remove this one + bc.OutputID + bc.UnspentID bc.AssetAmount ControlProgram []byte @@ -143,7 +144,7 @@ func (re *reserver) reserve(ctx context.Context, src source, amount uint64, clie // ReserveUTXO reserves a specific utxo for spending. The resulting // reservation expires at exp. -func (re *reserver) ReserveUTXO(ctx context.Context, out bc.Outpoint, clientToken *string, exp time.Time) (*reservation, error) { +func (re *reserver) ReserveUTXO(ctx context.Context, out bc.OutputID, clientToken *string, exp time.Time) (*reservation, error) { if clientToken == nil { return re.reserveUTXO(ctx, out, exp, nil) } @@ -154,7 +155,7 @@ func (re *reserver) ReserveUTXO(ctx context.Context, out bc.Outpoint, clientToke return untypedRes.(*reservation), err } -func (re *reserver) reserveUTXO(ctx context.Context, out bc.Outpoint, exp time.Time, clientToken *string) (*reservation, error) { +func (re *reserver) reserveUTXO(ctx context.Context, out bc.OutputID, exp time.Time, clientToken *string) (*reservation, error) { u, err := findSpecificUTXO(ctx, re.db, out) if err != nil { return nil, err @@ -231,7 +232,7 @@ func (re *reserver) ExpireReservations(ctx context.Context) error { func (re *reserver) checkUTXO(u *utxo) bool { _, s := re.c.State() - return s.Tree.ContainsKey(state.OutputKey(u.Outpoint)) + return s.Tree.ContainsKey(u.UnspentID.Bytes()) } func (re *reserver) source(src source) *sourceReserver { @@ -250,8 +251,8 @@ func (re *reserver) source(src source) *sourceReserver { heightFn: func() uint64 { return re.pinStore.Height(PinName) }, - cached: make(map[bc.Outpoint]*utxo), - reserved: make(map[bc.Outpoint]uint64), + cached: make(map[bc.OutputID]*utxo), + reserved: make(map[bc.OutputID]uint64), } re.sources[src] = sr return sr @@ -264,8 +265,8 @@ type sourceReserver struct { heightFn func() uint64 mu sync.Mutex - cached map[bc.Outpoint]*utxo - reserved map[bc.Outpoint]uint64 + cached map[bc.OutputID]*utxo + reserved map[bc.OutputID]uint64 lastHeight uint64 } @@ -294,7 +295,7 @@ func (sr *sourceReserver) reserveFromCache(rid uint64, amount uint64) ([]*utxo, for o, u := range sr.cached { // If the UTXO is already reserved, skip it. - if _, ok := sr.reserved[u.Outpoint]; ok { + if _, ok := sr.reserved[u.OutputID]; ok { unavailable += u.Amount continue } @@ -325,7 +326,7 @@ func (sr *sourceReserver) reserveFromCache(rid uint64, amount uint64) ([]*utxo, // We've found enough to satisfy the request. for _, u := range reservedUTXOs { - sr.reserved[u.Outpoint] = rid + sr.reserved[u.OutputID] = rid } return reservedUTXOs, reserved, nil @@ -335,12 +336,12 @@ func (sr *sourceReserver) reserveUTXO(rid uint64, utxo *utxo) error { sr.mu.Lock() defer sr.mu.Unlock() - _, isReserved := sr.reserved[utxo.Outpoint] + _, isReserved := sr.reserved[utxo.OutputID] if isReserved { return ErrReserved } - sr.reserved[utxo.Outpoint] = rid + sr.reserved[utxo.OutputID] = rid return nil } @@ -348,7 +349,7 @@ func (sr *sourceReserver) cancel(res *reservation) { sr.mu.Lock() defer sr.mu.Unlock() for _, utxo := range res.UTXOs { - delete(sr.reserved, utxo.Outpoint) + delete(sr.reserved, utxo.OutputID) } } @@ -372,7 +373,7 @@ func (sr *sourceReserver) refillCache(ctx context.Context) error { sr.lastHeight = curHeight } for _, u := range utxos { - sr.cached[u.Outpoint] = u + sr.cached[u.OutputID] = u } sr.mu.Unlock() @@ -381,18 +382,20 @@ func (sr *sourceReserver) refillCache(ctx context.Context) error { func findMatchingUTXOs(ctx context.Context, db pg.DB, src source, height uint64) ([]*utxo, error) { const q = ` - SELECT tx_hash, index, amount, control_program_index, control_program + SELECT tx_hash, index, output_id, unspent_id, amount, control_program_index, control_program FROM account_utxos WHERE account_id = $1 AND asset_id = $2 AND confirmed_in > $3 ` var utxos []*utxo err := pg.ForQueryRows(ctx, db, q, src.AccountID, src.AssetID, height, - func(txHash bc.Hash, index uint32, amount uint64, cpIndex uint64, controlProg []byte) { + func(txHash bc.Hash, index uint32, oid bc.OutputID, uid bc.UnspentID, amount uint64, cpIndex uint64, controlProg []byte) { utxos = append(utxos, &utxo{ Outpoint: bc.Outpoint{ Hash: txHash, Index: index, }, + OutputID: oid, + UnspentID: uid, AssetAmount: bc.AssetAmount{ Amount: amount, AssetID: src.AssetID, @@ -408,19 +411,20 @@ func findMatchingUTXOs(ctx context.Context, db pg.DB, src source, height uint64) return utxos, nil } -func findSpecificUTXO(ctx context.Context, db pg.DB, out bc.Outpoint) (*utxo, error) { +func findSpecificUTXO(ctx context.Context, db pg.DB, out bc.OutputID) (*utxo, error) { const q = ` - SELECT account_id, asset_id, amount, control_program_index, control_program + SELECT unspent_id, account_id, asset_id, amount, control_program_index, control_program FROM account_utxos - WHERE tx_hash = $1 AND index = $2 + WHERE output_id = $1 ` u := new(utxo) - err := db.QueryRow(ctx, q, out.Hash, out.Index).Scan(&u.AccountID, &u.AssetID, &u.Amount, &u.ControlProgramIndex, &u.ControlProgram) + // TODO(oleg): maybe we need to scan txid:index too from here... + err := db.QueryRow(ctx, q, out).Scan(&u.UnspentID, &u.AccountID, &u.AssetID, &u.Amount, &u.ControlProgramIndex, &u.ControlProgram) if err == sql.ErrNoRows { return nil, pg.ErrUserInputNotFound } else if err != nil { return nil, errors.Wrap(err) } - u.Outpoint = out + u.OutputID = out return u, nil } diff --git a/core/account/reserve_test.go b/core/account/reserve_test.go index 188be8df37..cfdd1b0b12 100644 --- a/core/account/reserve_test.go +++ b/core/account/reserve_test.go @@ -8,15 +8,16 @@ import ( "chain/database/pg/pgtest" "chain/protocol/bc" "chain/protocol/prottest" - "chain/protocol/state" ) const sampleAccountUTXOs = ` INSERT INTO account_utxos - (tx_hash, index, asset_id, amount, account_id, control_program_index, + (tx_hash, index, output_id, unspent_id, asset_id, amount, account_id, control_program_index, control_program, confirmed_in) VALUES ( decode('270b725a94429496a178c56b390a89d03f801fe2ee992d90cf4fdf7d7855318e', 'hex'), 0, + decode('9886ae2dc24b6d868c68768038c43801e905a62f1a9b826ca0dc357f00c30117', 'hex'), + decode('a31a96f07fc4e24c5d1aebd08e882f00324e64c66e9d7081fbbd1019a033e6c5', 'hex'), decode('df1df9d4f66437ab5be715e4d1faeb29d24c80a6dc8276d6a630f05c5f1f7693', 'hex'), 1000, 'accEXAMPLE', 1, '\x6a'::bytea, 1); ` @@ -41,23 +42,24 @@ func TestCancelReservation(t *testing.T) { if err != nil { t.Fatal(err) } - out := bc.Outpoint{Hash: h, Index: 0} + outputid := bc.ComputeOutputID(h, 0) + unspentid := bc.ComputeUnspentID(outputid, bc.Hash{}) // assuming all-zero hash of the output - this test ignores it. // Fake the output in the state tree. _, s := c.State() - err = s.Tree.Insert(state.OutputKey(out), []byte{0xc0, 0x01, 0xca, 0xfe}) + err = s.Tree.Insert(unspentid.Bytes(), []byte{0xc0, 0x01, 0xca, 0xfe}) if err != nil { t.Error(err) } utxoDB := newReserver(db, c, nil) - res, err := utxoDB.ReserveUTXO(ctx, out, nil, time.Now()) + res, err := utxoDB.ReserveUTXO(ctx, outputid, nil, time.Now()) if err != nil { t.Fatal(err) } // Verify that the UTXO is reserved. - _, err = utxoDB.ReserveUTXO(ctx, out, nil, time.Now()) + _, err = utxoDB.ReserveUTXO(ctx, outputid, nil, time.Now()) if err != ErrReserved { t.Fatalf("got=%s want=%s", err, ErrReserved) } @@ -69,7 +71,7 @@ func TestCancelReservation(t *testing.T) { } // Reserving again should succeed. - _, err = utxoDB.ReserveUTXO(ctx, out, nil, time.Now()) + _, err = utxoDB.ReserveUTXO(ctx, outputid, nil, time.Now()) if err != nil { t.Fatal(err) } diff --git a/core/api_test.go b/core/api_test.go index f715b46c05..11b9f72ed7 100644 --- a/core/api_test.go +++ b/core/api_test.go @@ -91,8 +91,8 @@ func TestBuildFinal(t *testing.T) { if err != nil { t.Fatal(err) } - if len(insts1) != 23 { - t.Fatalf("expected 23 instructions in sigwitness program 1, got %d (%x)", len(insts1), prog1) + if len(insts1) != 19 { + t.Fatalf("expected 19 instructions in sigwitness program 1, got %d (%x)", len(insts1), prog1) } if insts1[0].Op != vm.OP_MAXTIME { t.Fatalf("sigwitness program1 opcode 0 is %02x, expected %02x", insts1[0].Op, vm.OP_MAXTIME) @@ -103,18 +103,18 @@ func TestBuildFinal(t *testing.T) { if insts1[3].Op != vm.OP_VERIFY { t.Fatalf("sigwitness program1 opcode 3 is %02x, expected %02x", insts1[3].Op, vm.OP_VERIFY) } - for i, op := range []vm.Op{vm.OP_FALSE, vm.OP_OUTPOINT, vm.OP_ROT, vm.OP_NUMEQUAL, vm.OP_VERIFY, vm.OP_EQUAL, vm.OP_VERIFY} { + for i, op := range []vm.Op{vm.OP_OUTPUTID, vm.OP_EQUAL, vm.OP_VERIFY} { if insts1[i+5].Op != op { t.Fatalf("sigwitness program 1 opcode %d is %02x, expected %02x", i+5, insts1[i+5].Op, op) } } for i, op := range []vm.Op{vm.OP_REFDATAHASH, vm.OP_EQUAL, vm.OP_VERIFY, vm.OP_FALSE, vm.OP_FALSE} { - if insts1[i+13].Op != op { - t.Fatalf("sigwitness program 1 opcode %d is %02x, expected %02x", i+13, insts1[i+13].Op, op) + if insts1[i+9].Op != op { + t.Fatalf("sigwitness program 1 opcode %d is %02x, expected %02x", i+9, insts1[i+9].Op, op) } } - if insts1[22].Op != vm.OP_CHECKOUTPUT { - t.Fatalf("sigwitness program1 opcode 18 is %02x, expected %02x", insts1[18].Op, vm.OP_CHECKOUTPUT) + if insts1[18].Op != vm.OP_CHECKOUTPUT { + t.Fatalf("sigwitness program1 opcode %d is %02x, expected %02x", 18, insts1[18].Op, vm.OP_CHECKOUTPUT) } prog2 := tmpl2.SigningInstructions[0].WitnessComponents[0].(*txbuilder.SignatureWitness).Program diff --git a/core/coretest/fixtures.go b/core/coretest/fixtures.go index 85ec741b47..4f32c90124 100644 --- a/core/coretest/fixtures.go +++ b/core/coretest/fixtures.go @@ -65,7 +65,7 @@ func IssueAssets(ctx context.Context, t testing.TB, c *protocol.Chain, s txbuild } return state.Output{ - Outpoint: bc.Outpoint{Hash: tpl.Transaction.Hash(), Index: 0}, + OutputID: tpl.Transaction.OutputID(0), TxOutput: *tpl.Transaction.Outputs[0], } } diff --git a/core/migrate/data.go b/core/migrate/data.go index 52f8a2d836..17ba5f7aa9 100644 --- a/core/migrate/data.go +++ b/core/migrate/data.go @@ -107,4 +107,11 @@ var migrations = []migration{ {Name: "2017-01-19.0.asset.drop-mutable-flag.sql", SQL: ` ALTER TABLE assets DROP COLUMN definition_mutable; `}, + {Name: "2017-01-20.0.core.add-output-id-to-outputs.sql", SQL: ` + ALTER TABLE annotated_outputs + ADD COLUMN output_id bytea UNIQUE NOT NULL; + ALTER TABLE account_utxos + ADD COLUMN output_id bytea UNIQUE NOT NULL, + ADD COLUMN unspent_id bytea UNIQUE NOT NULL; + `}, } diff --git a/core/query/annotated.go b/core/query/annotated.go index c6560af462..4b81bf3e1a 100644 --- a/core/query/annotated.go +++ b/core/query/annotated.go @@ -34,7 +34,7 @@ type AnnotatedInput struct { Amount uint64 `json:"amount"` IssuanceProgram chainjson.HexBytes `json:"issuance_program,omitempty"` ControlProgram chainjson.HexBytes `json:"control_program,omitempty"` - SpentOutput *SpentOutput `json:"spent_output,omitempty"` + SpentOutputID chainjson.HexBytes `json:"spent_output_id,omitempty"` AccountID string `json:"account_id,omitempty"` AccountAlias string `json:"account_alias,omitempty"` AccountTags *json.RawMessage `json:"account_tags,omitempty"` @@ -45,6 +45,7 @@ type AnnotatedInput struct { type AnnotatedOutput struct { Type string `json:"type"` Purpose string `json:"purpose,omitempty"` + OutputID chainjson.HexBytes `json:"id"` TransactionID chainjson.HexBytes `json:"transaction_id,omitempty"` Position uint32 `json:"position"` AssetID chainjson.HexBytes `json:"asset_id"` @@ -61,11 +62,6 @@ type AnnotatedOutput struct { IsLocal Bool `json:"is_local"` } -type SpentOutput struct { - TransactionID chainjson.HexBytes `json:"transaction_id"` - Position uint32 `json:"position"` -} - type Bool bool func (b Bool) MarshalJSON() ([]byte, error) { @@ -104,7 +100,7 @@ func buildAnnotatedTransaction(orig *bc.Tx, b *bc.Block, indexInBlock uint32) *A tx.Inputs = append(tx.Inputs, buildAnnotatedInput(in)) } for i, out := range orig.Outputs { - tx.Outputs = append(tx.Outputs, buildAnnotatedOutput(out, uint32(i))) + tx.Outputs = append(tx.Outputs, buildAnnotatedOutput(out, uint32(i), orig.Hash)) } return tx } @@ -128,23 +124,22 @@ func buildAnnotatedInput(orig *bc.TxInput) *AnnotatedInput { in.IssuanceProgram = prog } else { prog := orig.ControlProgram() - outpoint := orig.Outpoint() + prevoutID := orig.SpentOutputID() in.Type = "spend" in.ControlProgram = prog - in.SpentOutput = &SpentOutput{ - TransactionID: outpoint.Hash[:], - Position: outpoint.Index, - } + in.SpentOutputID = prevoutID.Bytes() } return in } -func buildAnnotatedOutput(orig *bc.TxOutput, idx uint32) *AnnotatedOutput { +func buildAnnotatedOutput(orig *bc.TxOutput, idx uint32, txhash bc.Hash) *AnnotatedOutput { referenceData := json.RawMessage(orig.ReferenceData) if len(referenceData) == 0 { referenceData = []byte(`{}`) } + outid := bc.ComputeOutputID(txhash, idx) out := &AnnotatedOutput{ + OutputID: outid.Bytes(), Position: idx, AssetID: orig.AssetID[:], Amount: orig.Amount, diff --git a/core/query/index.go b/core/query/index.go index 7b16acc624..0a1ff675ff 100644 --- a/core/query/index.go +++ b/core/query/index.go @@ -108,17 +108,16 @@ func (ind *Indexer) insertAnnotatedOutputs(ctx context.Context, b *bc.Block, ann outputTxPositions pg.Uint32s outputIndexes pg.Uint32s outputTxHashes pq.ByteaArray + outputIDs pq.ByteaArray outputData pq.StringArray - prevoutHashes pq.ByteaArray - prevoutIndexes pg.Uint32s + prevoutIDs pq.ByteaArray ) for pos, tx := range b.Transactions { for _, in := range tx.Inputs { if !in.IsIssuance() { - outpoint := in.Outpoint() - prevoutHashes = append(prevoutHashes, outpoint.Hash[:]) - prevoutIndexes = append(prevoutIndexes, outpoint.Index) + prevoutID := in.SpentOutputID() + prevoutIDs = append(prevoutIDs, prevoutID.Bytes()) } } @@ -137,27 +136,28 @@ func (ind *Indexer) insertAnnotatedOutputs(ctx context.Context, b *bc.Block, ann outputTxPositions = append(outputTxPositions, uint32(pos)) outputIndexes = append(outputIndexes, uint32(outIndex)) outputTxHashes = append(outputTxHashes, tx.Hash[:]) + outputIDs = append(outputIDs, outCopy.OutputID) outputData = append(outputData, string(serializedData)) } } // Insert all of the block's outputs at once. const insertQ = ` - INSERT INTO annotated_outputs (block_height, tx_pos, output_index, tx_hash, data, timespan) - SELECT $1, unnest($2::integer[]), unnest($3::integer[]), unnest($4::bytea[]), - unnest($5::jsonb[]), int8range($6, NULL) + INSERT INTO annotated_outputs (block_height, tx_pos, output_index, tx_hash, output_id, data, timespan) + SELECT $1, unnest($2::integer[]), unnest($3::integer[]), unnest($4::bytea[]), unnest($5::bytea[]), + unnest($6::jsonb[]), int8range($7, NULL) ON CONFLICT (block_height, tx_pos, output_index) DO NOTHING; ` _, err := ind.db.Exec(ctx, insertQ, b.Height, outputTxPositions, - outputIndexes, outputTxHashes, outputData, b.TimestampMS) + outputIndexes, outputTxHashes, outputIDs, outputData, b.TimestampMS) if err != nil { return errors.Wrap(err, "batch inserting annotated outputs") } const updateQ = ` UPDATE annotated_outputs SET timespan = INT8RANGE(LOWER(timespan), $1) - WHERE (tx_hash, output_index) IN (SELECT unnest($2::bytea[]), unnest($3::integer[])) + WHERE (output_id) IN (SELECT unnest($2::bytea[])) ` - _, err = ind.db.Exec(ctx, updateQ, b.TimestampMS, prevoutHashes, prevoutIndexes) + _, err = ind.db.Exec(ctx, updateQ, b.TimestampMS, prevoutIDs) return errors.Wrap(err, "updating spent annotated outputs") } diff --git a/core/query/outputs_test.go b/core/query/outputs_test.go index be359d9dea..e5c3e66c6c 100644 --- a/core/query/outputs_test.go +++ b/core/query/outputs_test.go @@ -43,12 +43,12 @@ func TestOutputsAfter(t *testing.T) { _, db := pgtest.NewDB(t, pgtest.SchemaPath) ctx := context.Background() _, err := db.Exec(ctx, ` - INSERT INTO annotated_outputs (block_height, tx_pos, output_index, tx_hash, data, timespan) + INSERT INTO annotated_outputs (block_height, tx_pos, output_index, tx_hash, output_id, data, timespan) VALUES - (1, 0, 0, 'ab', '{"account_id": "abc"}', int8range(1, 100)), - (1, 1, 0, 'cd', '{"account_id": "abc"}', int8range(1, 100)), - (1, 1, 1, 'cd', '{"account_id": "abc"}', int8range(1, 100)), - (2, 0, 0, 'ef', '{"account_id": "abc"}', int8range(10, 50)); + (1, 0, 0, 'ab', 'o1', '{"account_id": "abc"}', int8range(1, 100)), + (1, 1, 0, 'cd', 'o2', '{"account_id": "abc"}', int8range(1, 100)), + (1, 1, 1, 'cd', 'o3', '{"account_id": "abc"}', int8range(1, 100)), + (2, 0, 0, 'ef', 'o4', '{"account_id": "abc"}', int8range(10, 50)); `) if err != nil { t.Fatal(err) diff --git a/core/schema.sql b/core/schema.sql index 387e7a349f..793a38b841 100644 --- a/core/schema.sql +++ b/core/schema.sql @@ -2,8 +2,8 @@ -- PostgreSQL database dump -- --- Dumped from database version 9.6.1 --- Dumped by pg_dump version 9.6.1 +-- Dumped from database version 9.6.0 +-- Dumped by pg_dump version 9.6.0 SET statement_timeout = 0; SET lock_timeout = 0; @@ -194,7 +194,9 @@ CREATE TABLE account_utxos ( account_id text NOT NULL, control_program_index bigint NOT NULL, control_program bytea NOT NULL, - confirmed_in bigint NOT NULL + confirmed_in bigint NOT NULL, + output_id bytea NOT NULL, + unspent_id bytea NOT NULL ); @@ -240,7 +242,8 @@ CREATE TABLE annotated_outputs ( output_index integer NOT NULL, tx_hash bytea NOT NULL, data jsonb NOT NULL, - timespan int8range NOT NULL + timespan int8range NOT NULL, + output_id bytea NOT NULL ); @@ -553,6 +556,14 @@ ALTER TABLE ONLY accounts ADD CONSTRAINT account_tags_pkey PRIMARY KEY (account_id); +-- +-- Name: account_utxos account_utxos_output_id_key; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY account_utxos + ADD CONSTRAINT account_utxos_output_id_key UNIQUE (output_id); + + -- -- Name: account_utxos account_utxos_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -561,6 +572,14 @@ ALTER TABLE ONLY account_utxos ADD CONSTRAINT account_utxos_pkey PRIMARY KEY (tx_hash, index); +-- +-- Name: account_utxos account_utxos_unspent_id_key; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY account_utxos + ADD CONSTRAINT account_utxos_unspent_id_key UNIQUE (unspent_id); + + -- -- Name: accounts accounts_alias_key; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -585,6 +604,14 @@ ALTER TABLE ONLY annotated_assets ADD CONSTRAINT annotated_assets_pkey PRIMARY KEY (id); +-- +-- Name: annotated_outputs annotated_outputs_output_id_key; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY annotated_outputs + ADD CONSTRAINT annotated_outputs_output_id_key UNIQUE (output_id); + + -- -- Name: annotated_outputs annotated_outputs_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -881,3 +908,4 @@ insert into migrations (filename, hash) values ('2017-01-10.0.signers.xpubs-type insert into migrations (filename, hash) values ('2017-01-11.0.core.hash-bytea.sql', '9f7f15df3479c38f193884a2d3cb7ae8001ed08607f9cc661fd5c420e248688d'); insert into migrations (filename, hash) values ('2017-01-13.0.core.asset-definition-bytea.sql', 'f49458c5c8873d919ec35be4683074be0b04913c95f5ab1bf1402aa2b4847cf5'); insert into migrations (filename, hash) values ('2017-01-19.0.asset.drop-mutable-flag.sql', '7850023d44c545c155c0ee372e7cdfef1859b40221bd94307b836503c26dd3de'); +insert into migrations (filename, hash) values ('2017-01-20.0.core.add-output-id-to-outputs.sql', '4c8531c06e62405d2989e0651a7ef6c2ebd0b2b269b57c179e9e36f7fdbb715b'); diff --git a/core/txbuilder/constraint.go b/core/txbuilder/constraint.go index 91ec409652..9976a0dc50 100644 --- a/core/txbuilder/constraint.go +++ b/core/txbuilder/constraint.go @@ -44,16 +44,14 @@ func (t timeConstraint) code() []byte { return builder.Program } -// outpointConstraint requires the outpoint being spent to equal the +// outpointConstraint requires the outputID (and therefore, the outpoint) being spent to equal the // given value. -type outpointConstraint bc.Outpoint +type outpointConstraint bc.OutputID func (o outpointConstraint) code() []byte { builder := vmutil.NewBuilder() - builder.AddData(o.Hash[:]).AddInt64(int64(o.Index)) - builder.AddOp(vm.OP_OUTPOINT) // stack is now [... hash index hash index] - builder.AddOp(vm.OP_ROT) // stack is now [... hash hash index index] - builder.AddOp(vm.OP_NUMEQUAL).AddOp(vm.OP_VERIFY) // stack is now [... hash hash] + builder.AddData(o.Bytes()) + builder.AddOp(vm.OP_OUTPUTID) builder.AddOp(vm.OP_EQUAL) return builder.Program } diff --git a/core/txbuilder/finalize_test.go b/core/txbuilder/finalize_test.go index e3ebb3b688..62fcb4cec5 100644 --- a/core/txbuilder/finalize_test.go +++ b/core/txbuilder/finalize_test.go @@ -256,10 +256,9 @@ func benchGenBlock(b *testing.B) { "00" + // common witness extensible string length "01" + // inputs count "01" + // input 0, asset version - "4c" + // input 0, input commitment length prefix + "4b" + // input 0, input commitment length prefix "01" + // input 0, input commitment, "spend" type - "dd385f6fe25d91d8c1bd0fa58951ad56b0c5229dcc01f61d9f9e8b9eb92d3292" + // input 0, spend input commitment, outpoint tx hash - "00" + // input 0, spend input commitment, outpoint index + "110bd1b4e5efc2994c9abc77f223a52c834d8f26b907c6c19d90b9e77a8e2fed" + // input 0, spend input commitment, output ID "29" + // input 0, spend input commitment, output commitment length prefix "0000000000000000000000000000000000000000000000000000000000000000" + // input 0, spend input commitment, output commitment, asset id "80a094a58d1d" + // input 0, spend input commitment, output commitment, amount diff --git a/core/txbuilder/txbuilder_test.go b/core/txbuilder/txbuilder_test.go index 38c713491c..3f0ffd4791 100644 --- a/core/txbuilder/txbuilder_test.go +++ b/core/txbuilder/txbuilder_test.go @@ -23,7 +23,7 @@ import ( type testAction bc.AssetAmount func (t testAction) Build(ctx context.Context, b *TemplateBuilder) error { - in := bc.NewSpendInput([32]byte{255}, 0, nil, t.AssetID, t.Amount, nil, nil) + in := bc.NewSpendInput(bc.ComputeOutputID([32]byte{255}, 0), nil, t.AssetID, t.Amount, nil, nil) tplIn := &SigningInstruction{} err := b.AddInput(in, tplIn) @@ -59,7 +59,7 @@ func TestBuild(t *testing.T) { Version: 1, MaxTime: bc.Millis(expiryTime), Inputs: []*bc.TxInput{ - bc.NewSpendInput([32]byte{255}, 0, nil, [32]byte{1}, 5, nil, nil), + bc.NewSpendInput(bc.ComputeOutputID([32]byte{255}, 0), nil, [32]byte{1}, 5, nil, nil), }, Outputs: []*bc.TxOutput{ bc.NewTxOutput([32]byte{2}, 6, []byte("dest"), nil), @@ -401,27 +401,27 @@ func TestCheckBlankCheck(t *testing.T) { want error }{{ tx: &bc.TxData{ - Inputs: []*bc.TxInput{bc.NewSpendInput(bc.Hash{}, 0, nil, bc.AssetID{0}, 5, nil, nil)}, + Inputs: []*bc.TxInput{bc.NewSpendInput(bc.ComputeOutputID(bc.Hash{}, 0), nil, bc.AssetID{0}, 5, nil, nil)}, }, want: ErrBlankCheck, }, { tx: &bc.TxData{ - Inputs: []*bc.TxInput{bc.NewSpendInput(bc.Hash{}, 0, nil, bc.AssetID{0}, 5, nil, nil)}, + Inputs: []*bc.TxInput{bc.NewSpendInput(bc.ComputeOutputID(bc.Hash{}, 0), nil, bc.AssetID{0}, 5, nil, nil)}, Outputs: []*bc.TxOutput{bc.NewTxOutput(bc.AssetID{0}, 3, nil, nil)}, }, want: ErrBlankCheck, }, { tx: &bc.TxData{ Inputs: []*bc.TxInput{ - bc.NewSpendInput(bc.Hash{}, 0, nil, bc.AssetID{0}, 5, nil, nil), - bc.NewSpendInput(bc.Hash{}, 0, nil, bc.AssetID{1}, 5, nil, nil), + bc.NewSpendInput(bc.ComputeOutputID(bc.Hash{}, 0), nil, bc.AssetID{0}, 5, nil, nil), + bc.NewSpendInput(bc.ComputeOutputID(bc.Hash{}, 0), nil, bc.AssetID{1}, 5, nil, nil), }, Outputs: []*bc.TxOutput{bc.NewTxOutput(bc.AssetID{0}, 5, nil, nil)}, }, want: ErrBlankCheck, }, { tx: &bc.TxData{ - Inputs: []*bc.TxInput{bc.NewSpendInput(bc.Hash{}, 0, nil, bc.AssetID{0}, 5, nil, nil)}, + Inputs: []*bc.TxInput{bc.NewSpendInput(bc.ComputeOutputID(bc.Hash{}, 0), nil, bc.AssetID{0}, 5, nil, nil)}, Outputs: []*bc.TxOutput{ bc.NewTxOutput(bc.AssetID{0}, math.MaxInt64, nil, nil), bc.NewTxOutput(bc.AssetID{0}, 7, nil, nil), @@ -431,14 +431,14 @@ func TestCheckBlankCheck(t *testing.T) { }, { tx: &bc.TxData{ Inputs: []*bc.TxInput{ - bc.NewSpendInput(bc.Hash{}, 0, nil, bc.AssetID{0}, 5, nil, nil), - bc.NewSpendInput(bc.Hash{}, 0, nil, bc.AssetID{0}, math.MaxInt64, nil, nil), + bc.NewSpendInput(bc.ComputeOutputID(bc.Hash{}, 0), nil, bc.AssetID{0}, 5, nil, nil), + bc.NewSpendInput(bc.ComputeOutputID(bc.Hash{}, 0), nil, bc.AssetID{0}, math.MaxInt64, nil, nil), }, }, want: ErrBadAmount, }, { tx: &bc.TxData{ - Inputs: []*bc.TxInput{bc.NewSpendInput(bc.Hash{}, 0, nil, bc.AssetID{0}, 5, nil, nil)}, + Inputs: []*bc.TxInput{bc.NewSpendInput(bc.ComputeOutputID(bc.Hash{}, 0), nil, bc.AssetID{0}, 5, nil, nil)}, Outputs: []*bc.TxOutput{bc.NewTxOutput(bc.AssetID{0}, 5, nil, nil)}, }, want: nil, @@ -449,7 +449,7 @@ func TestCheckBlankCheck(t *testing.T) { want: nil, }, { tx: &bc.TxData{ - Inputs: []*bc.TxInput{bc.NewSpendInput(bc.Hash{}, 0, nil, bc.AssetID{0}, 5, nil, nil)}, + Inputs: []*bc.TxInput{bc.NewSpendInput(bc.ComputeOutputID(bc.Hash{}, 0), nil, bc.AssetID{0}, 5, nil, nil)}, Outputs: []*bc.TxOutput{bc.NewTxOutput(bc.AssetID{1}, 5, nil, nil)}, }, want: nil, diff --git a/core/txbuilder/witness.go b/core/txbuilder/witness.go index d30acd0c8f..e59333dc55 100644 --- a/core/txbuilder/witness.go +++ b/core/txbuilder/witness.go @@ -99,7 +99,7 @@ var ErrEmptyProgram = errors.New("empty signature program") // a program committing to aspects of the current // transaction. Specifically, the program commits to: // - the mintime and maxtime of the transaction (if non-zero) -// - the outpoint and (if non-empty) reference data of the current input +// - the outputID and (if non-empty) reference data of the current input // - the assetID, amount, control program, and (if non-empty) reference data of each output. func (sw *SignatureWitness) Sign(ctx context.Context, tpl *Template, index uint32, xpubs []chainkd.XPub, signFn SignFunc) error { // Compute the predicate to sign. This is either a @@ -167,7 +167,7 @@ func buildSigProgram(tpl *Template, index uint32) []byte { }) inp := tpl.Transaction.Inputs[index] if !inp.IsIssuance() { - constraints = append(constraints, outpointConstraint(inp.Outpoint())) + constraints = append(constraints, outpointConstraint(inp.SpentOutputID())) } // Commitment to the tx-level refdata is conditional on it being diff --git a/core/txbuilder/witness_test.go b/core/txbuilder/witness_test.go index dca3af886b..8415434350 100644 --- a/core/txbuilder/witness_test.go +++ b/core/txbuilder/witness_test.go @@ -17,7 +17,7 @@ func TestInferConstraints(t *testing.T) { tpl := &Template{ Transaction: &bc.TxData{ Inputs: []*bc.TxInput{ - bc.NewSpendInput(bc.Hash{}, 1, nil, bc.AssetID{}, 123, nil, []byte{1}), + bc.NewSpendInput(bc.ComputeOutputID(bc.Hash{}, 1), nil, bc.AssetID{}, 123, nil, []byte{1}), }, Outputs: []*bc.TxOutput{ bc.NewTxOutput(bc.AssetID{}, 123, []byte{10, 11, 12}, nil), @@ -28,7 +28,7 @@ func TestInferConstraints(t *testing.T) { AllowAdditional: true, } prog := buildSigProgram(tpl, 0) - want, err := vm.Assemble("MINTIME 1 GREATERTHANOREQUAL VERIFY MAXTIME 2 LESSTHANOREQUAL VERIFY 0x0000000000000000000000000000000000000000000000000000000000000000 1 OUTPOINT ROT NUMEQUAL VERIFY EQUAL VERIFY 0x2767f15c8af2f2c7225d5273fdd683edc714110a987d1054697c348aed4e6cc7 REFDATAHASH EQUAL VERIFY 0 0 123 0x0000000000000000000000000000000000000000000000000000000000000000 1 0x0a0b0c CHECKOUTPUT") + want, err := vm.Assemble("MINTIME 1 GREATERTHANOREQUAL VERIFY MAXTIME 2 LESSTHANOREQUAL VERIFY 0xaa206544e4e51017b313c228a4e8b42035bba61f8a8e87abd5e1135dc919fa7c OUTPUTID EQUAL VERIFY 0x2767f15c8af2f2c7225d5273fdd683edc714110a987d1054697c348aed4e6cc7 REFDATAHASH EQUAL VERIFY 0 0 123 0x0000000000000000000000000000000000000000000000000000000000000000 1 0x0a0b0c CHECKOUTPUT") if err != nil { t.Fatal(err) } diff --git a/dashboard/src/features/transactions/components/New/FormActionItem.jsx b/dashboard/src/features/transactions/components/New/FormActionItem.jsx index 374bff9da6..cdef5bbb84 100644 --- a/dashboard/src/features/transactions/components/New/FormActionItem.jsx +++ b/dashboard/src/features/transactions/components/New/FormActionItem.jsx @@ -29,7 +29,7 @@ const actionLabels = { const visibleFields = { [ISSUE_KEY]: {asset: true, amount: true}, [SPEND_ACCOUNT_KEY]: {asset: true, account: true, amount: true}, - [SPEND_UNSPENT_KEY]: {transaction_id: true, position: true}, + [SPEND_UNSPENT_KEY]: {output_id: true}, [CONTROL_ACCOUNT_KEY]: {asset: true, account: true, amount: true}, [CONTROL_PROGRAM_KEY]: {asset: true, control_program: true, amount: true}, [RETIRE_ASSET_KEY]: {asset: true, amount: true}, @@ -58,12 +58,11 @@ export default class ActionItem extends React.Component { render() { const { + id, type, account_id, account_alias, control_program, - transaction_id, - position, asset_id, asset_alias, amount, @@ -102,11 +101,8 @@ export default class ActionItem extends React.Component { {visible.control_program && } - {visible.transaction_id && - } - - {visible.position && - } + {visible.output_id && + } {visible.asset && - Transaction {item.transaction_id} - - Position {item.position} + ID {item.id} } actions={[ diff --git a/dashboard/src/features/unspents/reducers.js b/dashboard/src/features/unspents/reducers.js index 5aa45c4dd0..e37cbb4378 100644 --- a/dashboard/src/features/unspents/reducers.js +++ b/dashboard/src/features/unspents/reducers.js @@ -2,7 +2,7 @@ import { reducers } from 'features/shared' import { combineReducers } from 'redux' const type = 'unspent' -const idFunc = item => `${item.transaction_id}-${item.position}` +const idFunc = item => `${item.id}` export default combineReducers({ items: reducers.itemsReducer(type, idFunc), diff --git a/docs/core/build-applications/unspent-outputs.md b/docs/core/build-applications/unspent-outputs.md index 8faebc2c3a..6dbd8d723c 100644 --- a/docs/core/build-applications/unspent-outputs.md +++ b/docs/core/build-applications/unspent-outputs.md @@ -41,6 +41,7 @@ Given the following unspent output in Alice's account: ``` { + "id": "997de5f311df2828d64a62cda16425c2a957fd386ca2e332467c9a1ef98202cd", "transaction_id": "ad8e8aa37b0969ec60151674c821f819371152156782f107ed49724b8edd7b24", "position": 1, "asset_id": "d02e4a4c3b260ae47ba67278ef841bbad6903bda3bd307bee2843246dae07a2d", @@ -62,6 +63,7 @@ Given the following unspent output in Alice's account: ``` { + "id": "997de5f311df2828d64a62cda16425c2a957fd386ca2e332467c9a1ef98202cd", "transaction_id": "ad8e8aa37b0969ec60151674c821f819371152156782f107ed49724b8edd7b24", "position": 1, "asset_id": "d02e4a4c3b260ae47ba67278ef841bbad6903bda3bd307bee2843246dae07a2d", diff --git a/docs/core/examples/java/UnspentOutputs.java b/docs/core/examples/java/UnspentOutputs.java index 27c10b93ac..2b15542d68 100644 --- a/docs/core/examples/java/UnspentOutputs.java +++ b/docs/core/examples/java/UnspentOutputs.java @@ -72,6 +72,8 @@ public static void main(String[] args) throws Exception { } // endsnippet + // TODO: update API to include output IDs into Transaction.SubmitResponse, not just txid. + // Or have a client-side helper to compute OutputID from (txid,position) pair. String prevTransactionId = issuanceTx.id; // snippet build-transaction-all diff --git a/docs/core/reference/api-objects.md b/docs/core/reference/api-objects.md index 5f5f1ea637..b1b5405413 100644 --- a/docs/core/reference/api-objects.md +++ b/docs/core/reference/api-objects.md @@ -123,19 +123,13 @@ The following fields are present in the transaction object. Fields with **global | account_id | string | local | Locally unique identifier of the account spending the asset units. | | account_alias | string | local | User-supplied, locally unique identifier of the account spending the asset units. | | account_tags | string | local | Arbitrary, user-supplied, key-value data about the account spending the asset units. | -| spent_output | JSON object | global | The previous transaction output being spent in the input. | - -##### Spent Output - -| Field | Type | Visibility | Description | -|----------------|---------|------------|----------------------------------------------------------------------------------------------| -| transaction_id | string | global | Globally unique identifier of the previous transaction containing the output that was spent. | -| position | integer | global | The sequential number (in the previous transaction) of the output that was spent. | +| spent_output_id | string | global | The ID of the previous transaction output being spent in the input. | #### Output | Field | Type | Visibility | Description | |-----------------|-------------|------------|----------------------------------------------------------------------------------------------------------------------------------------------| +| id | string | global | The unique ID of the output. | | type | string | global | Type of output - either `control` or `retirement`. | | is_local | string | local | Denotes that the input involves the Core, either by: a) issuing units an asset created in the Core, b) spending from an account in the Core. | | purpose | string | local | Purpose of the output - either a) `receive` if used to receive asset units from another account or external party, or b) `change` if used to create change back to the account, when spending only a portion of the amount of an unspent output in a "spending" input.| @@ -162,7 +156,7 @@ The following fields are present in the transaction object. Fields with **global { "id": "C5D3F8...", "timestamp": "2015-12-30T00:02:23Z", - "block_id": "A83585...", + "block_id": "3d6732d...", "block_height": 100, "position": ..., // position in block "reference_data": {"deal_id": "..."}, @@ -170,7 +164,7 @@ The following fields are present in the transaction object. Fields with **global "inputs": [ { "action": "issue", - "asset_id": "125B4E...", + "asset_id": "125b4e...", "asset_alias": "...", "asset_tags": {}, "asset_is_local": <"yes"|"no"> @@ -182,15 +176,12 @@ The following fields are present in the transaction object. Fields with **global }, { "action": "spend", - "asset_id": "125B4E...", + "asset_id": "125b4e...", "asset_alias": "...", "asset_tags": {}, "asset_is_local": <"yes"|"no">, "amount": 5000, - "spent_output": { - "transaction_id": "94C5D3...", - "position": 1, - }, + "spent_output_id": "997de5...", "account_id": "", "account_alias": "...", "account_tags": {}, @@ -202,8 +193,9 @@ The following fields are present in the transaction object. Fields with **global { "action": "control", "purpose": <"change"|"receive">, // provided if the control program was generated locally + "id": "311df2...", "position": "...", - "asset_id": "125B4E...", + "asset_id": "125b4e...", "asset_alias": "...", "asset_tags": {}, "asset_is_local": <"yes"|"no">, @@ -217,8 +209,9 @@ The following fields are present in the transaction object. Fields with **global }, { "action": "retire", + "id": "2eb5cf...", "position": "...", - "asset_id": "125B4E...", + "asset_id": "125b4e...", "asset_alias": "...", "asset_tags": {}, "asset_is_local": <"yes"|"no">, @@ -242,6 +235,7 @@ The unspent output object is a subset of the [transaction object](#transaction). ``` { + "id": "2eb5cf...", "action": "control", "purpose": <"change"|"receive"> "transaction_id": "...", diff --git a/docs/internal/api-spec.swagger.yml b/docs/internal/api-spec.swagger.yml index 89bce60664..74f6acecef 100644 --- a/docs/internal/api-spec.swagger.yml +++ b/docs/internal/api-spec.swagger.yml @@ -356,8 +356,9 @@ definitions: amount: type: integer description: The amount of the asset being spent/issued. - spent_output: - $ref: '#/definitions/SpentOutput' + spent_output_id: + type: string + description: The unique ID of the output being spent. account_id: type: string description: The unique ID of the account. Only present if `SpentOutput` @@ -384,20 +385,6 @@ definitions: `asset_is_local` is "yes", OR if `type` is "spend" and `account_is_local` is "yes". "no" otherwise. - SpentOutput: - type: object - required: - - transaction_id - - position - properties: - transaction_id: - type: string - description: The unique ID of the transaction containing the output. - position: - type: integer - description: The position of the output within the containing - tranasction's outputs. - TransactionOutput: type: object required: @@ -419,6 +406,9 @@ definitions: position: type: integer description: The index of the output among the transaction's outputs. + id: + type: string + description: The unique ID of the transaction output. asset_id: type: string description: The unique ID of the asset being controlled or retired. @@ -630,8 +620,7 @@ definitions: type: object required: - type - - transaction_id - - position + - output_id properties: type: type: string @@ -639,14 +628,18 @@ definitions: (required to be `spend_account_unspent_output`) enum: - spend_account_unspent_output + output_id: + type: string + description: The unique ID of the transaction output being spent. + This field replaces fields transaction_id and position. transaction_id: type: string description: The unique ID of the transaction containing the output - being spent. + being spent. This field is supported, but deprecated. Use output_id instead. position: type: string description: The output's index relative to other outputs in the - containing transaction. + containing transaction. This field is supported, but deprecated. Use output_id instead. reference_data: type: object description: Arbitrary, immutable key/value data that will accompany diff --git a/docs/protocol/specifications/data.md b/docs/protocol/specifications/data.md index 305b61f8b6..4cab99b53f 100644 --- a/docs/protocol/specifications/data.md +++ b/docs/protocol/specifications/data.md @@ -25,7 +25,8 @@ * [Transaction Input Commitment](#transaction-input-commitment) * [Issuance Hash](#issuance-hash) * [Transaction Input Witness](#transaction-input-witness) - * [Outpoint](#outpoint) + * [Output ID](#output-id) + * [Unspent ID](#unspent-id) * [Transaction Output](#transaction-output) * [Transaction Output Commitment](#transaction-output-commitment) * [Transaction Output Witness](#transaction-output-witness) @@ -252,7 +253,7 @@ An asset version other than 1 is reserved for future expansion. Input commitment #### Asset Version 1 Issuance Commitment -Unlike spending commitments, each of which is unique because it references a distinct outpoint, issuance commitments are not intrinsically unique and must be made so to protect against replay attacks. The field *nonce* contains an arbitrary string that must be distinct from the nonces in other issuances of the same asset ID during the interval between the transaction's minimum and maximum time. Nodes ensure uniqueness of the issuance by remembering the [issuance hash](#issuance-hash) that includes the nonce, asset ID and minimum and maximum timestamps. To make sure that *issuance memory* does not take an unbounded amount of RAM, network enforces the *maximum issuance window* for these timestamps. +Unlike spending commitments, each of which is unique because it references a distinct [output ID](#output-id), issuance commitments are not intrinsically unique and must be made so to protect against replay attacks. The field *nonce* contains an arbitrary string that must be distinct from the nonces in other issuances of the same asset ID during the interval between the transaction's minimum and maximum time. Nodes ensure uniqueness of the issuance by remembering the [issuance hash](#issuance-hash) that includes the nonce, asset ID and minimum and maximum timestamps. To make sure that *issuance memory* does not take an unbounded amount of RAM, network enforces the *maximum issuance window* for these timestamps. If the transaction has another input that guarantees uniqueness of the entire transaction (e.g. a [spend input](#asset-version-1-spend-commitment)), then the issuance input must be able to opt out of the bounded minimum and maximum timestamps and therefore the uniqueness test for the [issuance hash](#issuance-hash). The empty nonce signals if the input opts out of the uniqueness checks. @@ -272,8 +273,8 @@ Amount | varint63 | Amount being issued. Field | Type | Description ----------------------|-----------------------|---------------------------------------------------------- Type | byte | Equals 0x01 indicating the “spend” type. -Outpoint Reference | [Outpoint](#outpoint) | [Transaction ID](#transaction-id) and index of the output being spent. -Output Commitment | [Output Commitment](#transaction-output-commitment) | Optional output commitment used as the source for this input. Presence of this field is controlled by [serialization flags](#transaction-serialization-flags): if switched off, this field is excluded from the spend entirely. +Output ID | sha3-256 | [Output ID](#output-id) that references an output being spent. +Output Commitment | [Output Commitment](#transaction-output-commitment) | Output commitment used as the source for this input or its [SHA3-256](#sha3) hash, depending on [serialization flags](#transaction-serialization-flags). — | — | Additional fields may be added by future extensions. @@ -324,17 +325,26 @@ Program Arguments | [varstring31] | [Signatures](#signature) and — | — | Additional fields may be added by future extensions. -### Outpoint +### Output ID -An *outpoint* uniquely specifies a single transaction output. +An *output ID* uniquely identifies a single transaction output. It is defined as SHA3-256 hash of the following structure: Field | Type | Description ------------------------|-------------------------|---------------------------------------------------------- Transaction ID | sha3-256 | [Transaction ID](#transaction-id) of the referenced transaction. Output Index | varint31 | Index (zero-based) of the [output](#transaction-output) within the transaction. -Note: In the transaction wire format, outpoint uses the [varint encoding](#varint31) for the output index, but in the [assets merkle tree](#assets-merkle-root) a fixed-length big-endian encoding is used for lexicographic ordering of unspent outputs. +### Unspent ID + +An *unspent ID* identifies a transaction output and commits to its contents. +It is only used to track the set of unspent outputs. +It is defined as SHA3-256 hash of the following structure: + +Field | Type | Description +------------------------|-------------------------|---------------------------------------------------------- +Output ID | sha3-256 | [Output ID](#output-id) of the output. +Output Commitment Hash | sha3-256 | SHA3-256 hash of the [output commitment](#transaction-output-commitment) in the specified output. ### Transaction Output @@ -380,7 +390,7 @@ Serialization flags control what and how data is encoded in a given *Transaction The **first (least significant) bit** indicates whether the transaction includes witness data. If set to zero, the input and output witness fields are absent. -The **second bit** indicates whether the output commitment from the spent output is present in the [input spend commitment](#asset-version-1-spend-commitment). If set to zero, the output commitment field is absent. +The **second bit** indicates whether the output commitment from the spent output is present in the [input spend commitment](#asset-version-1-spend-commitment). If set to zero, the output commitment field is replaced by the [SHA3-256](#sha3) hash of the output commitment (hashing raw bytes without length prefix). The **third bit** indicates whether transaction reference data and asset definitions are present. If set to zero, the reference data and asset definitions are replaced by their optional hash values. @@ -388,9 +398,9 @@ All three bits can be used independently. Non-zero **higher bits** are reserved Serialization Flags Examples | Description -----------------------------|--------------------------------------------------------------------------- -0000 0000 | Minimal serialization without witness and with hashes of reference data and asset definitions instead of their actual content. -0000 0011 | Minimal serialization needed for full verification. Contains witness fields and redundant [output commitment](#transaction-output-commitment), but with hashes of reference data and asset definitions instead of their actual content. -0000 0101 | Non-redundant full binary serialization with witness fields, reference data and asset definitions. +0000 0000 | Minimal serialization without witness and with hashes of previous output commitment, reference data and asset definitions instead of their actual content. +0000 0011 | Minimal serialization needed for full verification. Contains witness fields and [output commitment](#transaction-output-commitment), but with hashes of reference data and asset definitions instead of their actual content. +0000 0101 | Non-redundant binary serialization with witness fields, reference data and asset definitions, but with the hash of the output commitment instead of its actual content. ### Transaction ID @@ -427,12 +437,8 @@ The transaction signature hash is the [SHA3-256](#sha3) of the following structu Field | Type | Description ------------------------|-------------------------------------------|---------------------------------------------------------- Transaction ID | sha3-256 | Current [transaction ID](#transaction-id). -Input Index | varint31 | Index of the current input encoded as [varint31](#varint31). -Output Commitment Hash | sha3-256 | [SHA3-256](#sha3) of the output commitment from the output being spent by the current input. Issuance input uses a hash of an empty string. - -Note 1. Including the spent output commitment makes it easier to verify the asset ID and amount at signing time, although those values are already committed to via the input's [outpoint](#outpoint). +Input Index | varint31 | Index of a given input encoded as [varint31](#varint31). -Note 2. Using the hash of the output commitment instead of the output commitment as-is does not incur additional overhead since this hash is readily available from the [assets merkle tree](#assets-merkle-root). As a result, total amount of data to be hashed by all nodes during transaction validation is reduced. ### Program @@ -499,14 +505,12 @@ Root hash of the [merkle binary hash tree](#merkle-binary-tree) formed by the *t Root hash of the [merkle patricia tree](#merkle-patricia-tree) formed by unspent outputs with an **asset version 1** after applying the block. Allows bootstrapping nodes from recent blocks and an archived copy of the corresponding merkle patricia tree without processing all historical transactions. -The tree contains [non-retired](#retired-asset) unspent outputs (one or more per [asset ID](#asset-id)): +The tree contains [non-retired](#retired-asset) unspent outputs (one or more per [asset ID](#asset-id)) where both key and value are the same value — the [Unspent ID](#unspent-id) of the unspent output. Key | Value --------------------------|------------------------------ -`` | [SHA3-256](#sha3) of the [output commitment](#transaction-output-commitment) - +[Unspent ID](#unspent-id) | [Unspent ID](#unspent-id) -Note: unspent output indices are encoded with a fixed-length big-endian format to support lexicographic ordering. ### Merkle Root diff --git a/docs/protocol/specifications/validation.md b/docs/protocol/specifications/validation.md index 41d3743226..de4705f8af 100644 --- a/docs/protocol/specifications/validation.md +++ b/docs/protocol/specifications/validation.md @@ -46,7 +46,7 @@ A *blockchain state* comprises: * A [block header](data.md#block-header). * A *timestamp* equal to the timestamp in the [block header](data.md#block-header). -* The set of [non-retired](data.md#retired-asset) unspent outputs in the block’s [assets merkle tree](data.md#assets-merkle-root). +* The set of [unspent IDs](data.md#unspent-id) representing [non-retired](data.md#retired-asset) unspent outputs in the block’s [assets merkle tree](data.md#assets-merkle-root). * An *issuance memory*: a set of (issuance hash, expiration timestamp) pairs. It records recent issuance inputs in the state in order to detect duplicates. @@ -244,8 +244,8 @@ A transaction is said to be *valid* with respect to a particular blockchain stat 2. Compute [asset ID](data.md#asset-id) from the initial block ID, asset version 1, and the *VM version* and *issuance program* declared in the witness. If the resulting asset ID is not equal to the declared asset ID in the issuance commitment, halt and return false. 3. [Evaluate](#evaluate-predicate) its [issuance program](data.md#issuance-program), for the VM version specified in the issuance commitment and with the [input witness](data.md#transaction-input-witness) [program arguments](data.md#program-arguments); if execution fails, halt and return false. 2. If the input is a *spend*: - 1. Load an output from the state as identified by the input’s [spent output reference](data.md#outpoint), yielding a *previous output*. - 2. If the previous output does not exist, halt and return false. + 1. Compute the [unspent ID](data.md#unspent-id) using the input’s [output ID](data.md#output-id) and the previous output commitment. + 2. Check if the state contains the resulting unspent ID. If the unspent ID does not exist, halt and return false. 3. [Evaluate](#evaluate-predicate) the previous output’s control program, for the VM version specified in the previous output and with the [input witness](data.md#transaction-input-witness) program arguments. 4. If the evaluation returns false, halt and return false. 3. Return true. @@ -316,10 +316,10 @@ Note: requirement for the input and output sums to be below 263 impli **Algorithm:** 1. For each spend input with asset version 1 in the transaction: - 1. Delete the previous output (referenced by the input’s [outpoint](data.md#outpoint)) from S, yielding a new state S′. + 1. Delete the previous [unspent ID](data.md#unspent-id) from S, yielding a new state S′. 2. Replace S with S′. 2. For each output with asset version 1 in the transaction: - 1. Add that output to the unspent output set in S, yielding a new state S′. + 1. Add that output’s [unspent ID](data.md#unspent-id) to S, yielding a new state S′. 2. Replace S with S′. 3. For all [asset version 1 issuance inputs](data.md#asset-version-1-issuance-commitment) with non-empty *nonce* string: 1. Compute the [issuance hash](data.md#issuance-hash) H. diff --git a/docs/protocol/specifications/vm1.md b/docs/protocol/specifications/vm1.md index 4e0cb79ce3..e65bb12973 100644 --- a/docs/protocol/specifications/vm1.md +++ b/docs/protocol/specifications/vm1.md @@ -78,7 +78,7 @@ Execution of any of the following instructions results in immediate failure: * [TXREFDATAHASH](#txrefdatahash) * [REFDATAHASH](#refdatahash) * [INDEX](#index) -* [OUTPOINT](#outpoint) +* [OUTPUTID](#outputid) * [NONCE](#nonce) @@ -1207,13 +1207,13 @@ Pushes the index of the current input on the data stack. Fails if executed in the [block context](#block-context). -#### OUTPOINT +#### OUTPUTID Code | Stack Diagram | Cost ------|-----------------|----------------------------------------------------- -0xcb | (∅ → outpointtx outpointindex) | 1; [standard memory cost](#standard-memory-cost) +0xcb | (∅ → outputid) | 1; [standard memory cost](#standard-memory-cost) -Pushes the transaction ID and output index fields of the current input's [outpoint](#outpoint) on the data stack as separate items. The index is encoded as a [VM number](#vm-number). +Pushes the [output ID](data.md#output-id) on the data stack. Fails if the current input is an [issuance input](data.md#transaction-input-commitment). diff --git a/protocol/bc/hash.go b/protocol/bc/hash.go index a23ad33cbf..1263bb9b8c 100644 --- a/protocol/bc/hash.go +++ b/protocol/bc/hash.go @@ -70,6 +70,10 @@ func (h *Hash) UnmarshalJSON(b []byte) error { return h.UnmarshalText([]byte(*s)) } +func (h Hash) Bytes() []byte { + return h[:] +} + // Value satisfies the driver.Valuer interface func (h Hash) Value() (driver.Value, error) { return h[:], nil diff --git a/protocol/bc/outpoint.go b/protocol/bc/outpoint.go new file mode 100644 index 0000000000..a3c3d4e709 --- /dev/null +++ b/protocol/bc/outpoint.go @@ -0,0 +1,39 @@ +package bc + +import ( + "io" + "strconv" + + "chain/encoding/blockchain" +) + +// Outpoint is a raw txhash+index pointer to an output. +type Outpoint struct { + Hash Hash `json:"hash"` + Index uint32 `json:"index"` +} + +// String returns the Outpoint in the human-readable form "hash:index". +func (p Outpoint) String() string { + return p.Hash.String() + ":" + strconv.FormatUint(uint64(p.Index), 10) +} + +// WriteTo writes p to w. +func (p *Outpoint) WriteTo(w io.Writer) (int64, error) { + n, err := w.Write(p.Hash[:]) + if err != nil { + return int64(n), err + } + n2, err := blockchain.WriteVarint31(w, uint64(p.Index)) + return int64(n + n2), err +} + +func (p *Outpoint) readFrom(r io.Reader) (int, error) { + n1, err := io.ReadFull(r, p.Hash[:]) + if err != nil { + return n1, err + } + var n2 int + p.Index, n2, err = blockchain.ReadVarint31(r) + return n1 + n2, err +} diff --git a/protocol/bc/output_commitment.go b/protocol/bc/output_commitment.go index 13f591ce3b..2c383c7c5f 100644 --- a/protocol/bc/output_commitment.go +++ b/protocol/bc/output_commitment.go @@ -4,6 +4,7 @@ import ( "fmt" "io" + "chain/crypto/sha3pool" "chain/encoding/blockchain" "chain/errors" ) @@ -67,3 +68,11 @@ func (oc *OutputCommitment) readFrom(r io.Reader, assetVersion uint64) (suffix [ return nil }) } + +func (oc *OutputCommitment) Hash(suffix []byte, assetVersion uint64) (outputhash Hash) { + h := sha3pool.Get256() + defer sha3pool.Put256(h) + oc.writeExtensibleString(h, suffix, assetVersion) // TODO(oleg): get rid of this assetVersion parameter to actually write all the bytes + h.Read(outputhash[:]) + return outputhash +} diff --git a/protocol/bc/outputid.go b/protocol/bc/outputid.go new file mode 100644 index 0000000000..8db8556410 --- /dev/null +++ b/protocol/bc/outputid.go @@ -0,0 +1,44 @@ +package bc + +import ( + "io" + + "chain/crypto/sha3pool" + "chain/encoding/blockchain" +) + +// OutputID identifies previous transaction output in transaction inputs. +type OutputID struct{ Hash } + +// UnspentID identifies and commits to unspent output. +type UnspentID struct{ Hash } + +// ComputeOutputID computes the output ID defined by transaction hash and output index. +func ComputeOutputID(txHash Hash, outputIndex uint32) (oid OutputID) { + h := sha3pool.Get256() + defer sha3pool.Put256(h) + h.Write(txHash[:]) + blockchain.WriteVarint31(h, uint64(outputIndex)) + h.Read(oid.Hash[:]) + return oid +} + +// ComputeOutputID computes the output ID defined by transaction hash, output index and output hash. +func ComputeUnspentID(oid OutputID, outputHash Hash) (uid UnspentID) { + h := sha3pool.Get256() + defer sha3pool.Put256(h) + h.Write(oid.Hash[:]) + h.Write(outputHash[:]) + h.Read(uid.Hash[:]) + return uid +} + +// WriteTo writes p to w. +func (outid *OutputID) WriteTo(w io.Writer) (int64, error) { + n, err := w.Write(outid.Hash[:]) + return int64(n), err +} + +func (outid *OutputID) readFrom(r io.Reader) (int, error) { + return io.ReadFull(r, outid.Hash[:]) +} diff --git a/protocol/bc/sighasher.go b/protocol/bc/sighasher.go index 968b8ad458..5741e16981 100644 --- a/protocol/bc/sighasher.go +++ b/protocol/bc/sighasher.go @@ -1,8 +1,6 @@ package bc import ( - "bytes" - "chain/crypto/sha3pool" "chain/encoding/blockchain" ) @@ -26,19 +24,6 @@ func (s *SigHasher) Hash(idx uint32) Hash { h.Write((*s.txHash)[:]) blockchain.WriteVarint31(h, uint64(idx)) // TODO(bobg): check and return error - var outHash Hash - inp := s.txData.Inputs[idx] - if si, ok := inp.TypedInput.(*SpendInput); ok { - // inp is a spend - var ocBuf bytes.Buffer - si.OutputCommitment.writeContents(&ocBuf, si.OutputCommitmentSuffix, inp.AssetVersion) - sha3pool.Sum256(outHash[:], ocBuf.Bytes()) - } else { - // inp is an issuance - outHash = EmptyStringHash - } - - h.Write(outHash[:]) var hash Hash h.Read(hash[:]) sha3pool.Put256(h) diff --git a/protocol/bc/spend.go b/protocol/bc/spend.go index 224a384a37..985c36651e 100644 --- a/protocol/bc/spend.go +++ b/protocol/bc/spend.go @@ -3,7 +3,7 @@ package bc // SpendInput satisfies the TypedInput interface and represents a spend transaction. type SpendInput struct { // Commitment - Outpoint + SpentOutputID OutputID OutputCommitment // The unconsumed suffix of the output commitment @@ -15,24 +15,26 @@ type SpendInput struct { func (si *SpendInput) IsIssuance() bool { return false } -func NewSpendInput(txhash Hash, index uint32, arguments [][]byte, assetID AssetID, amount uint64, controlProgram, referenceData []byte) *TxInput { +func NewSpendInput(prevoutID OutputID, arguments [][]byte, assetID AssetID, amount uint64, controlProgram, referenceData []byte) *TxInput { + const ( + vmver = 1 + assetver = 1 + ) + oc := OutputCommitment{ + AssetAmount: AssetAmount{ + AssetID: assetID, + Amount: amount, + }, + VMVersion: vmver, + ControlProgram: controlProgram, + } return &TxInput{ - AssetVersion: 1, + AssetVersion: assetver, ReferenceData: referenceData, TypedInput: &SpendInput{ - Outpoint: Outpoint{ - Hash: txhash, - Index: index, - }, - OutputCommitment: OutputCommitment{ - AssetAmount: AssetAmount{ - AssetID: assetID, - Amount: amount, - }, - VMVersion: 1, - ControlProgram: controlProgram, - }, - Arguments: arguments, + SpentOutputID: prevoutID, + OutputCommitment: oc, + Arguments: arguments, }, } } diff --git a/protocol/bc/transaction.go b/protocol/bc/transaction.go index bff007c864..c90ceeb4d3 100644 --- a/protocol/bc/transaction.go +++ b/protocol/bc/transaction.go @@ -6,7 +6,6 @@ import ( "encoding/hex" "fmt" "io" - "strconv" "chain/crypto/sha3pool" "chain/encoding/blockchain" @@ -52,6 +51,7 @@ const ( // Bit mask for accepted serialization flags. // All other flag bits must be 0. + SerTxHash = 0x0 // this is used only for computing transaction hash - prevout and refdata are replaced with their hashes SerValid = 0x7 serRequired = 0x7 // we support only this combination of flags ) @@ -77,13 +77,6 @@ type TxData struct { ReferenceData []byte } -// Outpoint defines a bitcoin data type that is used to track previous -// transaction outputs. -type Outpoint struct { - Hash Hash `json:"hash"` - Index uint32 `json:"index"` -} - // HasIssuance returns true if this transaction has an issuance input. func (tx *TxData) HasIssuance() bool { for _, in := range tx.Inputs { @@ -189,22 +182,12 @@ func (tx *TxData) readCommonWitness(r io.Reader) error { return nil } -func (p *Outpoint) readFrom(r io.Reader) (int, error) { - n1, err := io.ReadFull(r, p.Hash[:]) - if err != nil { - return n1, err - } - var n2 int - p.Index, n2, err = blockchain.ReadVarint31(r) - return n1 + n2, err -} - // Hash computes the hash of the transaction with reference data fields // replaced by their hashes, // and stores the result in Hash. func (tx *TxData) Hash() Hash { h := sha3pool.Get256() - tx.writeTo(h, 0) // error is impossible + tx.writeTo(h, SerTxHash) // error is impossible var v Hash h.Read(v[:]) sha3pool.Put256(h) @@ -297,6 +280,14 @@ func (tx *TxData) HashForSig(idx uint32) Hash { return NewSigHasher(tx).Hash(idx) } +func (tx *Tx) OutputID(outputIndex uint32) OutputID { + return ComputeOutputID(tx.Hash, outputIndex) +} + +func (tx *TxData) OutputID(outputIndex uint32) OutputID { + return ComputeOutputID(tx.Hash(), outputIndex) +} + func (tx *TxData) MarshalText() ([]byte, error) { var buf bytes.Buffer tx.WriteTo(&buf) // error is impossible @@ -376,24 +367,9 @@ func (tx *TxData) writeCommonWitness(w io.Writer) error { return nil } -// String returns the Outpoint in the human-readable form "hash:index". -func (p Outpoint) String() string { - return p.Hash.String() + ":" + strconv.FormatUint(uint64(p.Index), 10) -} - -// WriteTo writes p to w. -func (p *Outpoint) WriteTo(w io.Writer) (int64, error) { - n, err := w.Write(p.Hash[:]) - if err != nil { - return int64(n), err - } - n2, err := blockchain.WriteVarint31(w, uint64(p.Index)) - return int64(n + n2), err -} - func writeRefData(w io.Writer, data []byte, serflags byte) error { if serflags&SerMetadata != 0 { - _, err := blockchain.WriteVarstr31(w, data) // TODO(bobg): check and return error + _, err := blockchain.WriteVarstr31(w, data) return err } return writeFastHash(w, data) diff --git a/protocol/bc/transaction_test.go b/protocol/bc/transaction_test.go index a8e098c75c..e167f43d77 100644 --- a/protocol/bc/transaction_test.go +++ b/protocol/bc/transaction_test.go @@ -101,7 +101,7 @@ func TestTransaction(t *testing.T) { tx: NewTx(TxData{ Version: 1, Inputs: []*TxInput{ - NewSpendInput(mustDecodeHash("dd385f6fe25d91d8c1bd0fa58951ad56b0c5229dcc01f61d9f9e8b9eb92d3292"), 0, nil, AssetID{}, 1000000000000, []byte{1}, []byte("input")), + NewSpendInput(ComputeOutputID(mustDecodeHash("dd385f6fe25d91d8c1bd0fa58951ad56b0c5229dcc01f61d9f9e8b9eb92d3292"), 0), nil, AssetID{}, 1000000000000, []byte{1}, []byte("input")), }, Outputs: []*TxOutput{ NewTxOutput(ComputeAssetID(issuanceScript, initialBlockHash, 1, EmptyStringHash), 600000000000, []byte{1}, nil), @@ -119,10 +119,9 @@ func TestTransaction(t *testing.T) { "00" + // common witness extensible string length "01" + // inputs count "01" + // input 0, asset version - "4c" + // input 0, input commitment length prefix + "4b" + // input 0, input commitment length prefix "01" + // input 0, input commitment, "spend" type - "dd385f6fe25d91d8c1bd0fa58951ad56b0c5229dcc01f61d9f9e8b9eb92d3292" + // input 0, spend input commitment, outpoint tx hash - "00" + // input 0, spend input commitment, outpoint index + "110bd1b4e5efc2994c9abc77f223a52c834d8f26b907c6c19d90b9e77a8e2fed" + // input 0, spend input commitment, output ID "29" + // input 0, spend input commitment, output commitment length prefix "0000000000000000000000000000000000000000000000000000000000000000" + // input 0, spend input commitment, output commitment, asset id "80a094a58d1d" + // input 0, spend input commitment, output commitment, amount @@ -149,11 +148,10 @@ func TestTransaction(t *testing.T) { "00" + // output 1, reference data "00" + // output 1, output witness "0c646973747269627574696f6e"), // reference data - hash: mustDecodeHash("7af758d0d27a7885cb65243164e2f8084b06aa1a647792cae028926b19324604"), - witnessHash: mustDecodeHash("b87695021b85c490334fdfc47864c85c5da387d3540ae9836c65096e83b0dfca"), + hash: mustDecodeHash("86556ca6f6181dbb71bcd3cba53fce825f39083c409b7c4afdf40c9912487113"), + witnessHash: mustDecodeHash("be907283e04ac6f365bc85af01b1a7945f89bfa4d77ca6429533cb11472322ab"), }, } - for i, test := range cases { got := serialize(t, test.tx) want, _ := hex.DecodeString(test.hex) @@ -206,7 +204,7 @@ func TestHasIssuance(t *testing.T) { }, { tx: &TxData{ Inputs: []*TxInput{ - NewSpendInput(Hash{}, 0, nil, AssetID{}, 0, nil, nil), + NewSpendInput(ComputeOutputID(Hash{}, 0), nil, AssetID{}, 0, nil, nil), NewIssuanceInput(nil, 0, nil, Hash{}, nil, nil, nil), }, }, @@ -214,7 +212,7 @@ func TestHasIssuance(t *testing.T) { }, { tx: &TxData{ Inputs: []*TxInput{ - NewSpendInput(Hash{}, 0, nil, AssetID{}, 0, nil, nil), + NewSpendInput(ComputeOutputID(Hash{}, 0), nil, AssetID{}, 0, nil, nil), }, }, want: false, @@ -308,8 +306,8 @@ func TestTxHashForSig(t *testing.T) { tx := &TxData{ Version: 1, Inputs: []*TxInput{ - NewSpendInput(mustDecodeHash("d250fa36f2813ddb8aed0fc66790ee58121bcbe88909bf88be12083d45320151"), 0, [][]byte{[]byte{1}}, AssetID{}, 0, nil, []byte("input1")), - NewSpendInput(mustDecodeHash("d250fa36f2813ddb8aed0fc66790ee58121bcbe88909bf88be12083d45320151"), 1, [][]byte{[]byte{2}}, AssetID{}, 0, nil, nil), + NewSpendInput(ComputeOutputID(mustDecodeHash("d250fa36f2813ddb8aed0fc66790ee58121bcbe88909bf88be12083d45320151"), 0), [][]byte{[]byte{1}}, AssetID{}, 0, nil, []byte("input1")), + NewSpendInput(ComputeOutputID(mustDecodeHash("d250fa36f2813ddb8aed0fc66790ee58121bcbe88909bf88be12083d45320151"), 1), [][]byte{[]byte{2}}, AssetID{}, 0, nil, nil), }, Outputs: []*TxOutput{ NewTxOutput(assetID, 1000000000000, []byte{3}, nil), @@ -320,8 +318,12 @@ func TestTxHashForSig(t *testing.T) { idx uint32 wantHash string }{ - {0, "e5dcd964d94ce4aa8bf99e73df22d81f6b28fd54f4c2cb3c409a4cc7240cab49"}, - {1, "0359c90d5038adb93df55955782d350f43808a4dbd298f238861b7b505521c4e"}, + // <------ 8fed9a267e73fcdf1c1f2fbcf6b90e2447dce5b8 + // {0, "e5dcd964d94ce4aa8bf99e73df22d81f6b28fd54f4c2cb3c409a4cc7240cab49"}, + // {1, "0359c90d5038adb93df55955782d350f43808a4dbd298f238861b7b505521c4e"}, + // ------- + {0, "aeee1f382653dfc09c89dcf18ad8b641ff6ab05450352798a16f0ef4a4026618"}, + {1, "c25e5b4bbf6e440525cda4a77ca374c9cca14d0918d4b4cc8635db9c5ee1f5f3"}, } sigHasher := NewSigHasher(tx) @@ -371,7 +373,7 @@ func BenchmarkTxWriteToFalse(b *testing.B) { func BenchmarkTxWriteToTrue200(b *testing.B) { tx := &Tx{} for i := 0; i < 200; i++ { - tx.Inputs = append(tx.Inputs, NewSpendInput(Hash{}, 0, nil, AssetID{}, 0, nil, nil)) + tx.Inputs = append(tx.Inputs, NewSpendInput(ComputeOutputID(Hash{}, 0), nil, AssetID{}, 0, nil, nil)) tx.Outputs = append(tx.Outputs, NewTxOutput(AssetID{}, 0, nil, nil)) } for i := 0; i < b.N; i++ { @@ -382,7 +384,7 @@ func BenchmarkTxWriteToTrue200(b *testing.B) { func BenchmarkTxWriteToFalse200(b *testing.B) { tx := &Tx{} for i := 0; i < 200; i++ { - tx.Inputs = append(tx.Inputs, NewSpendInput(Hash{}, 0, nil, AssetID{}, 0, nil, nil)) + tx.Inputs = append(tx.Inputs, NewSpendInput(ComputeOutputID(Hash{}, 0), nil, AssetID{}, 0, nil, nil)) tx.Outputs = append(tx.Outputs, NewTxOutput(AssetID{}, 0, nil, nil)) } for i := 0; i < b.N; i++ { @@ -391,7 +393,7 @@ func BenchmarkTxWriteToFalse200(b *testing.B) { } func BenchmarkTxInputWriteToTrue(b *testing.B) { - input := NewSpendInput(Hash{}, 0, nil, AssetID{}, 0, nil, nil) + input := NewSpendInput(ComputeOutputID(Hash{}, 0), nil, AssetID{}, 0, nil, nil) ew := errors.NewWriter(ioutil.Discard) for i := 0; i < b.N; i++ { input.writeTo(ew, 0) @@ -399,7 +401,7 @@ func BenchmarkTxInputWriteToTrue(b *testing.B) { } func BenchmarkTxInputWriteToFalse(b *testing.B) { - input := NewSpendInput(Hash{}, 0, nil, AssetID{}, 0, nil, nil) + input := NewSpendInput(ComputeOutputID(Hash{}, 0), nil, AssetID{}, 0, nil, nil) ew := errors.NewWriter(ioutil.Discard) for i := 0; i < b.N; i++ { input.writeTo(ew, serRequired) diff --git a/protocol/bc/txinput.go b/protocol/bc/txinput.go index eb3bc9e980..01c08a7f33 100644 --- a/protocol/bc/txinput.go +++ b/protocol/bc/txinput.go @@ -127,7 +127,7 @@ func (t *TxInput) readFrom(r io.Reader) (err error) { case 1: si = new(SpendInput) - _, err = si.Outpoint.readFrom(r) + _, err = si.SpentOutputID.readFrom(r) if err != nil { return err } @@ -208,7 +208,10 @@ func (t *TxInput) writeTo(w io.Writer, serflags uint8) error { return errors.Wrap(err, "writing asset version") } - _, err = blockchain.WriteExtensibleString(w, t.CommitmentSuffix, t.WriteInputCommitment) + _, err = blockchain.WriteExtensibleString(w, t.CommitmentSuffix, func(w io.Writer) error { + return t.WriteInputCommitment(w, serflags) + }) + if err != nil { return errors.Wrap(err, "writing input commitment") } @@ -228,7 +231,7 @@ func (t *TxInput) writeTo(w io.Writer, serflags uint8) error { return nil } -func (t *TxInput) WriteInputCommitment(w io.Writer) error { +func (t *TxInput) WriteInputCommitment(w io.Writer, serflags uint8) error { if t.AssetVersion == 1 { switch inp := t.TypedInput.(type) { case *IssuanceInput: @@ -253,11 +256,16 @@ func (t *TxInput) WriteInputCommitment(w io.Writer) error { if err != nil { return err } - _, err = inp.Outpoint.WriteTo(w) + _, err = inp.SpentOutputID.WriteTo(w) if err != nil { return err } - err = inp.OutputCommitment.writeExtensibleString(w, inp.OutputCommitmentSuffix, t.AssetVersion) + if serflags&SerPrevout != 0 { + err = inp.OutputCommitment.writeExtensibleString(w, inp.OutputCommitmentSuffix, t.AssetVersion) + } else { + prevouthash := inp.OutputCommitment.Hash(inp.OutputCommitmentSuffix, t.AssetVersion) + _, err = w.Write(prevouthash[:]) + } return err } } @@ -306,9 +314,16 @@ func (t *TxInput) witnessHash() (h Hash, err error) { return h, nil } -func (t *TxInput) Outpoint() (o Outpoint) { +func (t *TxInput) SpentOutputID() (o OutputID) { if si, ok := t.TypedInput.(*SpendInput); ok { - o = si.Outpoint + o = si.SpentOutputID } return o } + +func (t *TxInput) UnspentID() (u UnspentID) { + if si, ok := t.TypedInput.(*SpendInput); ok { + u = ComputeUnspentID(si.SpentOutputID, si.OutputCommitment.Hash(si.OutputCommitmentSuffix, t.AssetVersion)) + } + return u +} diff --git a/protocol/bc/txoutput.go b/protocol/bc/txoutput.go index 42da7a815e..af65bdaea2 100644 --- a/protocol/bc/txoutput.go +++ b/protocol/bc/txoutput.go @@ -89,3 +89,7 @@ func (to *TxOutput) witnessHash() Hash { func (to *TxOutput) WriteCommitment(w io.Writer) error { return to.OutputCommitment.writeExtensibleString(w, to.CommitmentSuffix, to.AssetVersion) } + +func (to *TxOutput) CommitmentHash() Hash { + return to.OutputCommitment.Hash(to.CommitmentSuffix, to.AssetVersion) +} diff --git a/protocol/block_test.go b/protocol/block_test.go index 3d2f3a2a9a..5a45e1fcb3 100644 --- a/protocol/block_test.go +++ b/protocol/block_test.go @@ -95,7 +95,7 @@ func TestWaitForBlockSoonWaits(t *testing.T) { makeEmptyBlock(t, c) // height=2 go func() { - time.Sleep(10 * time.Millisecond) // sorry for the slow test 😔 + time.Sleep(10 * time.Millisecond) // sorry for the slow test  makeEmptyBlock(t, c) // height=3 }() @@ -167,7 +167,7 @@ func TestGenerateBlock(t *testing.T) { // TODO(bobg): verify these hashes are correct var wantTxRoot, wantAssetsRoot bc.Hash copy(wantTxRoot[:], mustDecodeHex("2bf0254a214f4a675b3a7801974fe87c9db1827c2a8dbfd5778012084b3c0a8d")) - copy(wantAssetsRoot[:], mustDecodeHex("0feb95ec66c0b3931f1336cc52da01d46a5d0761c984346eafb812e10f129d0a")) + copy(wantAssetsRoot[:], mustDecodeHex("1593fdd753558fbcaff9ba7529579e5dd638139b6d25537e53e3310d47444946")) want := &bc.Block{ BlockHeader: bc.BlockHeader{ diff --git a/protocol/sort.go b/protocol/sort.go index 73c6a3d334..1c74f6be98 100644 --- a/protocol/sort.go +++ b/protocol/sort.go @@ -19,7 +19,7 @@ func topSort(txs []*bc.Tx) []*bc.Tx { if in.IsIssuance() { continue } - if prev := in.Outpoint().Hash; nodes[prev] != nil { + if prev := in.SpentOutputID().Hash; nodes[prev] != nil { if children[prev] == nil { children[prev] = make([]bc.Hash, 0, 1) } @@ -70,7 +70,7 @@ func isTopSorted(txs []*bc.Tx) bool { if in.IsIssuance() { continue } - h := in.Outpoint().Hash + h := in.SpentOutputID().Hash if exists[h] && !seen[h] { return false } diff --git a/protocol/state/outputs.go b/protocol/state/outputs.go index de50427121..6317243b99 100644 --- a/protocol/state/outputs.go +++ b/protocol/state/outputs.go @@ -1,24 +1,25 @@ package state import ( - "bytes" - - "chain/errors" "chain/protocol/bc" ) // Output represents a spent or unspent output // for the validation process. type Output struct { - bc.Outpoint + bc.OutputID bc.TxOutput } +func (o *Output) UnspentID() bc.UnspentID { + return bc.ComputeUnspentID(o.OutputID, o.TxOutput.CommitmentHash()) +} + // NewOutput creates a new Output. -func NewOutput(o bc.TxOutput, p bc.Outpoint) *Output { +func NewOutput(o bc.TxOutput, outid bc.OutputID) *Output { return &Output{ TxOutput: o, - Outpoint: p, + OutputID: outid, } } @@ -27,30 +28,21 @@ func NewOutput(o bc.TxOutput, p bc.Outpoint) *Output { // excludes reference data). func Prevout(in *bc.TxInput) *Output { assetAmount := in.AssetAmount() + // TODO(oleg): for new outputid we need to have correct output commitment, not reconstruct this here + // Also we do not care about all these, but only about UnspentID t := bc.NewTxOutput(assetAmount.AssetID, assetAmount.Amount, in.ControlProgram(), nil) return &Output{ - Outpoint: in.Outpoint(), + OutputID: in.SpentOutputID(), TxOutput: *t, } } -// OutputKey returns the key of an output in the state tree. -func OutputKey(o bc.Outpoint) (bkey []byte) { - var b bytes.Buffer - w := errors.NewWriter(&b) // used to satisfy interfaces - o.WriteTo(w) - return b.Bytes() -} - -func outputBytes(o *Output) []byte { - var b bytes.Buffer - o.WriteCommitment(&b) - return b.Bytes() -} - // OutputTreeItem returns the key of an output in the state tree, // as well as the output commitment (a second []byte) for Inserts // into the state tree. func OutputTreeItem(o *Output) (bkey, commitment []byte) { - return OutputKey(o.Outpoint), outputBytes(o) + // We implement the set of unspent IDs via Patricia Trie + // by having the leaf data being equal to keys. + key := o.UnspentID().Bytes() + return key, key } diff --git a/protocol/validation/merkle_test.go b/protocol/validation/merkle_test.go index 110ee2eca9..80571a5319 100644 --- a/protocol/validation/merkle_test.go +++ b/protocol/validation/merkle_test.go @@ -20,7 +20,7 @@ func TestCalcMerkleRoot(t *testing.T) { []byte("00000"), }, }, - want: mustParseHash("d03b89af9d6e6459fdfd4bee671751649eb5cefb15674c10b450253b47272437"), + want: mustParseHash("13eda1f33bd7ddce6256533374518b2c4d374b3d3608f7a9fcb61c1779b34bab"), }, { witnesses: [][][]byte{ [][]byte{ @@ -32,7 +32,7 @@ func TestCalcMerkleRoot(t *testing.T) { []byte("111111"), }, }, - want: mustParseHash("9a720394ced2aa3eaba4b3466352b8c95b994f57576e0392a7daa3bb220bb738"), + want: mustParseHash("dcb76dd841b1787ff3c6590b8bc7b0583b0f17be9661d1842b7c1eb88b9bef1e"), }, { witnesses: [][][]byte{ [][]byte{ @@ -45,7 +45,7 @@ func TestCalcMerkleRoot(t *testing.T) { []byte("222222"), }, }, - want: mustParseHash("4cfe2925e4dd79b96cab4c5c592e3f5a5be93f8aef74ea2a9bed085de991090d"), + want: mustParseHash("027bb007a4600d92c8133c54a386e36985b358c324992c1353bc1237170f8a88"), }} for _, c := range cases { diff --git a/protocol/validation/tx.go b/protocol/validation/tx.go index 5a639de4dc..f0a79519b9 100644 --- a/protocol/validation/tx.go +++ b/protocol/validation/tx.go @@ -111,7 +111,7 @@ func ConfirmTx(snapshot *state.Snapshot, initialBlockHash bc.Hash, block *bc.Blo // Lookup the prevout in the blockchain state tree. k, val := state.OutputTreeItem(state.Prevout(txin)) if !snapshot.Tree.Contains(k, val) { - return badTxErrf(errInvalidOutput, "output %s for input %d is invalid", txin.Outpoint().String(), i) + return badTxErrf(errInvalidOutput, "output %s for input %d is invalid", txin.SpentOutputID().String(), i) } } return nil @@ -205,7 +205,7 @@ func CheckTxWellFormed(tx *bc.Tx) error { } buf := new(bytes.Buffer) - txin.WriteInputCommitment(buf) + txin.WriteInputCommitment(buf, bc.SerTxHash) if inp, ok := commitments[string(buf.Bytes())]; ok { return badTxErrf(errDuplicateInput, "input %d is a duplicate of %d", i, inp) } @@ -277,9 +277,11 @@ func ApplyTx(snapshot *state.Snapshot, tx *bc.Tx) error { continue } + si := in.TypedInput.(*bc.SpendInput) + // Remove the consumed output from the state tree. - prevoutKey := state.OutputKey(in.Outpoint()) - err := snapshot.Tree.Delete(prevoutKey) + uid := bc.ComputeUnspentID(si.SpentOutputID, si.OutputCommitment.Hash(si.OutputCommitmentSuffix, in.AssetVersion)) + err := snapshot.Tree.Delete(uid.Bytes()) if err != nil { return err } @@ -290,7 +292,8 @@ func ApplyTx(snapshot *state.Snapshot, tx *bc.Tx) error { continue } // Insert new outputs into the state tree. - o := state.NewOutput(*out, bc.Outpoint{Hash: tx.Hash, Index: uint32(i)}) + o := state.NewOutput(*out, tx.OutputID(uint32(i))) + err := snapshot.Tree.Insert(state.OutputTreeItem(o)) if err != nil { return err diff --git a/protocol/validation/tx_test.go b/protocol/validation/tx_test.go index b7708bb25f..171e62760c 100644 --- a/protocol/validation/tx_test.go +++ b/protocol/validation/tx_test.go @@ -94,7 +94,7 @@ func TestUniqueIssuance(t *testing.T) { tx = bc.NewTx(bc.TxData{ Version: 1, Inputs: []*bc.TxInput{ - bc.NewSpendInput(tx.Hash, 0, nil, assetID, 1, trueProg, nil), + bc.NewSpendInput(bc.ComputeOutputID(tx.Hash, 0), nil, assetID, 1, trueProg, nil), issuance2Inp, }, Outputs: []*bc.TxOutput{ @@ -189,7 +189,7 @@ func TestTxWellFormed(t *testing.T) { tx: bc.TxData{ Version: 1, Inputs: []*bc.TxInput{ - bc.NewSpendInput(txhash1, 0, nil, aid1, 1000, nil, nil), + bc.NewSpendInput(bc.ComputeOutputID(txhash1, 0), nil, aid1, 1000, nil, nil), }, Outputs: []*bc.TxOutput{ bc.NewTxOutput(aid1, 999, nil, nil), @@ -201,8 +201,8 @@ func TestTxWellFormed(t *testing.T) { tx: bc.TxData{ Version: 1, Inputs: []*bc.TxInput{ - bc.NewSpendInput(txhash1, 0, nil, aid1, 500, nil, nil), - bc.NewSpendInput(txhash2, 0, nil, aid2, 500, nil, nil), + bc.NewSpendInput(bc.ComputeOutputID(txhash1, 0), nil, aid1, 500, nil, nil), + bc.NewSpendInput(bc.ComputeOutputID(txhash2, 0), nil, aid2, 500, nil, nil), }, Outputs: []*bc.TxOutput{ bc.NewTxOutput(aid1, 500, nil, nil), @@ -216,7 +216,7 @@ func TestTxWellFormed(t *testing.T) { Version: 1, Inputs: []*bc.TxInput{ bc.NewIssuanceInput(nil, 0, nil, initialBlockHash, issuanceProg, nil, nil), - bc.NewSpendInput(txhash1, 0, nil, aid2, 0, nil, nil), + bc.NewSpendInput(bc.ComputeOutputID(txhash1, 0), nil, aid2, 0, nil, nil), }, Outputs: []*bc.TxOutput{ bc.NewTxOutput(aid1, 0, nil, nil), @@ -228,7 +228,7 @@ func TestTxWellFormed(t *testing.T) { tx: bc.TxData{ Version: 1, Inputs: []*bc.TxInput{ - bc.NewSpendInput(bc.Hash{}, 0, nil, aid1, 1000, trueProg, nil), + bc.NewSpendInput(bc.ComputeOutputID(bc.Hash{}, 0), nil, aid1, 1000, trueProg, nil), }, Outputs: []*bc.TxOutput{ bc.NewTxOutput(aid1, 1000, nil, nil), @@ -239,8 +239,8 @@ func TestTxWellFormed(t *testing.T) { tx: bc.TxData{ Version: 1, Inputs: []*bc.TxInput{ - bc.NewSpendInput(txhash1, 0, nil, aid1, 500, trueProg, nil), - bc.NewSpendInput(txhash2, 0, nil, aid2, 500, trueProg, nil), + bc.NewSpendInput(bc.ComputeOutputID(txhash1, 0), nil, aid1, 500, trueProg, nil), + bc.NewSpendInput(bc.ComputeOutputID(txhash2, 0), nil, aid2, 500, trueProg, nil), }, Outputs: []*bc.TxOutput{ bc.NewTxOutput(aid1, 500, nil, nil), @@ -254,8 +254,8 @@ func TestTxWellFormed(t *testing.T) { tx: bc.TxData{ Version: 1, Inputs: []*bc.TxInput{ - bc.NewSpendInput(txhash1, 0, nil, aid1, 500, trueProg, nil), - bc.NewSpendInput(txhash2, 0, nil, aid1, 500, trueProg, nil), + bc.NewSpendInput(bc.ComputeOutputID(txhash1, 0), nil, aid1, 500, trueProg, nil), + bc.NewSpendInput(bc.ComputeOutputID(txhash2, 0), nil, aid1, 500, trueProg, nil), }, Outputs: []*bc.TxOutput{ bc.NewTxOutput(aid1, 1000, nil, nil), @@ -269,7 +269,7 @@ func TestTxWellFormed(t *testing.T) { MinTime: 2, MaxTime: 1, Inputs: []*bc.TxInput{ - bc.NewSpendInput(bc.Hash{}, 0, nil, aid1, 1000, nil, nil), + bc.NewSpendInput(bc.ComputeOutputID(bc.Hash{}, 0), nil, aid1, 1000, nil, nil), }, Outputs: []*bc.TxOutput{ bc.NewTxOutput(aid1, 1000, nil, nil), @@ -910,8 +910,6 @@ func TestValidateInvalidIssuances(t *testing.T) { func TestConfirmTx(t *testing.T) { txhash1 := bc.Hash{1} - outpoint1 := bc.Outpoint{Hash: txhash1} - trueProg := []byte{0x51} assetID1 := bc.AssetID{10} @@ -929,7 +927,8 @@ func TestConfirmTx(t *testing.T) { OutputCommitment: out1, } - stateout := state.NewOutput(txout, outpoint1) + outid1 := bc.ComputeOutputID(txhash1, 0) + stateout := state.NewOutput(txout, outid1) snapshot := state.Empty() err := snapshot.Tree.Insert(state.OutputTreeItem(stateout)) @@ -1037,7 +1036,7 @@ func TestConfirmTx(t *testing.T) { { AssetVersion: 1, TypedInput: &bc.SpendInput{ - Outpoint: outpoint1, + SpentOutputID: outid1, OutputCommitment: out1, }, }, diff --git a/protocol/vm/crypto_test.go b/protocol/vm/crypto_test.go index 1a342b2c87..2e00aded0d 100644 --- a/protocol/vm/crypto_test.go +++ b/protocol/vm/crypto_test.go @@ -86,7 +86,7 @@ func TestCheckSig(t *testing.T) { func TestCryptoOps(t *testing.T) { tx := bc.NewTx(bc.TxData{ - Inputs: []*bc.TxInput{bc.NewSpendInput(bc.Hash{}, 0, nil, bc.AssetID{}, 5, nil, nil)}, + Inputs: []*bc.TxInput{bc.NewSpendInput(bc.ComputeOutputID(bc.Hash{}, 0), nil, bc.AssetID{}, 5, nil, nil)}, Outputs: []*bc.TxOutput{}, }) @@ -406,8 +406,8 @@ func TestCryptoOps(t *testing.T) { runLimit: 49704, tx: tx, dataStack: [][]byte{{ - 151, 11, 184, 56, 238, 17, 17, 33, 167, 78, 153, 232, 136, 160, 210, 169, - 176, 155, 229, 51, 229, 132, 91, 197, 90, 229, 62, 139, 176, 182, 9, 100, + 208, 138, 179, 134, 79, 112, 134, 131, 128, 32, 188, 242, 102, 10, 17, 125, + 72, 88, 141, 164, 179, 39, 217, 24, 181, 96, 134, 174, 50, 132, 86, 192, }}, }, }, { diff --git a/protocol/vm/introspection.go b/protocol/vm/introspection.go index 79b856b2d1..11a4a195e2 100644 --- a/protocol/vm/introspection.go +++ b/protocol/vm/introspection.go @@ -193,7 +193,7 @@ func opIndex(vm *virtualMachine) error { return vm.pushInt64(int64(vm.inputIndex), true) } -func opOutpoint(vm *virtualMachine) error { +func opOutputID(vm *virtualMachine) error { if vm.tx == nil { return ErrContext } @@ -208,13 +208,9 @@ func opOutpoint(vm *virtualMachine) error { return err } - outpoint := txin.Outpoint() + outid := txin.SpentOutputID() - err = vm.push(outpoint.Hash[:], true) - if err != nil { - return err - } - return vm.pushInt64(int64(outpoint.Index), true) + return vm.push(outid.Bytes(), true) } func opNonce(vm *virtualMachine) error { diff --git a/protocol/vm/introspection_test.go b/protocol/vm/introspection_test.go index 62af2f6bd7..90a74fff31 100644 --- a/protocol/vm/introspection_test.go +++ b/protocol/vm/introspection_test.go @@ -87,12 +87,12 @@ func TestBlockTime(t *testing.T) { } } -func TestOutpointAndNonceOp(t *testing.T) { +func TestOutputIDAndNonceOp(t *testing.T) { var zeroHash bc.Hash nonce := []byte{36, 37, 38} tx := bc.NewTx(bc.TxData{ Inputs: []*bc.TxInput{ - bc.NewSpendInput(zeroHash, 0, nil, bc.AssetID{1}, 5, []byte("spendprog"), []byte("ref")), + bc.NewSpendInput(bc.ComputeOutputID(zeroHash, 0), nil, bc.AssetID{1}, 5, []byte("spendprog"), []byte("ref")), bc.NewIssuanceInput(nonce, 6, nil, zeroHash, []byte("issueprog"), nil, nil), }, }) @@ -100,13 +100,14 @@ func TestOutpointAndNonceOp(t *testing.T) { runLimit: 50000, tx: tx, inputIndex: 0, - program: []byte{uint8(OP_OUTPOINT)}, + program: []byte{uint8(OP_OUTPUTID)}, } err := vm.step() if err != nil { t.Fatal(err) } - expectedStack := [][]byte{zeroHash[:], []byte{}} + + expectedStack := [][]byte{mustDecodeHex("dc33296e4d20f0ef35ff9fd449e23ebbaa5a049a17779db3c2fe194b499aaf74")} if !testutil.DeepEqual(vm.dataStack, expectedStack) { t.Errorf("expected stack %v, got %v", expectedStack, vm.dataStack) } @@ -115,7 +116,7 @@ func TestOutpointAndNonceOp(t *testing.T) { runLimit: 50000, tx: tx, inputIndex: 1, - program: []byte{uint8(OP_OUTPOINT)}, + program: []byte{uint8(OP_OUTPUTID)}, } err = vm.step() if err != ErrContext { @@ -152,7 +153,7 @@ func TestIntrospectionOps(t *testing.T) { tx := bc.NewTx(bc.TxData{ ReferenceData: []byte("txref"), Inputs: []*bc.TxInput{ - bc.NewSpendInput(bc.Hash{}, 0, nil, bc.AssetID{1}, 5, []byte("spendprog"), []byte("ref")), + bc.NewSpendInput(bc.ComputeOutputID(bc.Hash{}, 0), nil, bc.AssetID{1}, 5, []byte("spendprog"), []byte("ref")), bc.NewIssuanceInput(nil, 6, nil, bc.Hash{}, []byte("issueprog"), nil, nil), }, Outputs: []*bc.TxOutput{ @@ -479,7 +480,7 @@ func TestIntrospectionOps(t *testing.T) { txops := []Op{ OP_CHECKOUTPUT, OP_ASSET, OP_AMOUNT, OP_PROGRAM, OP_MINTIME, OP_MAXTIME, OP_TXREFDATAHASH, OP_REFDATAHASH, - OP_INDEX, OP_OUTPOINT, + OP_INDEX, OP_OUTPUTID, } for _, op := range txops { diff --git a/protocol/vm/ops.go b/protocol/vm/ops.go index 9d40778255..35edf71166 100644 --- a/protocol/vm/ops.go +++ b/protocol/vm/ops.go @@ -210,7 +210,7 @@ const ( OP_TXREFDATAHASH Op = 0xc7 OP_REFDATAHASH Op = 0xc8 OP_INDEX Op = 0xc9 - OP_OUTPOINT Op = 0xcb + OP_OUTPUTID Op = 0xcb OP_NONCE Op = 0xcc OP_NEXTPROGRAM Op = 0xcd OP_BLOCKTIME Op = 0xce @@ -321,7 +321,7 @@ var ( OP_TXREFDATAHASH: {OP_TXREFDATAHASH, "TXREFDATAHASH", opTxRefDataHash}, OP_REFDATAHASH: {OP_REFDATAHASH, "REFDATAHASH", opRefDataHash}, OP_INDEX: {OP_INDEX, "INDEX", opIndex}, - OP_OUTPOINT: {OP_OUTPOINT, "OUTPOINT", opOutpoint}, + OP_OUTPUTID: {OP_OUTPUTID, "OUTPUTID", opOutputID}, OP_NONCE: {OP_NONCE, "NONCE", opNonce}, OP_NEXTPROGRAM: {OP_NEXTPROGRAM, "NEXTPROGRAM", opNextProgram}, OP_BLOCKTIME: {OP_BLOCKTIME, "BLOCKTIME", opBlockTime}, diff --git a/protocol/vm/vm_test.go b/protocol/vm/vm_test.go index 81492f2aca..d95899cfd2 100644 --- a/protocol/vm/vm_test.go +++ b/protocol/vm/vm_test.go @@ -181,8 +181,7 @@ func TestVerifyTxInput(t *testing.T) { wantErr error }{{ input: bc.NewSpendInput( - bc.Hash{}, - 0, + bc.OutputID{}, [][]byte{{2}, {3}}, bc.AssetID{}, 1, @@ -486,7 +485,7 @@ func TestVerifyTxInputQuickCheck(t *testing.T) { } }() tx := bc.NewTx(bc.TxData{ - Inputs: []*bc.TxInput{bc.NewSpendInput(bc.Hash{}, 0, witnesses, bc.AssetID{}, 10, program, nil)}, + Inputs: []*bc.TxInput{bc.NewSpendInput(bc.ComputeOutputID(bc.Hash{}, 0), witnesses, bc.AssetID{}, 10, program, nil)}, }) verifyTxInput(tx, 0) return true diff --git a/sdk/java/src/main/java/com/chain/api/Transaction.java b/sdk/java/src/main/java/com/chain/api/Transaction.java index b70bcb6969..35102f95fb 100644 --- a/sdk/java/src/main/java/com/chain/api/Transaction.java +++ b/sdk/java/src/main/java/com/chain/api/Transaction.java @@ -192,16 +192,16 @@ public static class Input { public long amount; /** - * The id of the account transferring the asset (possibly null if the input is an issuance or an unspent output is specified). + * The id of the output consumed by this input. Null if the input is an issuance. */ - @SerializedName("account_id") - public String accountId; + @SerializedName("spent_output_id") + public String spentOutputId; /** - * The output consumed by this input. Null if the input is an issuance. + * The id of the account transferring the asset (possibly null if the input is an issuance or an unspent output is specified). */ - @SerializedName("spent_output") - public OutputPointer spentOutput; + @SerializedName("account_id") + public String accountId; /** * The alias of the account transferring the asset (possibly null if the input is an issuance or an unspent output is specified). @@ -239,6 +239,12 @@ public static class Input { * A single output included in a transaction. */ public static class Output { + /** + * The id of the output. + */ + @SerializedName("id") + public String id; + /** * The type the output.
* Possible values are "control" and "retire". @@ -331,17 +337,6 @@ public static class Output { public String isLocal; } - /** - * An OutputPointer consists of a transaction ID and an output position, and - * uniquely identifies an output on the blockchain. - */ - public static class OutputPointer { - @SerializedName("transaction_id") - public String transactionId; - - public int position; - } - /** * A built transaction that has not been submitted for block inclusion (returned from {@link Transaction#buildBatch(Client, List)}). */ @@ -676,20 +671,29 @@ public SpendAccountUnspentOutput() { /** * Specifies the unspent output to be spent.
- * Either this or a combination of {@link SpendAccountUnspentOutput#setTransactionId(String)} - * and {@link SpendAccountUnspentOutput#setPosition(int)} must be called. + * Either this or {@link SpendAccountUnspentOutput#setOutputId(String)} must be called. * @param unspentOutput unspent output to be spent * @return updated action object */ public SpendAccountUnspentOutput setUnspentOutput(UnspentOutput unspentOutput) { - setTransactionId(unspentOutput.transactionId); - setPosition(unspentOutput.position); + setOutputId(unspentOutput.id); + return this; + } + + /** + * Specifies the output id of the unspent output to be spent.
+ * @param id + * @return + */ + public SpendAccountUnspentOutput setOutputId(String id) { + this.put("output_id", id); return this; } /** * Specifies the transaction id of the unspent output to be spent.
* Must be called with {@link SpendAccountUnspentOutput#setPosition(int)}. + * @deprecated This method is deprecated in favor of {@link SpendAccountUnspentOutput#setOutputId(String)}. * @param id * @return */ @@ -701,6 +705,7 @@ public SpendAccountUnspentOutput setTransactionId(String id) { /** * Specifies the position in the transaction of the unspent output to be spent.
* Must be called with {@link SpendAccountUnspentOutput#setTransactionId(String)}. + * @deprecated This method is deprecated in favor of {@link SpendAccountUnspentOutput#setOutputId(String)}. * @param pos * @return */ diff --git a/sdk/java/src/main/java/com/chain/api/UnspentOutput.java b/sdk/java/src/main/java/com/chain/api/UnspentOutput.java index f9ac955ca4..92d9d9be11 100644 --- a/sdk/java/src/main/java/com/chain/api/UnspentOutput.java +++ b/sdk/java/src/main/java/com/chain/api/UnspentOutput.java @@ -13,6 +13,12 @@ import java.util.Map; public class UnspentOutput { + /** + * The ID of the output. + */ + @SerializedName("id") + public String id; + /** * The type of action being taken on the output.
* Possible actions are "control_account", "control_program", and "retire". diff --git a/sdk/java/src/test/java/com/chain/integration/TransactionTest.java b/sdk/java/src/test/java/com/chain/integration/TransactionTest.java index 09dccd2d18..8be73356b2 100644 --- a/sdk/java/src/test/java/com/chain/integration/TransactionTest.java +++ b/sdk/java/src/test/java/com/chain/integration/TransactionTest.java @@ -102,7 +102,7 @@ public void testBasicTransaction() throws Exception { assertNull(input.accountAlias); assertNull(input.accountTags); assertEquals("yes", input.isLocal); - assertNull(input.spentOutput); + assertNull(input.spentOutputId); assertNotNull(input.issuanceProgram); assertNotNull(input.referenceData); @@ -152,9 +152,7 @@ public void testBasicTransaction() throws Exception { assertTrue(tx.inputs.size() > 0); input = tx.inputs.get(0); - assertNotNull(input.spentOutput); - assertNotNull(input.spentOutput.position); - assertNotNull(input.spentOutput.transactionId); + assertNotNull(input.spentOutputId); Transaction.Template retirement = new Transaction.Builder() @@ -521,10 +519,7 @@ public void testUnspentOutputs() throws Exception { Transaction.Template spending = new Transaction.Builder() - .addAction( - new Transaction.Action.SpendAccountUnspentOutput() - .setPosition(output.position) - .setTransactionId(resp.id)) + .addAction(new Transaction.Action.SpendAccountUnspentOutput().setOutputId(output.id)) .addAction( new Transaction.Action.ControlWithAccount() .setAccountAlias(bob) diff --git a/sdk/node/src/api/transactions.js b/sdk/node/src/api/transactions.js index a2e6bcd5a7..3b61efd41d 100644 --- a/sdk/node/src/api/transactions.js +++ b/sdk/node/src/api/transactions.js @@ -103,6 +103,16 @@ class TransactionBuilder { this.actions.push(Object.assign({}, params, {type: 'spend_account'})) } + /** + * Add an action that spends an unspent output. + * + * @param {Object} params - Action parameters. + * @param {String} params.outputId - ID of the transaction output to be spent. + */ + spendUnspentOutput(params) { + this.actions.push(Object.assign({}, params, {type: 'spend_account_unspent_output'})) + } + /** * Add an action that spends an unspent output. * @@ -112,7 +122,7 @@ class TransactionBuilder { * @param {Number} params.position - Position of the output within the * transaction to be spent. */ - spendUnspentOutput(params) { + spendUnspentOutputDeprecated(params) { this.actions.push(Object.assign({}, params, {type: 'spend_account_unspent_output'})) } diff --git a/sdk/ruby/lib/chain/transaction.rb b/sdk/ruby/lib/chain/transaction.rb index 30efb72e2c..4fca67043e 100755 --- a/sdk/ruby/lib/chain/transaction.rb +++ b/sdk/ruby/lib/chain/transaction.rb @@ -150,10 +150,10 @@ class Input < ResponseObject # @return [Integer] attrib :amount - # @!attribute [r] spent_output - # The output consumed by this input. - # @return [SpentOutput] - attrib(:spent_output) { |raw| SpentOutput.new(raw) } + # @!attribute [r] spent_output_id + # The id of the output consumed by this input. ID is nil if this is an issuance input. + # @return [String] + attrib :spent_output_id # @!attribute [r] account_id # The id of the account transferring the asset (possibly null if the @@ -196,21 +196,14 @@ class Input < ResponseObject # A flag indicating if the input is local. # @return [Boolean] attrib :is_local - - class SpentOutput < ResponseObject - # @!attribute [r] transaction_id - # Unique transaction identifier. - # @return [String] - attrib :transaction_id - - # @!attribute [r] position - # Position of an output within the transaction. - # @return [Integer] - attrib :position - end end class Output < ResponseObject + # @!attribute [r] id + # The id of the output. + # @return [String] + attrib :id + # @!attribute [r] type # The type of the output. # @@ -340,7 +333,7 @@ def to_json(opts = nil) to_h.to_json(opts) end - # Add an action to the tranasction builder + # Add an action to the transaction builder # @param [Hash] params Action parameters containing a type field and the # required parameters for that type # @return [Builder] @@ -394,13 +387,21 @@ def spend_from_account(params) # Add a spend action taken on a particular unspent output. # @param [Hash] params Action parameters - # @option params [String] :transaction_id Transaction ID specifying the tranasction to select an output from. - # @option params [Integer] :position Position of the output within the transaction to be spent. + # @option params [String] :output_id Output ID specifying the transaction output to spend. # @return [Builder] def spend_account_unspent_output(params) add_action(params.merge(type: :spend_account_unspent_output)) end + # Add a spend action taken on a particular unspent output. + # @param [Hash] params Action parameters + # @option params [String] :transaction_id Transaction ID specifying the transaction to select an output from. + # @option params [Integer] :position Position of the output within the transaction to be spent. + # @return [Builder] + def spend_account_unspent_output_deprecated(params) + add_action(params.merge(type: :spend_account_unspent_output)) + end + # Add a control action taken on a particular account. # @param [Hash] params Action parameters # @option params [String] :asset_id Asset ID specifiying the asset to be controlled. diff --git a/sdk/ruby/lib/chain/unspent_output.rb b/sdk/ruby/lib/chain/unspent_output.rb index f90e37972b..fd894fbbfb 100755 --- a/sdk/ruby/lib/chain/unspent_output.rb +++ b/sdk/ruby/lib/chain/unspent_output.rb @@ -4,6 +4,9 @@ module Chain class UnspentOutput < ResponseObject + # @!attribute [r] id + # @return [String] + attrib :id # @!attribute [r] type # @return [String]