mirror of
https://github.com/QuilibriumNetwork/ceremonyclient.git
synced 2026-02-21 18:37:26 +08:00
1604 lines
40 KiB
Go
1604 lines
40 KiB
Go
package token
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/sha512"
|
|
"encoding/binary"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"math/big"
|
|
"slices"
|
|
|
|
"github.com/iden3/go-iden3-crypto/poseidon"
|
|
"github.com/pkg/errors"
|
|
"golang.org/x/crypto/sha3"
|
|
hgstate "source.quilibrium.com/quilibrium/monorepo/node/execution/state/hypergraph"
|
|
"source.quilibrium.com/quilibrium/monorepo/types/crypto"
|
|
"source.quilibrium.com/quilibrium/monorepo/types/execution/intrinsics"
|
|
"source.quilibrium.com/quilibrium/monorepo/types/execution/state"
|
|
"source.quilibrium.com/quilibrium/monorepo/types/hypergraph"
|
|
"source.quilibrium.com/quilibrium/monorepo/types/keys"
|
|
"source.quilibrium.com/quilibrium/monorepo/types/schema"
|
|
qcrypto "source.quilibrium.com/quilibrium/monorepo/types/tries"
|
|
)
|
|
|
|
const FRAME_2_1_CUTOVER = 244200
|
|
const FRAME_2_1_EXTENDED_ENROLL_END = 255840
|
|
const FRAME_2_1_EXTENDED_ENROLL_CONFIRM_END = FRAME_2_1_EXTENDED_ENROLL_END + 6500
|
|
|
|
// used to skip frame-based checks, for tests
|
|
var BEHAVIOR_PASS = false
|
|
|
|
// using ed448 derivation process of seed = [57]byte{0x00..}
|
|
var publicReadKey, _ = hex.DecodeString("2cf07ca8d9ab1a4bb0902e25a9b90759dd54d881f54d52a76a17e79bf0361c325650f12746e4337ffb5940e7665ad7bf83f44af98d964bbe")
|
|
|
|
// TransactionInput is an input specific to the Transaction flow, where a token
|
|
// intrinsic is either configured as Acceptable, and the input is thus a
|
|
// pending:PendingTransaction, or not Acceptable, and the input is thus a
|
|
// coin:Coin.
|
|
type TransactionInput struct {
|
|
// Public input values:
|
|
|
|
// The constructed commitment to the input balance.
|
|
Commitment []byte
|
|
// The underlying signature authorizing spend and proving validity.
|
|
Signature []byte
|
|
// The proofs of various attributes of the token. Must verify against the
|
|
// transaction's multiproofs for full end-to-end verification.
|
|
Proofs [][]byte
|
|
|
|
// Private input values used for construction of public values:
|
|
|
|
// The address of the input value
|
|
address []byte
|
|
// The underlying input value
|
|
value *big.Int
|
|
// The signing operation which sets the signature, after the outputs are
|
|
// generated.
|
|
signOp func(transcript []byte) error
|
|
}
|
|
|
|
func NewTransactionInput(address []byte) (*TransactionInput, error) {
|
|
return &TransactionInput{
|
|
address: address, // buildutils:allow-slice-alias slice is static
|
|
}, nil
|
|
}
|
|
|
|
func (i *TransactionInput) Prove(tx *Transaction, index int) ([]byte, error) {
|
|
_, err := tx.hypergraph.GetVertex([64]byte(i.address))
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "prove input")
|
|
}
|
|
|
|
tree, err := tx.hypergraph.GetVertexData([64]byte(i.address))
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "prove input")
|
|
}
|
|
|
|
fnData, err := tree.Get([]byte{0})
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "prove input")
|
|
}
|
|
|
|
frameNumber := uint64(0)
|
|
if len(fnData) != 8 {
|
|
return nil, errors.Wrap(errors.New("invalid frame number"), "prove input")
|
|
} else {
|
|
frameNumber = binary.BigEndian.Uint64(fnData[:8])
|
|
}
|
|
|
|
var blind []byte
|
|
|
|
if bytes.Equal(i.address[:32], QUIL_TOKEN_ADDRESS) &&
|
|
frameNumber <= FRAME_2_1_EXTENDED_ENROLL_CONFIRM_END && !BEHAVIOR_PASS {
|
|
return nil, errors.Wrap(errors.New("invalid action"), "prove input")
|
|
}
|
|
|
|
coinTypeBI, err := poseidon.HashBytes(
|
|
slices.Concat(i.address[:32], []byte("coin:Coin")),
|
|
)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "prove input")
|
|
}
|
|
|
|
coinTypeBytes := coinTypeBI.FillBytes(make([]byte, 32))
|
|
|
|
pendingTypeBI, err := poseidon.HashBytes(
|
|
slices.Concat(i.address[:32], []byte("pending:PendingTransaction")),
|
|
)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "prove input")
|
|
}
|
|
|
|
pendingTypeBytes := pendingTypeBI.FillBytes(make([]byte, 32))
|
|
|
|
checkType, err := tree.Get(bytes.Repeat([]byte{0xff}, 32))
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "prove input")
|
|
}
|
|
|
|
commitmentIndex := 1 << 2
|
|
oneTimeKeyIndex := 2 << 2
|
|
keyImageIndex := 3 << 2
|
|
coinBalanceIndex := 4 << 2
|
|
blindIndex := 5 << 2
|
|
addRef1Index := 6 << 2
|
|
addRef2Index := 7 << 2
|
|
|
|
if !bytes.Equal(coinTypeBytes, checkType) &&
|
|
tx.config.Behavior&Acceptable == 0 {
|
|
return nil, errors.Wrap(
|
|
errors.New("invalid type for address"),
|
|
"prove input",
|
|
)
|
|
}
|
|
|
|
if tx.config.Behavior&Acceptable != 0 {
|
|
if !bytes.Equal(pendingTypeBytes, checkType) {
|
|
return nil, errors.Wrap(
|
|
errors.New(
|
|
fmt.Sprintf(
|
|
"invalid type for address: %x, expected %x",
|
|
checkType,
|
|
pendingTypeBytes,
|
|
),
|
|
),
|
|
"prove input",
|
|
)
|
|
}
|
|
|
|
commitmentIndex = 1 << 2
|
|
oneTimeKeyIndex = 2 << 2
|
|
keyImageIndex = 4 << 2
|
|
coinBalanceIndex = 6 << 2
|
|
blindIndex = 8 << 2
|
|
addRef1Index = 10 << 2
|
|
addRef2Index = 11 << 2
|
|
|
|
// R
|
|
oneTimeKey, err := tree.Get([]byte{byte(oneTimeKeyIndex)})
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "prove input")
|
|
}
|
|
|
|
// VK
|
|
possibleViewKey, err := tx.keyRing.GetAgreementKey(
|
|
"q-view-key",
|
|
i.address,
|
|
crypto.KeyTypeDecaf448,
|
|
)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "prove input")
|
|
}
|
|
|
|
viewKey, ok := possibleViewKey.(crypto.DecafAgreement)
|
|
if !ok {
|
|
return nil, errors.Wrap(errors.New("invalid view key"), "prove input")
|
|
}
|
|
|
|
// SK
|
|
possibleSpendKey, err := tx.keyRing.GetAgreementKey(
|
|
"q-spend-key",
|
|
i.address,
|
|
crypto.KeyTypeDecaf448,
|
|
)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "prove input")
|
|
}
|
|
|
|
spendKey, ok := possibleSpendKey.(crypto.DecafAgreement)
|
|
if !ok {
|
|
return nil, errors.Wrap(errors.New("invalid spend key"), "prove input")
|
|
}
|
|
|
|
// rVK
|
|
shared, err := viewKey.AgreeWithAndHashToScalar(oneTimeKey)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "prove input")
|
|
}
|
|
|
|
checkKeyImage, err := shared.Add(spendKey.Public())
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "prove input")
|
|
}
|
|
|
|
keyImage, err := tree.Get([]byte{byte(keyImageIndex)})
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "prove input")
|
|
}
|
|
|
|
if !bytes.Equal(checkKeyImage, keyImage) {
|
|
oneTimeKeyIndex = 3 << 2
|
|
keyImageIndex = 5 << 2
|
|
coinBalanceIndex = 7 << 2
|
|
blindIndex = 9 << 2
|
|
addRef1Index = 12 << 2
|
|
addRef2Index = 13 << 2
|
|
}
|
|
}
|
|
|
|
// C
|
|
commitment, err := tree.Get([]byte{byte(commitmentIndex)})
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "prove input")
|
|
}
|
|
|
|
// R
|
|
oneTimeKey, err := tree.Get([]byte{byte(oneTimeKeyIndex)})
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "prove input")
|
|
}
|
|
|
|
// VK
|
|
possibleViewKey, err := tx.keyRing.GetAgreementKey(
|
|
"q-view-key",
|
|
i.address,
|
|
crypto.KeyTypeDecaf448,
|
|
)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "prove input")
|
|
}
|
|
|
|
viewKey, ok := possibleViewKey.(crypto.DecafAgreement)
|
|
if !ok {
|
|
return nil, errors.Wrap(errors.New("invalid view key"), "prove input")
|
|
}
|
|
|
|
// SK
|
|
possibleSpendKey, err := tx.keyRing.GetAgreementKey(
|
|
"q-spend-key",
|
|
i.address,
|
|
crypto.KeyTypeDecaf448,
|
|
)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "prove input")
|
|
}
|
|
|
|
spendKey, ok := possibleSpendKey.(crypto.DecafAgreement)
|
|
if !ok {
|
|
return nil, errors.Wrap(errors.New("invalid spend key"), "prove input")
|
|
}
|
|
|
|
// rVK
|
|
shared, err := viewKey.AgreeWithAndHashToScalar(oneTimeKey)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "prove input")
|
|
}
|
|
|
|
coinBalanceData, err := tree.Get([]byte{byte(coinBalanceIndex)})
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "prove input")
|
|
}
|
|
|
|
blindData, err := tree.Get([]byte{byte(blindIndex)})
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "prove input")
|
|
}
|
|
|
|
blindMask := make([]byte, 56)
|
|
coinMask := make([]byte, 56)
|
|
shake := sha3.NewCShake256([]byte{}, []byte("blind"))
|
|
shake.Write(shared.Public())
|
|
shake.Read(blindMask)
|
|
|
|
shake = sha3.NewCShake256([]byte{}, []byte("coin"))
|
|
shake.Write(shared.Public())
|
|
shake.Read(coinMask)
|
|
|
|
for i := range blindMask {
|
|
blindData[i] ^= blindMask[i]
|
|
}
|
|
|
|
blind = blindData
|
|
|
|
balance := make([]byte, len(coinBalanceData))
|
|
|
|
for i := range coinBalanceData {
|
|
balance[len(balance)-i-1] = coinBalanceData[i] ^ coinMask[i]
|
|
coinBalanceData[i] ^= coinMask[i]
|
|
}
|
|
|
|
// If non-divisible, outputs should be aligned input-relative
|
|
if tx.config.Behavior&Divisible == 0 {
|
|
addRef, err := tree.Get([]byte{byte(addRef1Index)})
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "prove input")
|
|
}
|
|
addRefKey, err := tree.Get([]byte{byte(addRef2Index)})
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "prove input")
|
|
}
|
|
|
|
tx.Outputs[index].rawAdditionalReference = addRef
|
|
tx.Outputs[index].rawAdditionalReferenceKey = addRefKey
|
|
}
|
|
|
|
i.value = new(big.Int).SetBytes(balance)
|
|
i.signOp = func(transcript []byte) error {
|
|
i.Signature = tx.bulletproofProver.SignHidden(
|
|
shared.Private(),
|
|
spendKey.Private(),
|
|
transcript,
|
|
coinBalanceData,
|
|
blindData,
|
|
)
|
|
|
|
return nil
|
|
}
|
|
|
|
if tx.rdfMultiprover == nil || tx.rdfHypergraphSchema == "" {
|
|
return nil, errors.Wrap(
|
|
errors.New("RDF multiprover not available"),
|
|
"prove input",
|
|
)
|
|
}
|
|
|
|
if tx.config.Behavior&Acceptable == 0 {
|
|
// coin:Coin inputs
|
|
fields := []string{
|
|
"coin:Coin.Commitment",
|
|
"coin:Coin.VerificationKey",
|
|
}
|
|
if tx.config.Behavior&Divisible == 0 {
|
|
fields = append(
|
|
fields,
|
|
"coin:Coin.AdditionalReference",
|
|
"coin:Coin.AdditionalReferenceKey",
|
|
)
|
|
}
|
|
typeIndex := uint64(63)
|
|
multiproof, err := tx.rdfMultiprover.ProveWithType(
|
|
tx.rdfHypergraphSchema,
|
|
fields,
|
|
tree,
|
|
&typeIndex,
|
|
)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "prove input")
|
|
}
|
|
multiproofBytes, err := multiproof.ToBytes()
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "prove input")
|
|
}
|
|
i.Proofs = [][]byte{multiproofBytes}
|
|
if tx.config.Behavior&Divisible == 0 {
|
|
addref, err := tx.rdfMultiprover.Get(
|
|
tx.rdfHypergraphSchema,
|
|
"coin:Coin",
|
|
"AdditionalReference",
|
|
tree,
|
|
)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "prove input")
|
|
}
|
|
addrefkey, err := tx.rdfMultiprover.Get(
|
|
tx.rdfHypergraphSchema,
|
|
"coin:Coin",
|
|
"AdditionalReferenceKey",
|
|
tree,
|
|
)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "prove input")
|
|
}
|
|
i.Proofs = append(i.Proofs, slices.Concat(addref, addrefkey))
|
|
}
|
|
} else {
|
|
// pending:PendingTransaction inputs
|
|
fields := []string{
|
|
"pending:PendingTransaction.Commitment",
|
|
"pending:PendingTransaction.ToVerificationKey",
|
|
"pending:PendingTransaction.RefundVerificationKey",
|
|
}
|
|
|
|
if tx.config.Behavior&Divisible == 0 {
|
|
fields = append(
|
|
fields,
|
|
"pending:PendingTransaction.ToAdditionalReference",
|
|
"pending:PendingTransaction.ToAdditionalReferenceKey",
|
|
"pending:PendingTransaction.RefundAdditionalReference",
|
|
"pending:PendingTransaction.RefundAdditionalReferenceKey",
|
|
)
|
|
}
|
|
|
|
// Add expiration field if needed
|
|
if tx.config.Behavior&Expirable != 0 {
|
|
fields = append(fields, "pending:PendingTransaction.Expiration")
|
|
}
|
|
|
|
typeIndex := uint64(63)
|
|
multiproof, err := tx.rdfMultiprover.ProveWithType(
|
|
tx.rdfHypergraphSchema,
|
|
fields,
|
|
tree,
|
|
&typeIndex,
|
|
)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "prove input")
|
|
}
|
|
|
|
multiproofBytes, err := multiproof.ToBytes()
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "prove input")
|
|
}
|
|
i.Proofs = [][]byte{multiproofBytes}
|
|
|
|
// Get expiration value if needed
|
|
var exp []byte = nil
|
|
if tx.config.Behavior&Expirable != 0 {
|
|
exp, err = tx.rdfMultiprover.Get(
|
|
tx.rdfHypergraphSchema,
|
|
"pending:PendingTransaction",
|
|
"Expiration",
|
|
tree,
|
|
)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "prove input")
|
|
}
|
|
}
|
|
|
|
if exp != nil {
|
|
i.Proofs = append(i.Proofs, exp)
|
|
}
|
|
|
|
// To, so refund
|
|
if keyImageIndex>>2 == 4 {
|
|
refundImage, err := tree.Get([]byte{5 << 2})
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "prove input")
|
|
}
|
|
|
|
i.Proofs = append(i.Proofs, []byte{2}, refundImage)
|
|
} else { // Refund, so to
|
|
toImage, err := tree.Get([]byte{4 << 2})
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "prove input")
|
|
}
|
|
|
|
i.Proofs = append(i.Proofs, []byte{1}, toImage)
|
|
}
|
|
|
|
if tx.config.Behavior&Divisible == 0 {
|
|
addref, err := tx.rdfMultiprover.Get(
|
|
tx.rdfHypergraphSchema,
|
|
"pending:PendingTransaction",
|
|
"ToAdditionalReference",
|
|
tree,
|
|
)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "prove input")
|
|
}
|
|
addrefkey, err := tx.rdfMultiprover.Get(
|
|
tx.rdfHypergraphSchema,
|
|
"pending:PendingTransaction",
|
|
"ToAdditionalReferenceKey",
|
|
tree,
|
|
)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "prove input")
|
|
}
|
|
i.Proofs = append(i.Proofs, slices.Concat(addref, addrefkey))
|
|
}
|
|
}
|
|
|
|
i.Commitment = commitment
|
|
|
|
return blind, nil
|
|
}
|
|
|
|
// Verifies an input's signature, if a QUIL transaction, allows legacy
|
|
// verification. If invalid, has an associated error.
|
|
func (i *TransactionInput) Verify(
|
|
frameNumber uint64,
|
|
tx *Transaction,
|
|
transcript []byte,
|
|
checkLegacy bool,
|
|
txMultiproof *qcrypto.TraversalProof,
|
|
index int,
|
|
) (bool, error) {
|
|
if len(i.Commitment) != 56 {
|
|
return false, errors.Wrap(
|
|
errors.New("invalid commitment length"),
|
|
"verify input",
|
|
)
|
|
}
|
|
|
|
if len(i.Signature) != 336 {
|
|
return false, errors.Wrap(
|
|
errors.New("invalid signature length"),
|
|
"verify input",
|
|
)
|
|
}
|
|
|
|
if !bytes.Equal(i.Commitment, i.Signature[(56*5):(56*6)]) {
|
|
return false, errors.Wrap(
|
|
errors.New("invalid commitment"),
|
|
"verify input",
|
|
)
|
|
}
|
|
|
|
spendCheckBI, err := poseidon.HashBytes(i.Signature[56*4 : 56*5])
|
|
if err != nil {
|
|
return false, errors.Wrap(
|
|
errors.New("invalid address"),
|
|
"verify input",
|
|
)
|
|
}
|
|
|
|
// Key has been committed to hypergraph, Coin is already spent
|
|
if v, err := tx.hypergraph.GetVertex([64]byte(slices.Concat(
|
|
tx.Domain[:],
|
|
spendCheckBI.FillBytes(make([]byte, 32)),
|
|
))); err == nil && v != nil {
|
|
return false, errors.Wrap(err, "verify input")
|
|
}
|
|
|
|
addRefDelta := 0
|
|
if tx.config.Behavior&Divisible == 0 {
|
|
addRefDelta++
|
|
if len(i.Proofs[len(i.Proofs)-1]) != 64+56 {
|
|
return false, errors.Wrap(
|
|
errors.New("invalid proof"),
|
|
"verify input",
|
|
)
|
|
}
|
|
}
|
|
|
|
if len(i.Proofs) == 1+addRefDelta {
|
|
if tx.config.Behavior&Acceptable != 0 {
|
|
return false, errors.Wrap(
|
|
errors.New(fmt.Sprintf("invalid proof length: %d", len(i.Proofs))),
|
|
"verify input",
|
|
)
|
|
}
|
|
|
|
coinTypeBI, err := poseidon.HashBytes(
|
|
slices.Concat(tx.Domain[:], []byte("coin:Coin")),
|
|
)
|
|
if err != nil {
|
|
return false, errors.Wrap(err, "verify input")
|
|
}
|
|
|
|
inputs := [][]byte{
|
|
i.Signature[56*5 : 56*6],
|
|
i.Signature[56*4 : 56*5],
|
|
}
|
|
indices := []int{1, 3}
|
|
if tx.config.Behavior&Divisible == 0 {
|
|
inputs = append(inputs, i.Proofs[1][:64], i.Proofs[1][64:])
|
|
indices = append(indices, 6, 7)
|
|
}
|
|
indices = append(indices, 63)
|
|
|
|
coinTypeBytes := coinTypeBI.FillBytes(make([]byte, 32))
|
|
|
|
inputs = append(inputs, coinTypeBytes)
|
|
keys := make([][]byte, len(inputs))
|
|
keys[len(keys)-1] = bytes.Repeat([]byte{0xff}, 32)
|
|
if valid, err := i.verifyProof(
|
|
tx.hypergraph,
|
|
inputs,
|
|
i.Proofs[0],
|
|
txMultiproof,
|
|
indices,
|
|
keys,
|
|
index,
|
|
); err != nil || !valid {
|
|
return false, err
|
|
}
|
|
} else {
|
|
if tx.config.Behavior&Acceptable == 0 {
|
|
return false, errors.Wrap(
|
|
errors.New(fmt.Sprintf("invalid proof length: %d", len(i.Proofs))),
|
|
"verify input",
|
|
)
|
|
}
|
|
|
|
pendingTypeBI, err := poseidon.HashBytes(
|
|
slices.Concat(tx.Domain[:], []byte("pending:PendingTransaction")),
|
|
)
|
|
if err != nil {
|
|
return false, errors.Wrap(err, "verify input")
|
|
}
|
|
|
|
pendingTypeBytes := pendingTypeBI.FillBytes(make([]byte, 32))
|
|
|
|
indices := []int{1, 4, 5}
|
|
data := [][]byte{i.Signature[56*5 : 56*6]}
|
|
|
|
offset := 0
|
|
expiration := uint64(0)
|
|
if tx.config.Behavior&Expirable != 0 {
|
|
offset = 1
|
|
proofIndex := 10
|
|
if len(i.Proofs) != 4+addRefDelta {
|
|
return false, errors.Wrap(
|
|
errors.New(fmt.Sprintf("invalid proof length: %d", len(i.Proofs))),
|
|
"verify input",
|
|
)
|
|
}
|
|
if tx.config.Behavior&Divisible == 0 {
|
|
indices = append(indices, 10, 11, 12, 13)
|
|
proofIndex = 14
|
|
}
|
|
|
|
expiration = binary.BigEndian.Uint64(i.Proofs[1])
|
|
indices = append(indices, proofIndex)
|
|
} else {
|
|
if len(i.Proofs) != 3+addRefDelta {
|
|
return false, errors.Wrap(
|
|
errors.New(fmt.Sprintf("invalid proof length: %d", len(i.Proofs))),
|
|
"verify input",
|
|
)
|
|
}
|
|
}
|
|
|
|
altCheckBI, err := poseidon.HashBytes(i.Proofs[offset+2])
|
|
if err != nil {
|
|
return false, errors.Wrap(
|
|
errors.New("invalid address"),
|
|
"verify input",
|
|
)
|
|
}
|
|
|
|
// Key has been committed to hypergraph, Pending is already spent
|
|
if v, err := tx.hypergraph.GetVertex([64]byte(slices.Concat(
|
|
tx.Domain[:],
|
|
altCheckBI.FillBytes(make([]byte, 32)),
|
|
))); err == nil && v != nil {
|
|
return false, errors.Wrap(err, "verify input")
|
|
}
|
|
|
|
isTo := bytes.Equal(i.Proofs[offset+1], []byte{2})
|
|
|
|
if isTo {
|
|
data = append(data, i.Signature[56*4:56*5], i.Proofs[offset+2])
|
|
} else {
|
|
if frameNumber < expiration {
|
|
return false, errors.Wrap(
|
|
errors.New("not expired"),
|
|
"verify input",
|
|
)
|
|
}
|
|
|
|
data = append(data, i.Proofs[offset+2], i.Signature[56*4:56*5])
|
|
}
|
|
|
|
if tx.config.Behavior&Divisible == 0 {
|
|
data = append(
|
|
data,
|
|
i.Proofs[offset+3][:64],
|
|
i.Proofs[offset+3][64:],
|
|
i.Proofs[offset+3][:64],
|
|
i.Proofs[offset+3][64:],
|
|
)
|
|
}
|
|
|
|
if tx.config.Behavior&Expirable != 0 {
|
|
data = append(data, i.Proofs[1])
|
|
}
|
|
data = append(data, pendingTypeBytes)
|
|
|
|
indices = append(indices, 63)
|
|
keys := [][]byte{}
|
|
for i := 0; i < len(data)-1; i++ {
|
|
keys = append(keys, nil)
|
|
}
|
|
keys = append(keys, bytes.Repeat([]byte{0xff}, 32))
|
|
|
|
if valid, err := i.verifyProof(
|
|
tx.hypergraph,
|
|
data,
|
|
i.Proofs[0],
|
|
txMultiproof,
|
|
indices,
|
|
keys,
|
|
index,
|
|
); err != nil || !valid {
|
|
return false, errors.Wrap(err, "verify input")
|
|
}
|
|
}
|
|
|
|
return tx.bulletproofProver.VerifyHidden(
|
|
i.Signature[(56*0):(56*1)],
|
|
transcript,
|
|
i.Signature[(56*1):(56*2)],
|
|
i.Signature[(56*2):(56*3)],
|
|
i.Signature[(56*3):(56*4)],
|
|
i.Signature[(56*4):(56*5)],
|
|
i.Signature[(56*5):(56*6)],
|
|
), nil
|
|
}
|
|
|
|
func (i *TransactionInput) verifyProof(
|
|
hg hypergraph.Hypergraph,
|
|
data [][]byte,
|
|
proof []byte,
|
|
txMultiproof *qcrypto.TraversalProof,
|
|
indices []int,
|
|
keys [][]byte,
|
|
index int,
|
|
) (bool, error) {
|
|
commits := [][]byte{} // same value, but needs to repeat
|
|
evaluations := [][]byte{}
|
|
uindices := []uint64{}
|
|
for i, d := range data {
|
|
h := sha512.New()
|
|
h.Write([]byte{0})
|
|
if keys[i] == nil {
|
|
h.Write([]byte{byte(indices[i]) << 2})
|
|
} else {
|
|
h.Write(keys[i])
|
|
}
|
|
h.Write(d)
|
|
out := h.Sum(nil)
|
|
evaluations = append(evaluations, out)
|
|
commits = append(
|
|
commits,
|
|
txMultiproof.SubProofs[index].Ys[len(txMultiproof.SubProofs[index].Ys)-1],
|
|
)
|
|
uindices = append(uindices, uint64(indices[i]))
|
|
}
|
|
|
|
mp := hg.GetProver().NewMultiproof()
|
|
if err := mp.FromBytes(proof); err != nil {
|
|
return false, errors.Wrap(err, "verify proof")
|
|
}
|
|
|
|
if valid := hg.GetProver().VerifyMultiple(
|
|
commits,
|
|
evaluations,
|
|
uindices,
|
|
64,
|
|
mp.GetMulticommitment(),
|
|
mp.GetProof(),
|
|
); !valid {
|
|
return false, errors.Wrap(
|
|
errors.New("invalid proof"),
|
|
"verify input",
|
|
)
|
|
}
|
|
|
|
return true, nil
|
|
}
|
|
|
|
// RecipientBundle is the bundle of values that represents a recipient's output.
|
|
type RecipientBundle struct {
|
|
// Public output values:
|
|
|
|
// The public key used to derive shared secret for mask and balance.
|
|
OneTimeKey []byte // Raw pubkey value is stored
|
|
// The public key used to verify spend output.
|
|
VerificationKey []byte // Raw pubkey value is stored
|
|
// The encrypted underlying masked balance of the coin.
|
|
CoinBalance []byte
|
|
// The encrypted mask.
|
|
Mask []byte
|
|
// The address value optionally used for non-divisible/interchangeable units.
|
|
AdditionalReference []byte
|
|
// The key associated with the AdditionalReference, present if
|
|
// AdditionalReference is present
|
|
AdditionalReferenceKey []byte
|
|
|
|
// Private output values use for construction of public values:
|
|
|
|
// The public view key of the recipient
|
|
recipientView []byte
|
|
// The public spend key of the recipient
|
|
recipientSpend []byte
|
|
}
|
|
|
|
// TransactionOutput is the output specific to a Transaction. When encoded as
|
|
// the finalized state of the token intrinsic operation, produces a coin:Coin.
|
|
type TransactionOutput struct {
|
|
// Public output values:
|
|
|
|
// The frame number this output is created on
|
|
FrameNumber []byte
|
|
// The commitment to the balance
|
|
Commitment []byte // Raw commitment value is stored
|
|
// The output entries corresponding to the serialized coin:Coin
|
|
RecipientOutput RecipientBundle
|
|
|
|
// Private output values use for construction of public values:
|
|
|
|
// The underlying quantity used to generate the output
|
|
value *big.Int
|
|
// The underlying raw additionalReference value mapped from input, if present
|
|
rawAdditionalReference []byte
|
|
// The underlying raw additionalReferenceKey value mapped from input, if
|
|
// present
|
|
rawAdditionalReferenceKey []byte
|
|
}
|
|
|
|
func NewTransactionOutput(
|
|
value *big.Int,
|
|
recipientViewPubkey []byte,
|
|
recipientSpendPubkey []byte,
|
|
) (*TransactionOutput, error) {
|
|
return &TransactionOutput{
|
|
value: value,
|
|
RecipientOutput: RecipientBundle{
|
|
recipientView: recipientViewPubkey, // buildutils:allow-slice-alias slice is static
|
|
recipientSpend: recipientSpendPubkey, // buildutils:allow-slice-alias slice is static
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
func (o *TransactionOutput) Prove(
|
|
res crypto.RangeProofResult,
|
|
index int,
|
|
tx *Transaction,
|
|
frameNumber uint64,
|
|
) error {
|
|
o.Commitment = res.Commitment[index*56 : (index+1)*56]
|
|
blind := slices.Clone(res.Blinding[index*56 : (index+1)*56])
|
|
r, err := tx.decafConstructor.New()
|
|
if err != nil {
|
|
return errors.Wrap(err, "prove output")
|
|
}
|
|
|
|
shared, err := r.AgreeWithAndHashToScalar(o.RecipientOutput.recipientView)
|
|
if err != nil {
|
|
return errors.Wrap(err, "prove output")
|
|
}
|
|
|
|
blindMask := make([]byte, 56)
|
|
coinMask := make([]byte, 56)
|
|
shake := sha3.NewCShake256([]byte{}, []byte("blind"))
|
|
shake.Write(shared.Public())
|
|
shake.Read(blindMask)
|
|
|
|
shake = sha3.NewCShake256([]byte{}, []byte("coin"))
|
|
shake.Write(shared.Public())
|
|
shake.Read(coinMask)
|
|
|
|
for i := range blindMask {
|
|
blind[i] ^= blindMask[i]
|
|
}
|
|
|
|
rawBalance := o.value.FillBytes(make([]byte, 56))
|
|
slices.Reverse(rawBalance)
|
|
|
|
for i := range rawBalance {
|
|
rawBalance[i] = rawBalance[i] ^ coinMask[i]
|
|
}
|
|
|
|
o.RecipientOutput.Mask = blind
|
|
o.RecipientOutput.CoinBalance = rawBalance
|
|
|
|
o.RecipientOutput.OneTimeKey = r.Public()
|
|
o.RecipientOutput.VerificationKey, err = shared.Add(
|
|
o.RecipientOutput.recipientSpend,
|
|
)
|
|
if err != nil {
|
|
return errors.Wrap(err, "prove output")
|
|
}
|
|
|
|
// TODO(2.1.1+): there's some other options we can pursue here, leaving this
|
|
// section a bare copy until then.
|
|
if tx.config.Behavior&Divisible == 0 {
|
|
addRef := slices.Clone(tx.Outputs[index].rawAdditionalReference)
|
|
addRefKey := slices.Clone(tx.Outputs[index].rawAdditionalReferenceKey)
|
|
|
|
tx.Outputs[index].RecipientOutput.AdditionalReference = addRef
|
|
tx.Outputs[index].RecipientOutput.AdditionalReferenceKey = addRefKey
|
|
}
|
|
|
|
o.FrameNumber = binary.BigEndian.AppendUint64(nil, frameNumber)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (o *TransactionOutput) Verify(
|
|
frameNumber uint64,
|
|
config *TokenIntrinsicConfiguration,
|
|
) (bool, error) {
|
|
if frameNumber <= binary.BigEndian.Uint64(o.FrameNumber) {
|
|
return false, errors.Wrap(
|
|
errors.New("invalid frame number"),
|
|
"verify output",
|
|
)
|
|
}
|
|
|
|
if len(o.Commitment) != 56 ||
|
|
len(o.RecipientOutput.VerificationKey) != 56 ||
|
|
len(o.RecipientOutput.CoinBalance) != 56 {
|
|
return false, errors.Wrap(
|
|
errors.New("invalid commitment, verification key, or coin balance"),
|
|
"verify output",
|
|
)
|
|
}
|
|
|
|
if config.Behavior&Divisible == 0 {
|
|
if len(o.RecipientOutput.AdditionalReference) != 64 {
|
|
return false, errors.Wrap(
|
|
errors.New("missing additional reference for non-divisible coin"),
|
|
"verify output",
|
|
)
|
|
}
|
|
|
|
if len(o.RecipientOutput.AdditionalReferenceKey) != 56 {
|
|
return false, errors.Wrap(
|
|
errors.New("missing additional reference key for non-divisible coin"),
|
|
"verify output",
|
|
)
|
|
}
|
|
}
|
|
|
|
if len(o.RecipientOutput.Mask) != 56 {
|
|
return false, errors.Wrap(errors.New("missing mask"), "verify output")
|
|
}
|
|
|
|
return true, nil
|
|
}
|
|
|
|
// Transaction defines the intrinsic execution for converting a collection of
|
|
// coin:Coin or pending:PendingTransaction inputs into coin:Coin outputs,
|
|
// depending on configuration. If the token has Acceptable flows enabled,
|
|
// expects inputs of type pending:PendingTransaction in configuration, otherwise
|
|
// expects coin:Coin inputs.
|
|
type Transaction struct {
|
|
Domain [32]byte
|
|
Inputs []*TransactionInput
|
|
Outputs []*TransactionOutput
|
|
Fees []*big.Int
|
|
RangeProof []byte
|
|
TraversalProof *qcrypto.TraversalProof
|
|
hypergraph hypergraph.Hypergraph
|
|
bulletproofProver crypto.BulletproofProver
|
|
inclusionProver crypto.InclusionProver
|
|
verEnc crypto.VerifiableEncryptor
|
|
decafConstructor crypto.DecafConstructor
|
|
keyRing keys.KeyRing
|
|
config *TokenIntrinsicConfiguration
|
|
rdfHypergraphSchema string
|
|
rdfMultiprover *schema.RDFMultiprover
|
|
}
|
|
|
|
func NewTransaction(
|
|
domain [32]byte,
|
|
inputs []*TransactionInput,
|
|
outputs []*TransactionOutput,
|
|
fees []*big.Int,
|
|
config *TokenIntrinsicConfiguration,
|
|
hypergraph hypergraph.Hypergraph,
|
|
bulletproofProver crypto.BulletproofProver,
|
|
inclusionProver crypto.InclusionProver,
|
|
verEnc crypto.VerifiableEncryptor,
|
|
decafConstructor crypto.DecafConstructor,
|
|
keyRing keys.KeyRing,
|
|
rdfHypergraphSchema string,
|
|
rdfMultiprover *schema.RDFMultiprover,
|
|
) *Transaction {
|
|
return &Transaction{
|
|
Domain: domain,
|
|
Inputs: inputs, // buildutils:allow-slice-alias slice is static
|
|
Outputs: outputs, // buildutils:allow-slice-alias slice is static
|
|
Fees: fees, // buildutils:allow-slice-alias slice is static
|
|
hypergraph: hypergraph,
|
|
bulletproofProver: bulletproofProver,
|
|
inclusionProver: inclusionProver,
|
|
verEnc: verEnc,
|
|
decafConstructor: decafConstructor,
|
|
keyRing: keyRing,
|
|
config: config,
|
|
rdfHypergraphSchema: rdfHypergraphSchema,
|
|
rdfMultiprover: rdfMultiprover,
|
|
}
|
|
}
|
|
|
|
// GetCost implements intrinsics.IntrinsicOperation.
|
|
func (tx *Transaction) GetCost() (*big.Int, error) {
|
|
size := big.NewInt(int64(len(tx.Domain)))
|
|
size.Add(size, big.NewInt(int64(len(tx.RangeProof))))
|
|
|
|
pb, err := tx.TraversalProof.ToBytes()
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "get cost")
|
|
}
|
|
size.Add(size, big.NewInt(int64(len(pb))))
|
|
|
|
for _, o := range tx.Outputs {
|
|
size.Add(size, big.NewInt(8)) // frame number
|
|
size.Add(size, big.NewInt(int64(len(o.Commitment))))
|
|
size.Add(size, big.NewInt(int64(len(o.RecipientOutput.CoinBalance))))
|
|
size.Add(size, big.NewInt(int64(len(o.RecipientOutput.Mask))))
|
|
size.Add(size, big.NewInt(int64(len(o.RecipientOutput.OneTimeKey))))
|
|
size.Add(size, big.NewInt(int64(len(o.RecipientOutput.VerificationKey))))
|
|
if len(o.RecipientOutput.AdditionalReference) == 64 {
|
|
size.Add(size, big.NewInt(120))
|
|
}
|
|
}
|
|
|
|
return size, nil
|
|
}
|
|
|
|
// Materialize implements intrinsics.IntrinsicOperation.
|
|
func (tx *Transaction) Materialize(
|
|
frameNumber uint64,
|
|
state state.State,
|
|
) (state.State, error) {
|
|
ms := state.(*hgstate.HypergraphState)
|
|
|
|
// Create the coin type hash
|
|
coinTypeBI, err := poseidon.HashBytes(
|
|
slices.Concat(tx.Domain[:], []byte("coin:Coin")),
|
|
)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "materialize")
|
|
}
|
|
coinTypeBytes := coinTypeBI.FillBytes(make([]byte, 32))
|
|
|
|
// For each output, create a coin
|
|
for _, output := range tx.Outputs {
|
|
// Create coin tree
|
|
coinTree := &qcrypto.VectorCommitmentTree{}
|
|
|
|
// Index 0: FrameNumber
|
|
if err := coinTree.Insert(
|
|
[]byte{0},
|
|
output.FrameNumber,
|
|
nil,
|
|
big.NewInt(8),
|
|
); err != nil {
|
|
return nil, errors.Wrap(err, "materialize")
|
|
}
|
|
|
|
// Index 1: Commitment
|
|
if err := coinTree.Insert(
|
|
[]byte{1 << 2},
|
|
output.Commitment,
|
|
nil,
|
|
big.NewInt(56),
|
|
); err != nil {
|
|
return nil, errors.Wrap(err, "materialize")
|
|
}
|
|
|
|
// Index 2: OneTimeKey
|
|
if err := coinTree.Insert(
|
|
[]byte{2 << 2},
|
|
output.RecipientOutput.OneTimeKey,
|
|
nil,
|
|
big.NewInt(56),
|
|
); err != nil {
|
|
return nil, errors.Wrap(err, "materialize")
|
|
}
|
|
|
|
// Index 3: VerificationKey
|
|
if err := coinTree.Insert(
|
|
[]byte{3 << 2},
|
|
output.RecipientOutput.VerificationKey,
|
|
nil,
|
|
big.NewInt(56),
|
|
); err != nil {
|
|
return nil, errors.Wrap(err, "materialize")
|
|
}
|
|
|
|
// Index 4: CoinBalance (encrypted)
|
|
if err := coinTree.Insert(
|
|
[]byte{4 << 2},
|
|
output.RecipientOutput.CoinBalance,
|
|
nil,
|
|
big.NewInt(56),
|
|
); err != nil {
|
|
return nil, errors.Wrap(err, "materialize")
|
|
}
|
|
|
|
// Index 5: Mask (encrypted)
|
|
if err := coinTree.Insert(
|
|
[]byte{5 << 2},
|
|
output.RecipientOutput.Mask,
|
|
nil,
|
|
big.NewInt(56),
|
|
); err != nil {
|
|
return nil, errors.Wrap(err, "materialize")
|
|
}
|
|
|
|
// Index 6 & 7: Additional references (for non-divisible tokens)
|
|
if len(output.RecipientOutput.AdditionalReference) == 64 &&
|
|
len(output.RecipientOutput.AdditionalReferenceKey) == 56 {
|
|
if err := coinTree.Insert(
|
|
[]byte{6 << 2},
|
|
output.RecipientOutput.AdditionalReference,
|
|
nil,
|
|
big.NewInt(56),
|
|
); err != nil {
|
|
return nil, errors.Wrap(err, "materialize")
|
|
}
|
|
|
|
if err := coinTree.Insert(
|
|
[]byte{7 << 2},
|
|
output.RecipientOutput.AdditionalReferenceKey,
|
|
nil,
|
|
big.NewInt(56),
|
|
); err != nil {
|
|
return nil, errors.Wrap(err, "materialize")
|
|
}
|
|
}
|
|
|
|
// Type marker at max index
|
|
if err := coinTree.Insert(
|
|
bytes.Repeat([]byte{0xff}, 32),
|
|
coinTypeBytes,
|
|
nil,
|
|
big.NewInt(32),
|
|
); err != nil {
|
|
return nil, errors.Wrap(err, "materialize")
|
|
}
|
|
|
|
// Compute address and add to state
|
|
commit := coinTree.Commit(tx.inclusionProver, false)
|
|
outAddrBI, err := poseidon.HashBytes(commit)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "materialize")
|
|
}
|
|
|
|
coinAddress := outAddrBI.FillBytes(make([]byte, 32))
|
|
|
|
// Create materialized state for coin
|
|
coinState := ms.NewVertexAddMaterializedState(
|
|
tx.Domain,
|
|
[32]byte(coinAddress),
|
|
frameNumber,
|
|
nil,
|
|
coinTree,
|
|
)
|
|
|
|
// Set the state
|
|
err = ms.Set(
|
|
tx.Domain[:],
|
|
coinAddress,
|
|
hgstate.VertexAddsDiscriminator,
|
|
frameNumber,
|
|
coinState,
|
|
)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "materialize")
|
|
}
|
|
}
|
|
|
|
// Mark inputs as spent
|
|
for _, input := range tx.Inputs {
|
|
if len(input.Signature) == 336 {
|
|
// Standard format
|
|
verificationKey := input.Signature[56*4 : 56*5]
|
|
spendCheckBI, err := poseidon.HashBytes(verificationKey)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "materialize")
|
|
}
|
|
|
|
// Create spent marker
|
|
spentTree := &qcrypto.VectorCommitmentTree{}
|
|
if err := spentTree.Insert(
|
|
[]byte{0},
|
|
[]byte{0x01},
|
|
nil,
|
|
big.NewInt(0),
|
|
); err != nil {
|
|
return nil, errors.Wrap(err, "materialize")
|
|
}
|
|
|
|
spentAddress := spendCheckBI.FillBytes(make([]byte, 32))
|
|
|
|
// Create materialized state for spend
|
|
spentState := ms.NewVertexAddMaterializedState(
|
|
tx.Domain,
|
|
[32]byte(spentAddress),
|
|
frameNumber,
|
|
nil,
|
|
spentTree,
|
|
)
|
|
|
|
// Set the state
|
|
err = ms.Set(
|
|
tx.Domain[:],
|
|
spentAddress,
|
|
hgstate.VertexAddsDiscriminator,
|
|
frameNumber,
|
|
spentState,
|
|
)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "materialize")
|
|
}
|
|
}
|
|
}
|
|
|
|
return ms, nil
|
|
}
|
|
|
|
func (tx *Transaction) Prove(frameNumber uint64) error {
|
|
if len(tx.Inputs) == 0 || len(tx.Outputs) == 0 ||
|
|
len(tx.Inputs) > 100 || len(tx.Outputs) > 100 {
|
|
return errors.Wrap(
|
|
errors.New("invalid quantity of inputs, outputs, or proofs"),
|
|
"prove",
|
|
)
|
|
}
|
|
|
|
values := []*big.Int{}
|
|
for _, o := range tx.Outputs {
|
|
values = append(values, o.value)
|
|
}
|
|
|
|
addresses := [][]byte{}
|
|
blinds := []byte{}
|
|
for i, input := range tx.Inputs {
|
|
blind, err := input.Prove(tx, i)
|
|
if err != nil {
|
|
return errors.Wrap(err, "prove")
|
|
}
|
|
|
|
addresses = append(addresses, input.address)
|
|
blinds = append(blinds, blind...)
|
|
}
|
|
|
|
res, err := tx.bulletproofProver.GenerateRangeProofFromBig(
|
|
values,
|
|
blinds,
|
|
128,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(res.Commitment) != len(tx.Outputs)*56 ||
|
|
len(res.Blinding) != len(tx.Outputs)*56 {
|
|
return errors.Wrap(errors.New("invalid range proof"), "prove")
|
|
}
|
|
|
|
for i, o := range tx.Outputs {
|
|
if err := o.Prove(res, i, tx, frameNumber); err != nil {
|
|
return errors.Wrap(err, "prove")
|
|
}
|
|
}
|
|
|
|
challenge, err := tx.GetChallenge()
|
|
if err != nil {
|
|
return errors.Wrap(err, "prove")
|
|
}
|
|
|
|
for _, input := range tx.Inputs {
|
|
if err := input.signOp(challenge); err != nil {
|
|
return errors.Wrap(err, "prove")
|
|
}
|
|
}
|
|
|
|
tx.RangeProof = res.Proof
|
|
tx.TraversalProof, err = tx.hypergraph.CreateTraversalProof(
|
|
tx.Domain,
|
|
hypergraph.VertexAtomType,
|
|
hypergraph.AddsPhaseType,
|
|
addresses,
|
|
)
|
|
|
|
if err != nil {
|
|
return errors.Wrap(err, "prove")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (tx *Transaction) GetReadAddresses(
|
|
frameNumber uint64,
|
|
) ([][]byte, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func (tx *Transaction) GetWriteAddresses(
|
|
frameNumber uint64,
|
|
) ([][]byte, error) {
|
|
// Create the coin type hash
|
|
coinTypeBI, err := poseidon.HashBytes(
|
|
slices.Concat(tx.Domain[:], []byte("coin:Coin")),
|
|
)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "get write addresses")
|
|
}
|
|
coinTypeBytes := coinTypeBI.FillBytes(make([]byte, 32))
|
|
|
|
addresses := [][]byte{}
|
|
|
|
// For each output, create a coin
|
|
for _, output := range tx.Outputs {
|
|
// Create coin tree
|
|
coinTree := &qcrypto.VectorCommitmentTree{}
|
|
|
|
// Index 0: FrameNumber
|
|
if err := coinTree.Insert(
|
|
[]byte{0},
|
|
output.FrameNumber,
|
|
nil,
|
|
big.NewInt(8),
|
|
); err != nil {
|
|
return nil, errors.Wrap(err, "get write addresses")
|
|
}
|
|
|
|
// Index 1: Commitment
|
|
if err := coinTree.Insert(
|
|
[]byte{1 << 2},
|
|
output.Commitment,
|
|
nil,
|
|
big.NewInt(56),
|
|
); err != nil {
|
|
return nil, errors.Wrap(err, "get write addresses")
|
|
}
|
|
|
|
// Index 2: OneTimeKey
|
|
if err := coinTree.Insert(
|
|
[]byte{2 << 2},
|
|
output.RecipientOutput.OneTimeKey,
|
|
nil,
|
|
big.NewInt(56),
|
|
); err != nil {
|
|
return nil, errors.Wrap(err, "get write addresses")
|
|
}
|
|
|
|
// Index 3: VerificationKey
|
|
if err := coinTree.Insert(
|
|
[]byte{3 << 2},
|
|
output.RecipientOutput.VerificationKey,
|
|
nil,
|
|
big.NewInt(56),
|
|
); err != nil {
|
|
return nil, errors.Wrap(err, "get write addresses")
|
|
}
|
|
|
|
// Index 4: CoinBalance (encrypted)
|
|
if err := coinTree.Insert(
|
|
[]byte{4 << 2},
|
|
output.RecipientOutput.CoinBalance,
|
|
nil,
|
|
big.NewInt(56),
|
|
); err != nil {
|
|
return nil, errors.Wrap(err, "get write addresses")
|
|
}
|
|
|
|
// Index 5: Mask (encrypted)
|
|
if err := coinTree.Insert(
|
|
[]byte{5 << 2},
|
|
output.RecipientOutput.Mask,
|
|
nil,
|
|
big.NewInt(56),
|
|
); err != nil {
|
|
return nil, errors.Wrap(err, "get write addresses")
|
|
}
|
|
|
|
// Index 6 & 7: Additional references (for non-divisible tokens)
|
|
if len(output.RecipientOutput.AdditionalReference) == 64 &&
|
|
len(output.RecipientOutput.AdditionalReferenceKey) == 56 {
|
|
if err := coinTree.Insert(
|
|
[]byte{6 << 2},
|
|
output.RecipientOutput.AdditionalReference,
|
|
nil,
|
|
big.NewInt(56),
|
|
); err != nil {
|
|
return nil, errors.Wrap(err, "get write addresses")
|
|
}
|
|
|
|
if err := coinTree.Insert(
|
|
[]byte{7 << 2},
|
|
output.RecipientOutput.AdditionalReferenceKey,
|
|
nil,
|
|
big.NewInt(56),
|
|
); err != nil {
|
|
return nil, errors.Wrap(err, "get write addresses")
|
|
}
|
|
}
|
|
|
|
// Type marker at max index
|
|
if err := coinTree.Insert(
|
|
bytes.Repeat([]byte{0xff}, 32),
|
|
coinTypeBytes,
|
|
nil,
|
|
big.NewInt(32),
|
|
); err != nil {
|
|
return nil, errors.Wrap(err, "get write addresses")
|
|
}
|
|
|
|
// Compute address and add to state
|
|
commit := coinTree.Commit(tx.inclusionProver, false)
|
|
outAddrBI, err := poseidon.HashBytes(commit)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "get write addresses")
|
|
}
|
|
|
|
coinAddress := outAddrBI.FillBytes(make([]byte, 32))
|
|
|
|
addresses = append(addresses, slices.Concat(
|
|
tx.Domain[:],
|
|
coinAddress,
|
|
))
|
|
}
|
|
|
|
// Mark inputs as spent
|
|
for _, input := range tx.Inputs {
|
|
if len(input.Signature) == 336 {
|
|
// Standard format
|
|
verificationKey := input.Signature[56*4 : 56*5]
|
|
spendCheckBI, err := poseidon.HashBytes(verificationKey)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "get write addresses")
|
|
}
|
|
|
|
// Create spent marker
|
|
spentTree := &qcrypto.VectorCommitmentTree{}
|
|
if err := spentTree.Insert(
|
|
[]byte{0},
|
|
[]byte{0x01},
|
|
nil,
|
|
big.NewInt(0),
|
|
); err != nil {
|
|
return nil, errors.Wrap(err, "get write addresses")
|
|
}
|
|
|
|
spentAddress := spendCheckBI.FillBytes(make([]byte, 32))
|
|
|
|
addresses = append(addresses, slices.Concat(
|
|
tx.Domain[:],
|
|
spentAddress,
|
|
))
|
|
}
|
|
}
|
|
|
|
return addresses, nil
|
|
}
|
|
|
|
func (tx *Transaction) GetChallenge() ([]byte, error) {
|
|
transcript := []byte{}
|
|
transcript = append(transcript, tx.Domain[:]...)
|
|
for _, o := range tx.Outputs {
|
|
transcript = append(transcript, o.Commitment...)
|
|
transcript = append(transcript, o.FrameNumber...)
|
|
transcript = append(transcript, o.RecipientOutput.CoinBalance...)
|
|
transcript = append(transcript, o.RecipientOutput.Mask...)
|
|
transcript = append(transcript, o.RecipientOutput.OneTimeKey...)
|
|
transcript = append(transcript, o.RecipientOutput.VerificationKey...)
|
|
if len(o.RecipientOutput.AdditionalReference) == 64 {
|
|
transcript = append(transcript, o.RecipientOutput.AdditionalReference...)
|
|
transcript = append(
|
|
transcript,
|
|
o.RecipientOutput.AdditionalReferenceKey...,
|
|
)
|
|
}
|
|
}
|
|
|
|
challenge, err := tx.decafConstructor.HashToScalar(transcript)
|
|
return challenge.Private(), errors.Wrap(err, "get challenge")
|
|
}
|
|
|
|
// Verifies the transaction's validity at the given frame number. If invalid,
|
|
// also provides the associated error.
|
|
func (tx *Transaction) Verify(frameNumber uint64) (bool, error) {
|
|
if len(tx.Inputs) == 0 || len(tx.Outputs) == 0 ||
|
|
len(tx.Inputs) > 100 || len(tx.Outputs) > 100 ||
|
|
len(tx.Inputs) != len(tx.TraversalProof.SubProofs) {
|
|
return false, errors.Wrap(
|
|
errors.New("invalid quantity of inputs, outputs, or proofs"),
|
|
"verify",
|
|
)
|
|
}
|
|
|
|
for _, fee := range tx.Fees {
|
|
if fee == nil ||
|
|
new(big.Int).Lsh(big.NewInt(1), uint(128)).Cmp(fee) < 0 ||
|
|
new(big.Int).Cmp(fee) > 0 {
|
|
return false, errors.Wrap(errors.New("invalid fees"), "verify")
|
|
}
|
|
}
|
|
|
|
if tx.config.Behavior&Divisible == 0 && len(tx.Inputs) != len(tx.Outputs) {
|
|
return false, errors.Wrap(
|
|
errors.New("non-divisible token has mismatching inputs and outputs"),
|
|
"verify",
|
|
)
|
|
}
|
|
|
|
challenge, err := tx.GetChallenge()
|
|
if err != nil {
|
|
return false, errors.Wrap(err, "verify")
|
|
}
|
|
|
|
inputs := [][]byte{}
|
|
check := map[string]struct{}{}
|
|
for i, input := range tx.Inputs {
|
|
if valid, err := input.Verify(
|
|
frameNumber,
|
|
tx,
|
|
challenge,
|
|
bytes.Equal(tx.Domain[:], QUIL_TOKEN_ADDRESS),
|
|
tx.TraversalProof,
|
|
i,
|
|
); !valid {
|
|
return false, errors.Wrap(err, "verify")
|
|
}
|
|
|
|
if _, ok := check[string(input.Signature[(56*4):(56*5)])]; ok {
|
|
return false, errors.Wrap(
|
|
errors.New("attempted double-spend"),
|
|
"verify",
|
|
)
|
|
}
|
|
check[string(input.Signature[(56*4):(56*5)])] = struct{}{}
|
|
inputs = append(inputs, input.Commitment)
|
|
}
|
|
|
|
commitment := make([]byte, len(tx.Outputs)*56)
|
|
commitments := [][]byte{}
|
|
for i, o := range tx.Outputs {
|
|
if valid, err := o.Verify(frameNumber, tx.config); !valid {
|
|
return false, errors.Wrap(err, "verify")
|
|
}
|
|
|
|
if tx.config.Behavior&Divisible == 0 {
|
|
if !bytes.Equal(
|
|
o.RecipientOutput.AdditionalReference,
|
|
tx.Inputs[i].Proofs[len(tx.Inputs[i].Proofs)-1][:64],
|
|
) || !bytes.Equal(
|
|
o.RecipientOutput.AdditionalReferenceKey,
|
|
tx.Inputs[i].Proofs[len(tx.Inputs[i].Proofs)-1][64:],
|
|
) {
|
|
return false, errors.Wrap(errors.New("invalid reference"), "verify")
|
|
}
|
|
}
|
|
|
|
spendCheckBI, err := poseidon.HashBytes(o.RecipientOutput.VerificationKey)
|
|
if err != nil {
|
|
return false, errors.Wrap(err, "verify")
|
|
}
|
|
|
|
_, err = tx.hypergraph.GetVertex([64]byte(
|
|
slices.Concat(tx.Domain[:], spendCheckBI.FillBytes(make([]byte, 32))),
|
|
))
|
|
if err == nil {
|
|
return false, errors.Wrap(
|
|
errors.New("invalid verification key"),
|
|
"verify",
|
|
)
|
|
}
|
|
|
|
copy(commitment[i*56:(i+1)*56], tx.Outputs[i].Commitment[:])
|
|
commitments = append(commitments, tx.Outputs[i].Commitment)
|
|
}
|
|
|
|
roots, err := tx.hypergraph.GetShardCommits(
|
|
binary.BigEndian.Uint64(tx.Outputs[0].FrameNumber),
|
|
tx.Domain[:],
|
|
)
|
|
if err != nil {
|
|
return false, errors.Wrap(err, "verify")
|
|
}
|
|
|
|
valid, err := tx.hypergraph.VerifyTraversalProof(
|
|
tx.Domain,
|
|
hypergraph.VertexAtomType,
|
|
hypergraph.AddsPhaseType,
|
|
roots[0],
|
|
tx.TraversalProof,
|
|
)
|
|
if err != nil || !valid {
|
|
return false, errors.Wrap(err, "verify")
|
|
}
|
|
|
|
if !tx.bulletproofProver.VerifyRangeProof(tx.RangeProof, commitment, 128) {
|
|
return false, errors.Wrap(errors.New("invalid range proof"), "verify")
|
|
}
|
|
|
|
sumcheckFees := []*big.Int{}
|
|
if bytes.Equal(tx.Domain[:], QUIL_TOKEN_ADDRESS) {
|
|
sumcheckFees = append(sumcheckFees, tx.Fees...)
|
|
}
|
|
|
|
if !tx.bulletproofProver.SumCheck(
|
|
inputs,
|
|
[]*big.Int{},
|
|
commitments,
|
|
sumcheckFees,
|
|
) {
|
|
return false, errors.Wrap(errors.New("invalid sum check"), "verify")
|
|
}
|
|
|
|
return true, nil
|
|
}
|
|
|
|
var _ intrinsics.IntrinsicOperation = (*Transaction)(nil)
|