ceremonyclient/node/execution/intrinsics/token/token_intrinsic_test.go
2025-11-26 03:22:48 -06:00

661 lines
22 KiB
Go

package token
import (
"encoding/binary"
"math/big"
"slices"
"testing"
"github.com/iden3/go-iden3-crypto/poseidon"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"source.quilibrium.com/quilibrium/monorepo/hypergraph"
hgstate "source.quilibrium.com/quilibrium/monorepo/node/execution/state/hypergraph"
tcrypto "source.quilibrium.com/quilibrium/monorepo/types/crypto"
"source.quilibrium.com/quilibrium/monorepo/types/execution/state"
"source.quilibrium.com/quilibrium/monorepo/types/mocks"
"source.quilibrium.com/quilibrium/monorepo/types/schema"
crypto "source.quilibrium.com/quilibrium/monorepo/types/tries"
)
func createNonMintableTestConfig() *TokenIntrinsicConfiguration {
return &TokenIntrinsicConfiguration{
Behavior: Burnable | Divisible,
Units: big.NewInt(1),
Supply: big.NewInt(1000000),
Name: "Test Token",
Symbol: "TEST",
}
}
// Creates a mintable token configuration with authority for testing
func createMintableTestConfig() *TokenIntrinsicConfiguration {
return &TokenIntrinsicConfiguration{
Behavior: Mintable | Burnable | Divisible,
MintStrategy: &TokenMintStrategy{
MintBehavior: MintWithAuthority,
Authority: &Authority{
KeyType: tcrypto.KeyTypeEd448,
PublicKey: []byte("test-public-key"),
CanBurn: true,
},
},
Units: big.NewInt(100),
Supply: big.NewInt(1000000),
Name: "Mintable Test Token",
Symbol: "MTEST",
}
}
func createMintWithPaymentTestConfig() *TokenIntrinsicConfiguration {
return &TokenIntrinsicConfiguration{
Behavior: Mintable | Divisible,
MintStrategy: &TokenMintStrategy{
MintBehavior: MintWithPayment,
PaymentAddress: make([]byte, 32), // 32 byte address
FeeBasis: &FeeBasis{
Type: PerUnit,
Baseline: big.NewInt(100),
},
},
Units: big.NewInt(100),
Supply: big.NewInt(1000000),
Name: "Payment Test Token",
Symbol: "PTEST",
}
}
func createMockDependencies() (
*mocks.MockInclusionProver,
*mocks.MockBulletproofProver,
*mocks.MockKeyManager,
*mocks.MockHypergraph,
*mocks.MockVerifiableEncryptor,
*mocks.MockDecafConstructor,
) {
inclusionProver := new(mocks.MockInclusionProver)
inclusionProver.On("CommitRaw", mock.Anything, mock.Anything).Return(make([]byte, 74), nil)
bulletproofProver := new(mocks.MockBulletproofProver)
keyManager := new(mocks.MockKeyManager)
hypergraph := new(mocks.MockHypergraph)
decafConstructor := new(mocks.MockDecafConstructor)
verEnc := new(mocks.MockVerifiableEncryptor)
hypergraph.On("GetProver").Return(inclusionProver)
return inclusionProver, bulletproofProver, keyManager, hypergraph, verEnc, decafConstructor
}
func TestNewTokenIntrinsic(t *testing.T) {
tests := []struct {
name string
config *TokenIntrinsicConfiguration
expectError bool
}{
{
name: "valid non-mintable token",
config: createNonMintableTestConfig(),
expectError: false,
},
{
name: "valid mintable token with authority",
config: createMintableTestConfig(),
expectError: false,
},
{
name: "valid mintable token with payment",
config: createMintWithPaymentTestConfig(),
expectError: false,
},
{
name: "invalid - mintable without mint strategy",
config: &TokenIntrinsicConfiguration{
Behavior: Mintable,
Supply: big.NewInt(1000000),
Name: "Invalid Token",
Symbol: "INVLD",
},
expectError: true,
},
{
name: "invalid - divisible without units",
config: &TokenIntrinsicConfiguration{
Behavior: Divisible,
Supply: big.NewInt(1000000),
Name: "Invalid Token",
Symbol: "INVLD",
},
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
inclusionProver, bulletproofProver, keyManager, hypergraph, verEnc, decafConstructor := createMockDependencies()
intrinsic, err := NewTokenIntrinsic(
tt.config,
hypergraph,
verEnc,
decafConstructor,
bulletproofProver,
inclusionProver,
keyManager,
)
if tt.expectError {
// We don't expect errors from NewTokenIntrinsic directly,
// as it just initializes the struct. But we'll check validation
// by calling validateTokenConfiguration
err := validateTokenConfiguration(tt.config)
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.NotNil(t, intrinsic)
assert.Equal(t, tt.config, intrinsic.config)
assert.Equal(t, hypergraph, intrinsic.hypergraph)
assert.Equal(t, bulletproofProver, intrinsic.bulletproofProver)
assert.Equal(t, inclusionProver, intrinsic.inclusionProver)
assert.Equal(t, keyManager, intrinsic.keyManager)
}
})
}
}
func TestDeploy(t *testing.T) {
inclusionProver, bulletproofProver, keyManager, hypergraph, verEnc, decafConstructor := createMockDependencies()
// Setup the intrinsic with a valid config
config := createNonMintableTestConfig()
intrinsic, err := NewTokenIntrinsic(
config,
hypergraph,
verEnc,
decafConstructor,
bulletproofProver,
inclusionProver,
keyManager,
)
require.NoError(t, err)
t.Run("creates successful deployment state", func(t *testing.T) {
domain := TOKEN_BASE_DOMAIN
provers := [][]byte{}
creator := []byte("creator")
fee := big.NewInt(10)
var st state.State = hgstate.NewHypergraphState(hypergraph)
st, _, err = intrinsic.Deploy(domain, provers, creator, fee, []byte{}, 1, st)
require.NoError(t, err)
require.Len(t, st.Changeset(), 1)
initChange := st.Changeset()[0]
require.Equal(t, initChange.StateChange, state.InitializeStateChangeEvent)
})
}
func TestLoadTokenIntrinsic(t *testing.T) {
inclusionProver, bulletproofProver, keyManager, _, verEnc, decafConstructor := createMockDependencies()
// Setup mocks for the error case
mockHypergraphErr := new(mocks.MockHypergraph)
mockHypergraphErr.On("GetVertex", mock.Anything).Return(nil, errors.New("vertex not found"))
// Setup mocks for the success case
mockHypergraphSuccess := new(mocks.MockHypergraph)
// Create test configuration
config := createMintableTestConfig()
// Create the metadata tree with valid configuration
metadataTree := &crypto.VectorCommitmentTree{}
rdfMultiprover := schema.NewRDFMultiprover(&schema.TurtleRDFParser{}, inclusionProver)
configTree, err := NewTokenConfigurationMetadata(config, rdfMultiprover)
require.NoError(t, err)
tokenDomainBI, _ := poseidon.HashBytes(
slices.Concat(
TOKEN_PREFIX,
configTree.Commit(inclusionProver, false),
),
)
appAddress := tokenDomainBI.FillBytes(make([]byte, 32))
metadataAddress := make([]byte, 64)
copy(metadataAddress[:32], appAddress)
mockHypergraphSuccess.On("GetVertex", mock.Anything).Return(hypergraph.NewVertex([32]byte{}, [32]byte{}, []byte{}, big.NewInt(0)), nil)
// Store consensus tree
consensus := &crypto.VectorCommitmentTree{}
consensusData, _ := crypto.SerializeNonLazyTree(consensus)
require.NoError(t, metadataTree.Insert([]byte{0 << 2}, consensusData, nil, big.NewInt(int64(len(consensusData)))))
// Store sumcheck tree
sumcheck := &crypto.VectorCommitmentTree{}
sumcheckData, _ := crypto.SerializeNonLazyTree(sumcheck)
require.NoError(t, metadataTree.Insert([]byte{1 << 2}, sumcheckData, nil, big.NewInt(int64(len(sumcheckData)))))
// Store RDF schema
rdfschema, _ := newTokenRDFHypergraphSchema(appAddress, config)
require.NoError(t, metadataTree.Insert([]byte{2 << 2}, []byte(rdfschema), nil, big.NewInt(int64(len(rdfschema)))))
// Store config metadata at the right index
configBytes, err := crypto.SerializeNonLazyTree(configTree)
require.NoError(t, err)
require.NoError(t, metadataTree.Insert([]byte{16 << 2}, configBytes, nil, big.NewInt(int64(len(configBytes)))))
// Mock the GetVertexData to return our tree
mockHypergraphSuccess.On("GetVertexData", mock.Anything).Return(metadataTree, nil)
// Setup inclusion prover to validate token domain
inclusionProver.On("CommitRaw", mock.Anything, mock.Anything).Return([]byte("mock-commitment"), nil)
t.Run("load failure - vertex not found", func(t *testing.T) {
_, err := LoadTokenIntrinsic(appAddress, mockHypergraphErr, verEnc, decafConstructor, bulletproofProver, inclusionProver, keyManager, nil)
assert.Error(t, err)
assert.Contains(t, err.Error(), "vertex not found")
mockHypergraphErr.AssertExpectations(t)
})
t.Run("load success - valid token intrinsic", func(t *testing.T) {
inclusionProver.On("CommitRaw", mock.Anything, mock.Anything).Return([]byte("commitment"), nil)
tokenIntrinsic, err := LoadTokenIntrinsic(appAddress, mockHypergraphSuccess, verEnc, decafConstructor, bulletproofProver, inclusionProver, keyManager, nil)
require.NoError(t, err)
require.NotNil(t, tokenIntrinsic)
// Verify the loaded intrinsic has all the required components
assert.Equal(t, mockHypergraphSuccess, tokenIntrinsic.hypergraph)
assert.Equal(t, bulletproofProver, tokenIntrinsic.bulletproofProver)
assert.Equal(t, inclusionProver, tokenIntrinsic.inclusionProver)
assert.Equal(t, keyManager, tokenIntrinsic.keyManager)
// Verify RDF schema is loaded
assert.NotEmpty(t, tokenIntrinsic.rdfHypergraphSchema)
mockHypergraphSuccess.AssertExpectations(t)
})
}
func TestTokenConfigurationSerialization(t *testing.T) {
// Create an inclusion prover mock that can be used by the tree operations
inclusionProver := new(mocks.MockInclusionProver)
inclusionProver.On("CommitRaw", mock.Anything, mock.Anything).Return([]byte("mock-commitment"), nil)
tests := []struct {
name string
config *TokenIntrinsicConfiguration
}{
{
name: "non-mintable token",
config: createNonMintableTestConfig(),
},
{
name: "mintable token with authority",
config: createMintableTestConfig(),
},
{
name: "mintable token with payment",
config: createMintWithPaymentTestConfig(),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Create configuration tree
rdfMultiprover := schema.NewRDFMultiprover(&schema.TurtleRDFParser{}, inclusionProver)
tree, err := NewTokenConfigurationMetadata(tt.config, rdfMultiprover)
require.NoError(t, err)
// Check that it has stored the expected keys
behaviorBytes, err := tree.Get([]byte{0 << 2})
require.NoError(t, err)
assert.Equal(t, uint16(tt.config.Behavior), binary.BigEndian.Uint16(behaviorBytes))
// Check name and symbol
nameBytes, err := tree.Get([]byte{4 << 2})
require.NoError(t, err)
assert.Equal(t, tt.config.Name, string(nameBytes))
symbolBytes, err := tree.Get([]byte{5 << 2})
require.NoError(t, err)
assert.Equal(t, tt.config.Symbol, string(symbolBytes))
// Check for MintStrategy if applicable
if (tt.config.Behavior & Mintable) != 0 {
mintStrategyBytes, err := tree.Get([]byte{1 << 2})
require.NoError(t, err)
assert.NotEmpty(t, mintStrategyBytes)
// Deserialize and validate all MintStrategy fields by packing and reusing the helper
metadataTree := &crypto.VectorCommitmentTree{}
flat, _ := crypto.SerializeNonLazyTree(tree)
metadataTree.Insert([]byte{16 << 2}, flat, nil, big.NewInt(int64(len(flat))))
deserializedConfig, err := unpackAndVerifyTokenConfigurationMetadata(inclusionProver, metadataTree)
require.NoError(t, err)
require.NotNil(t, deserializedConfig.MintStrategy)
// Validate MintBehavior
assert.Equal(t, tt.config.MintStrategy.MintBehavior, deserializedConfig.MintStrategy.MintBehavior)
// Validate ProofBasis
assert.Equal(t, tt.config.MintStrategy.ProofBasis, deserializedConfig.MintStrategy.ProofBasis)
// Validate Authority if present
if tt.config.MintStrategy.Authority != nil {
require.NotNil(t, deserializedConfig.MintStrategy.Authority)
assert.Equal(t, tt.config.MintStrategy.Authority.KeyType, deserializedConfig.MintStrategy.Authority.KeyType)
assert.Equal(t, tt.config.MintStrategy.Authority.PublicKey, deserializedConfig.MintStrategy.Authority.PublicKey)
assert.Equal(t, tt.config.MintStrategy.Authority.CanBurn, deserializedConfig.MintStrategy.Authority.CanBurn)
}
// Validate PaymentAddress if present
if tt.config.MintStrategy.PaymentAddress != nil {
assert.Equal(t, tt.config.MintStrategy.PaymentAddress, deserializedConfig.MintStrategy.PaymentAddress)
}
// Validate FeeBasis if present
if tt.config.MintStrategy.FeeBasis != nil {
require.NotNil(t, deserializedConfig.MintStrategy.FeeBasis)
assert.Equal(t, tt.config.MintStrategy.FeeBasis.Type, deserializedConfig.MintStrategy.FeeBasis.Type)
assert.Equal(t, 0, tt.config.MintStrategy.FeeBasis.Baseline.Cmp(deserializedConfig.MintStrategy.FeeBasis.Baseline))
}
// Validate VerkleRoot if present
if tt.config.MintStrategy.VerkleRoot != nil {
assert.Equal(t, tt.config.MintStrategy.VerkleRoot, deserializedConfig.MintStrategy.VerkleRoot)
}
}
// Check for Units if applicable
if (tt.config.Behavior & Divisible) != 0 {
unitsBytes, err := tree.Get([]byte{2 << 2})
require.NoError(t, err)
assert.Equal(t, tt.config.Units.FillBytes(make([]byte, 32)), unitsBytes)
}
// Check for Supply
supplyBytes, err := tree.Get([]byte{3 << 2})
require.NoError(t, err)
assert.Equal(t, tt.config.Supply.FillBytes(make([]byte, 32)), supplyBytes)
// Check for AdditionalReference
refBytes, err := tree.Get([]byte{6 << 2})
require.NoError(t, err)
assert.Equal(t, tt.config.AdditionalReference[:], refBytes)
})
}
}
// TestTokenConfigurationSerializationRoundTrip tests full serialization/deserialization cycle
func TestTokenConfigurationSerializationRoundTrip(t *testing.T) {
inclusionProver := new(mocks.MockInclusionProver)
inclusionProver.On("CommitRaw", mock.Anything, mock.Anything).Return([]byte("mock-commitment"), nil)
tests := []struct {
name string
config *TokenIntrinsicConfiguration
}{
{
name: "non-mintable token",
config: createNonMintableTestConfig(),
},
{
name: "mintable token with authority",
config: createMintableTestConfig(),
},
{
name: "mintable token with payment",
config: createMintWithPaymentTestConfig(),
},
{
name: "mintable token with proof and verkle root",
config: &TokenIntrinsicConfiguration{
Behavior: Mintable | Divisible,
MintStrategy: &TokenMintStrategy{
MintBehavior: MintWithProof,
ProofBasis: ProofOfMeaningfulWork,
VerkleRoot: []byte("test-verkle-root-hash"),
},
Units: big.NewInt(100),
Supply: big.NewInt(1000000),
Name: "Proof Test Token",
Symbol: "PROOF",
},
},
{
name: "complex token with all features",
config: &TokenIntrinsicConfiguration{
Behavior: Mintable | Burnable | Divisible | Acceptable | Expirable | Tenderable,
MintStrategy: &TokenMintStrategy{
MintBehavior: MintWithAuthority,
Authority: &Authority{
KeyType: tcrypto.KeyTypeBLS48581G1,
PublicKey: make([]byte, 585), // Max G2 pubkey size
CanBurn: true,
},
PaymentAddress: make([]byte, 32),
FeeBasis: &FeeBasis{
Type: PerUnit,
Baseline: big.NewInt(1000),
},
},
Units: big.NewInt(10000),
Supply: big.NewInt(1000000000),
Name: "Complex Test Token With Long Name",
Symbol: "COMPLEX",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Create configuration tree
rdfMultiprover := schema.NewRDFMultiprover(&schema.TurtleRDFParser{}, inclusionProver)
tree, err := NewTokenConfigurationMetadata(tt.config, rdfMultiprover)
require.NoError(t, err)
// Deserialize and validate all MintStrategy fields by packing and reusing the helper
metadataTree := &crypto.VectorCommitmentTree{}
flat, _ := crypto.SerializeNonLazyTree(tree)
metadataTree.Insert([]byte{16 << 2}, flat, nil, big.NewInt(int64(len(flat))))
deserializedConfig, err := unpackAndVerifyTokenConfigurationMetadata(inclusionProver, metadataTree)
require.NoError(t, err)
// Validate all fields match
assert.Equal(t, tt.config.Behavior, deserializedConfig.Behavior)
assert.Equal(t, tt.config.Name, deserializedConfig.Name)
assert.Equal(t, tt.config.Symbol, deserializedConfig.Symbol)
assert.Equal(t, tt.config.AdditionalReference, deserializedConfig.AdditionalReference)
// Validate Units if present
if tt.config.Units != nil {
require.NotNil(t, deserializedConfig.Units)
assert.Equal(t, 0, tt.config.Units.Cmp(deserializedConfig.Units))
}
// Validate Supply if present
if tt.config.Supply != nil {
require.NotNil(t, deserializedConfig.Supply)
assert.Equal(t, 0, tt.config.Supply.Cmp(deserializedConfig.Supply))
}
// Validate MintStrategy if present
if tt.config.MintStrategy != nil {
require.NotNil(t, deserializedConfig.MintStrategy)
assert.Equal(t, tt.config.MintStrategy.MintBehavior, deserializedConfig.MintStrategy.MintBehavior)
assert.Equal(t, tt.config.MintStrategy.ProofBasis, deserializedConfig.MintStrategy.ProofBasis)
// Validate Authority
if tt.config.MintStrategy.Authority != nil {
require.NotNil(t, deserializedConfig.MintStrategy.Authority)
assert.Equal(t, tt.config.MintStrategy.Authority.KeyType, deserializedConfig.MintStrategy.Authority.KeyType)
assert.Equal(t, tt.config.MintStrategy.Authority.PublicKey, deserializedConfig.MintStrategy.Authority.PublicKey)
assert.Equal(t, tt.config.MintStrategy.Authority.CanBurn, deserializedConfig.MintStrategy.Authority.CanBurn)
} else {
assert.Nil(t, deserializedConfig.MintStrategy.Authority)
}
// Validate PaymentAddress
if tt.config.MintStrategy.PaymentAddress != nil {
assert.Equal(t, tt.config.MintStrategy.PaymentAddress, deserializedConfig.MintStrategy.PaymentAddress)
} else {
assert.Nil(t, deserializedConfig.MintStrategy.PaymentAddress)
}
// Validate FeeBasis
if tt.config.MintStrategy.FeeBasis != nil {
require.NotNil(t, deserializedConfig.MintStrategy.FeeBasis)
assert.Equal(t, tt.config.MintStrategy.FeeBasis.Type, deserializedConfig.MintStrategy.FeeBasis.Type)
assert.Equal(t, 0, tt.config.MintStrategy.FeeBasis.Baseline.Cmp(deserializedConfig.MintStrategy.FeeBasis.Baseline))
} else {
assert.Nil(t, deserializedConfig.MintStrategy.FeeBasis)
}
// Validate VerkleRoot
if tt.config.MintStrategy.VerkleRoot != nil {
assert.Equal(t, tt.config.MintStrategy.VerkleRoot, deserializedConfig.MintStrategy.VerkleRoot)
} else {
assert.Nil(t, deserializedConfig.MintStrategy.VerkleRoot)
}
} else {
assert.Nil(t, deserializedConfig.MintStrategy)
}
})
}
}
// TestTokenConfigurationValidation specifically tests the validation logic
func TestTokenConfigurationValidation(t *testing.T) {
tests := []struct {
name string
config *TokenIntrinsicConfiguration
expectError bool
errorContains string
}{
{
name: "valid non-mintable token",
config: createNonMintableTestConfig(),
expectError: false,
},
{
name: "valid mintable token with authority",
config: createMintableTestConfig(),
expectError: false,
},
{
name: "invalid - mintable without mint strategy",
config: &TokenIntrinsicConfiguration{
Behavior: Mintable,
Supply: big.NewInt(1000000),
Name: "Invalid Token",
Symbol: "INVLD",
},
expectError: true,
errorContains: "mintable token must have mint strategy defined",
},
{
name: "invalid - divisible without units",
config: &TokenIntrinsicConfiguration{
Behavior: Divisible,
Supply: big.NewInt(1000000),
Name: "Invalid Token",
Symbol: "INVLD",
},
expectError: true,
errorContains: "divisible token must have units defined",
},
{
name: "invalid - expirable but not acceptable",
config: &TokenIntrinsicConfiguration{
Behavior: Expirable,
Supply: big.NewInt(1000000),
Name: "Invalid Token",
Symbol: "INVLD",
},
expectError: true,
errorContains: "expirable token must be acceptable",
},
{
name: "invalid - mint with proof but no proof basis",
config: &TokenIntrinsicConfiguration{
Behavior: Mintable,
MintStrategy: &TokenMintStrategy{
MintBehavior: MintWithProof,
ProofBasis: NoProofBasis,
},
Supply: big.NewInt(1000000),
Name: "Invalid Token",
Symbol: "INVLD",
},
expectError: true,
errorContains: "mint with proof must define proof basis",
},
{
name: "invalid - mint with authority but no authority",
config: &TokenIntrinsicConfiguration{
Behavior: Mintable,
MintStrategy: &TokenMintStrategy{
MintBehavior: MintWithAuthority,
},
Supply: big.NewInt(1000000),
Name: "Invalid Token",
Symbol: "INVLD",
},
expectError: true,
errorContains: "mint with authority/signature must define authority",
},
{
name: "invalid - mint with payment but no payment address",
config: &TokenIntrinsicConfiguration{
Behavior: Mintable,
MintStrategy: &TokenMintStrategy{
MintBehavior: MintWithPayment,
FeeBasis: &FeeBasis{
Type: PerUnit,
Baseline: big.NewInt(100),
},
},
Supply: big.NewInt(1000000),
Name: "Invalid Token",
Symbol: "INVLD",
},
expectError: true,
errorContains: "mint with payment must define payment address",
},
{
name: "invalid - mint with payment but no fee basis",
config: &TokenIntrinsicConfiguration{
Behavior: Mintable,
MintStrategy: &TokenMintStrategy{
MintBehavior: MintWithPayment,
PaymentAddress: make([]byte, 32),
},
Supply: big.NewInt(1000000),
Name: "Invalid Token",
Symbol: "INVLD",
},
expectError: true,
errorContains: "mint with payment must define fee basis",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validateTokenConfiguration(tt.config)
if tt.expectError {
assert.Error(t, err)
if tt.errorContains != "" {
assert.Contains(t, err.Error(), tt.errorContains)
}
} else {
assert.NoError(t, err)
}
})
}
}